Skip to content

Commit afa4fdb

Browse files
author
Chris Rossi
authored
feat: don't flush entire global cache on transient errors (#654)
It turns out this was unnecessary and can cause problems of its own. Fixes #649 #634
1 parent 4de4bc3 commit afa4fdb

File tree

3 files changed

+30
-122
lines changed

3 files changed

+30
-122
lines changed

packages/google-cloud-ndb/google/cloud/ndb/_cache.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -143,15 +143,15 @@ def wrap(wrapped):
143143
def retry(wrapped, transient_errors):
144144
@functools.wraps(wrapped)
145145
@tasklets.tasklet
146-
def retry_wrapper(*args, **kwargs):
146+
def retry_wrapper(key, *args, **kwargs):
147147
sleep_generator = core_retry.exponential_sleep_generator(0.1, 1)
148148
attempts = 5
149149
for sleep_time in sleep_generator: # pragma: NO BRANCH
150150
# pragma is required because loop never exits normally, it only gets
151151
# raised out of.
152152
attempts -= 1
153153
try:
154-
result = yield wrapped(*args, **kwargs)
154+
result = yield wrapped(key, *args, **kwargs)
155155
raise tasklets.Return(result)
156156
except transient_errors:
157157
if not attempts:
@@ -163,7 +163,7 @@ def retry_wrapper(*args, **kwargs):
163163

164164
@functools.wraps(wrapped)
165165
@tasklets.tasklet
166-
def wrapper(*args, **kwargs):
166+
def wrapper(key, *args, **kwargs):
167167
cache = _global_cache()
168168

169169
is_read = read
@@ -177,17 +177,10 @@ def wrapper(*args, **kwargs):
177177
function = wrapped
178178

179179
try:
180-
if cache.clear_cache_soon:
181-
warnings.warn("Clearing global cache...", RuntimeWarning)
182-
cache.clear()
183-
cache.clear_cache_soon = False
184-
185-
result = yield function(*args, **kwargs)
180+
result = yield function(key, *args, **kwargs)
186181
raise tasklets.Return(result)
187182

188183
except cache.transient_errors as error:
189-
cache.clear_cache_soon = True
190-
191184
if strict:
192185
raise
193186

packages/google-cloud-ndb/google/cloud/ndb/global_cache.py

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ class GlobalCache(object):
8181
This should be overridden by subclasses.
8282
"""
8383

84-
clear_cache_soon = False
8584
strict_read = True
8685
strict_write = True
8786

@@ -267,13 +266,10 @@ class RedisCache(GlobalCache):
267266
the application. If :data:`True`, connection errors during write will be
268267
raised as exceptions in the application. Because write operations involve
269268
cache invalidation, setting this to :data:`False` may allow other clients to
270-
retrieve stale data from the cache. If there is a connection error, an
271-
internal flag will be set to clear the cache the next time any method is
272-
called on this object, to try and minimize the opportunity for clients to
273-
read stale data from the cache. If :data:`True`, in the event of connection
274-
errors, cache operations will be retried a number of times before eventually
275-
raising the connection error to the application layer, if it does not
276-
resolve after retrying. Setting this to :data:`True` will cause NDB
269+
retrieve stale data from the cache. If :data:`True`, in the event of
270+
connection errors, cache operations will be retried a number of times before
271+
eventually raising the connection error to the application layer, if it does
272+
not resolve after retrying. Setting this to :data:`True` will cause NDB
277273
operations to take longer to complete if there are transient errors in the
278274
cache layer. Default: :data:`True`.
279275
"""
@@ -309,10 +305,7 @@ def from_environment(cls, strict_read=False, strict_write=True):
309305
exception in the application. If :data:`True`, connection errors during
310306
write will be raised as exceptions in the application. Because write
311307
operations involve cache invalidation, setting this to :data:`False` may
312-
allow other clients to retrieve stale data from the cache. If there is
313-
a connection error, an internal flag will be set to clear the cache the
314-
next time any method is called on this object, to try and minimize the
315-
opportunity for clients to read stale data from the cache. If
308+
allow other clients to retrieve stale data from the cache. If
316309
:data:`True`, in the event of connection errors, cache operations will
317310
be retried a number of times before eventually raising the connection
318311
error to the application layer, if it does not resolve after retrying.
@@ -444,16 +437,12 @@ class MemcacheCache(GlobalCache):
444437
exception in the application. If :data:`True`, connection errors during
445438
write will be raised as exceptions in the application. Because write
446439
operations involve cache invalidation, setting this to :data:`False` may
447-
allow other clients to retrieve stale data from the cache. If there is
448-
a connection error, an internal flag will be set to clear the cache the
449-
next time any method is called on this object, to try and minimize the
450-
opportunity for clients to read stale data from the cache. If
451-
:data:`True`, in the event of connection errors, cache operations will
452-
be retried a number of times before eventually raising the connection
453-
error to the application layer, if it does not resolve after retrying.
454-
Setting this to :data:`True` will cause NDB operations to take longer to
455-
complete if there are transient errors in the cache layer. Default:
456-
:data:`True`.
440+
allow other clients to retrieve stale data from the cache. If :data:`True`,
441+
in the event of connection errors, cache operations will be retried a number
442+
of times before eventually raising the connection error to the application
443+
layer, if it does not resolve after retrying. Setting this to :data:`True`
444+
will cause NDB operations to take longer to complete if there are transient
445+
errors in the cache layer. Default: :data:`True`.
457446
"""
458447

459448
transient_errors = (
@@ -512,10 +501,7 @@ def from_environment(cls, max_pool_size=4, strict_read=False, strict_write=True)
512501
exception in the application. If :data:`True`, connection errors during
513502
write will be raised as exceptions in the application. Because write
514503
operations involve cache invalidation, setting this to :data:`False` may
515-
allow other clients to retrieve stale data from the cache. If there is
516-
a connection error, an internal flag will be set to clear the cache the
517-
next time any method is called on this object, to try and minimize the
518-
opportunity for clients to read stale data from the cache. If
504+
allow other clients to retrieve stale data from the cache. If
519505
:data:`True`, in the event of connection errors, cache operations will
520506
be retried a number of times before eventually raising the connection
521507
error to the application layer, if it does not resolve after retrying.

packages/google-cloud-ndb/tests/unit/test__cache.py

Lines changed: 14 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -104,69 +104,15 @@ def test_global_get(_batch, _global_cache):
104104
batch.add.return_value = future
105105
_global_cache.return_value = mock.Mock(
106106
transient_errors=(),
107-
clear_cache_soon=False,
108107
strict_read=False,
109-
spec=("transient_errors", "clear_cache_soon", "strict_read"),
108+
spec=("transient_errors", "strict_read"),
110109
)
111110

112111
assert _cache.global_get(b"foo").result() == "hi mom!"
113112
_batch.get_batch.assert_called_once_with(_cache._GlobalCacheGetBatch)
114113
batch.add.assert_called_once_with(b"foo")
115114

116115

117-
@pytest.mark.usefixtures("in_context")
118-
@mock.patch("google.cloud.ndb._cache._global_cache")
119-
@mock.patch("google.cloud.ndb._cache._batch")
120-
def test_global_get_clear_cache_soon(_batch, _global_cache):
121-
batch = _batch.get_batch.return_value
122-
future = _future_result("hi mom!")
123-
batch.add.return_value = future
124-
_global_cache.return_value = mock.Mock(
125-
transient_errors=(),
126-
clear_cache_soon=True,
127-
strict_read=False,
128-
spec=("transient_errors", "clear_cache_soon", "clear", "strict_read"),
129-
)
130-
131-
with warnings.catch_warnings(record=True) as logged:
132-
assert _cache.global_get(b"foo").result() == "hi mom!"
133-
assert len(logged) == 1
134-
135-
_batch.get_batch.assert_called_once_with(_cache._GlobalCacheGetBatch)
136-
batch.add.assert_called_once_with(b"foo")
137-
_global_cache.return_value.clear.assert_called_once_with()
138-
139-
140-
@pytest.mark.usefixtures("in_context")
141-
@mock.patch("google.cloud.ndb._cache._global_cache")
142-
@mock.patch("google.cloud.ndb._cache._batch")
143-
def test_global_get_clear_cache_soon_with_error(_batch, _global_cache):
144-
"""Regression test for #633
145-
146-
https://github.com/googleapis/python-ndb/issues/633
147-
"""
148-
149-
class TransientError(Exception):
150-
pass
151-
152-
batch = _batch.get_batch.return_value
153-
future = _future_result("hi mom!")
154-
batch.add.return_value = future
155-
_global_cache.return_value = mock.Mock(
156-
transient_errors=(TransientError),
157-
clear_cache_soon=True,
158-
strict_read=False,
159-
clear=mock.Mock(side_effect=TransientError("oops!"), spec=()),
160-
spec=("transient_errors", "clear_cache_soon", "clear", "strict_read"),
161-
)
162-
163-
with warnings.catch_warnings(record=True) as logged:
164-
assert _cache.global_get(b"foo").result() is None
165-
assert len(logged) == 2
166-
167-
_global_cache.return_value.clear.assert_called_once_with()
168-
169-
170116
@pytest.mark.usefixtures("in_context")
171117
@mock.patch("google.cloud.ndb.tasklets.sleep")
172118
@mock.patch("google.cloud.ndb._cache._global_cache")
@@ -181,17 +127,15 @@ class TransientError(Exception):
181127
batch.add.return_value = future
182128
_global_cache.return_value = mock.Mock(
183129
transient_errors=(TransientError,),
184-
clear_cache_soon=False,
185130
strict_read=True,
186-
spec=("transient_errors", "clear_cache_soon", "strict_read"),
131+
spec=("transient_errors", "strict_read"),
187132
)
188133

189134
with pytest.raises(TransientError):
190135
_cache.global_get(b"foo").result()
191136

192137
_batch.get_batch.assert_called_with(_cache._GlobalCacheGetBatch)
193138
batch.add.assert_called_with(b"foo")
194-
assert _global_cache.return_value.clear_cache_soon is True
195139

196140

197141
@pytest.mark.usefixtures("in_context")
@@ -210,15 +154,13 @@ class TransientError(Exception):
210154
]
211155
_global_cache.return_value = mock.Mock(
212156
transient_errors=(TransientError,),
213-
clear_cache_soon=False,
214157
strict_read=True,
215-
spec=("transient_errors", "clear_cache_soon", "strict_read"),
158+
spec=("transient_errors", "strict_read"),
216159
)
217160

218161
assert _cache.global_get(b"foo").result() == "hi mom!"
219162
_batch.get_batch.assert_called_with(_cache._GlobalCacheGetBatch)
220163
batch.add.assert_called_with(b"foo")
221-
assert _global_cache.return_value.clear_cache_soon is False
222164

223165

224166
@pytest.mark.usefixtures("in_context")
@@ -233,9 +175,8 @@ class TransientError(Exception):
233175
batch.add.return_value = future
234176
_global_cache.return_value = mock.Mock(
235177
transient_errors=(TransientError,),
236-
clear_cache_soon=False,
237178
strict_read=False,
238-
spec=("transient_errors", "clear_cache_soon", "strict_read"),
179+
spec=("transient_errors", "strict_read"),
239180
)
240181

241182
with warnings.catch_warnings(record=True) as logged:
@@ -325,9 +266,8 @@ def test_without_expires(_batch, _global_cache):
325266
batch.add.return_value = future
326267
_global_cache.return_value = mock.Mock(
327268
transient_errors=(),
328-
clear_cache_soon=False,
329269
strict_write=False,
330-
spec=("transient_errors", "clear_cache_soon", "strict_write"),
270+
spec=("transient_errors", "strict_write"),
331271
)
332272

333273
assert _cache.global_set(b"key", b"value").result() == "hi mom!"
@@ -348,16 +288,14 @@ class TransientError(Exception):
348288
batch.add.return_value = future
349289
_global_cache.return_value = mock.Mock(
350290
transient_errors=(TransientError,),
351-
clear_cache_soon=False,
352-
spec=("transient_errors", "clear_cache_soon", "strict_write"),
291+
spec=("transient_errors", "strict_write"),
353292
)
354293

355294
with pytest.raises(TransientError):
356295
_cache.global_set(b"key", b"value").result()
357296

358297
_batch.get_batch.assert_called_with(_cache._GlobalCacheSetBatch, {})
359298
batch.add.assert_called_with(b"key", b"value")
360-
assert _global_cache.return_value.clear_cache_soon is True
361299

362300
@staticmethod
363301
@mock.patch("google.cloud.ndb._cache._global_cache")
@@ -373,9 +311,8 @@ class TransientError(Exception):
373311
batch.add.return_value = future
374312
_global_cache.return_value = mock.Mock(
375313
transient_errors=(TransientError,),
376-
clear_cache_soon=False,
377314
strict_write=False,
378-
spec=("transient_errors", "clear_cache_soon", "strict_write"),
315+
spec=("transient_errors", "strict_write"),
379316
)
380317

381318
with warnings.catch_warnings(record=True) as logged:
@@ -384,7 +321,6 @@ class TransientError(Exception):
384321

385322
_batch.get_batch.assert_called_once_with(_cache._GlobalCacheSetBatch, {})
386323
batch.add.assert_called_once_with(b"key", b"value")
387-
assert _global_cache.return_value.clear_cache_soon is True
388324

389325
@staticmethod
390326
@mock.patch("google.cloud.ndb._cache._global_cache")
@@ -395,9 +331,8 @@ def test_with_expires(_batch, _global_cache):
395331
batch.add.return_value = future
396332
_global_cache.return_value = mock.Mock(
397333
transient_errors=(),
398-
clear_cache_soon=False,
399334
strict_write=False,
400-
spec=("transient_errors", "clear_cache_soon", "strict_write"),
335+
spec=("transient_errors", "strict_write"),
401336
)
402337

403338
future = _cache.global_set(b"key", b"value", expires=5)
@@ -475,9 +410,8 @@ def test_global_delete(_batch, _global_cache):
475410
batch.add.return_value = future
476411
_global_cache.return_value = mock.Mock(
477412
transient_errors=(),
478-
clear_cache_soon=False,
479413
strict_write=False,
480-
spec=("transient_errors", "clear_cache_soon", "strict_write"),
414+
spec=("transient_errors", "strict_write"),
481415
)
482416

483417
assert _cache.global_delete(b"key").result() == "hi mom!"
@@ -511,9 +445,8 @@ def test_global_watch(_batch, _global_cache):
511445
batch.add.return_value = future
512446
_global_cache.return_value = mock.Mock(
513447
transient_errors=(),
514-
clear_cache_soon=False,
515448
strict_read=False,
516-
spec=("transient_errors", "clear_cache_soon", "strict_read"),
449+
spec=("transient_errors", "strict_read"),
517450
)
518451

519452
assert _cache.global_watch(b"key").result() == "hi mom!"
@@ -547,9 +480,8 @@ def test_global_unwatch(_batch, _global_cache):
547480
batch.add.return_value = future
548481
_global_cache.return_value = mock.Mock(
549482
transient_errors=(),
550-
clear_cache_soon=False,
551483
strict_write=False,
552-
spec=("transient_errors", "clear_cache_soon", "strict_write"),
484+
spec=("transient_errors", "strict_write"),
553485
)
554486

555487
assert _cache.global_unwatch(b"key").result() == "hi mom!"
@@ -585,9 +517,8 @@ def test_without_expires(_batch, _global_cache):
585517
batch.add.return_value = future
586518
_global_cache.return_value = mock.Mock(
587519
transient_errors=(),
588-
clear_cache_soon=False,
589520
strict_read=False,
590-
spec=("transient_errors", "clear_cache_soon", "strict_read"),
521+
spec=("transient_errors", "strict_read"),
591522
)
592523

593524
future = _cache.global_compare_and_swap(b"key", b"value")
@@ -606,9 +537,8 @@ def test_with_expires(_batch, _global_cache):
606537
batch.add.return_value = future
607538
_global_cache.return_value = mock.Mock(
608539
transient_errors=(),
609-
clear_cache_soon=False,
610540
strict_read=False,
611-
spec=("transient_errors", "clear_cache_soon", "strict_read"),
541+
spec=("transient_errors", "strict_read"),
612542
)
613543

614544
future = _cache.global_compare_and_swap(b"key", b"value", expires=5)
@@ -668,9 +598,8 @@ def test_global_lock(_batch, _global_cache):
668598
batch.add.return_value = future
669599
_global_cache.return_value = mock.Mock(
670600
transient_errors=(),
671-
clear_cache_soon=False,
672601
strict_write=False,
673-
spec=("transient_errors", "clear_cache_soon", "strict_write"),
602+
spec=("transient_errors", "strict_write"),
674603
)
675604

676605
assert _cache.global_lock(b"key").result() == "hi mom!"

0 commit comments

Comments
 (0)