Skip to content

Commit 15f3c1d

Browse files
author
Chris Rossi
authored
fix: Fix bug with the _GlobalCacheGetBatch. (#305)
@VladNF found a bug (and the solution) that caused results from calls to the global cache to get out of synch with the futures waiting on them. Fixes #294.
1 parent 0289d5a commit 15f3c1d

File tree

4 files changed

+81
-53
lines changed

4 files changed

+81
-53
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def done_callback(self, cache_call):
169169
def make_call(self):
170170
"""Call :method:`GlobalCache.get`."""
171171
cache = context_module.get_context().global_cache
172-
return cache.get(self.todo.keys())
172+
return cache.get(self.keys)
173173

174174
def future_info(self, key):
175175
"""Generate info string for Future."""

packages/google-cloud-ndb/tests/system/conftest.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from google.cloud import datastore
99
from google.cloud import ndb
1010

11+
from google.cloud.ndb import global_cache as global_cache_module
12+
1113
from . import KIND, OTHER_KIND, OTHER_NAMESPACE
1214

1315

@@ -110,8 +112,8 @@ def make_entity(*key_args, **entity_kwargs):
110112

111113
@pytest.fixture
112114
def dispose_of(with_ds_client, to_delete):
113-
def delete_entity(ds_key):
114-
to_delete.append(ds_key)
115+
def delete_entity(*ds_keys):
116+
to_delete.extend(ds_keys)
115117

116118
return delete_entity
117119

@@ -126,3 +128,11 @@ def client_context(namespace):
126128
client = ndb.Client(namespace=namespace)
127129
with client.context(cache_policy=False, legacy_data=False) as the_context:
128130
yield the_context
131+
132+
133+
@pytest.fixture
134+
def redis_context(client_context):
135+
global_cache = global_cache_module.RedisCache.from_environment()
136+
with client_context.new(global_cache=global_cache).use() as context:
137+
context.set_global_cache_policy(None) # Use default
138+
yield context

packages/google-cloud-ndb/tests/system/test_crud.py

Lines changed: 65 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import functools
2020
import operator
2121
import os
22+
import random
2223
import threading
2324
import zlib
2425

@@ -118,7 +119,7 @@ class SomeKind(ndb.Model):
118119

119120

120121
@pytest.mark.skipif(not USE_REDIS_CACHE, reason="Redis is not configured")
121-
def test_retrieve_entity_with_redis_cache(ds_entity, client_context):
122+
def test_retrieve_entity_with_redis_cache(ds_entity, redis_context):
122123
entity_id = test_utils.system.unique_resource_id()
123124
ds_entity(KIND, entity_id, foo=42, bar="none", baz=b"night")
124125

@@ -127,29 +128,25 @@ class SomeKind(ndb.Model):
127128
bar = ndb.StringProperty()
128129
baz = ndb.StringProperty()
129130

130-
global_cache = global_cache_module.RedisCache.from_environment()
131-
with client_context.new(global_cache=global_cache).use() as context:
132-
context.set_global_cache_policy(None) # Use default
131+
key = ndb.Key(KIND, entity_id)
132+
entity = key.get()
133+
assert isinstance(entity, SomeKind)
134+
assert entity.foo == 42
135+
assert entity.bar == "none"
136+
assert entity.baz == "night"
133137

134-
key = ndb.Key(KIND, entity_id)
138+
cache_key = _cache.global_cache_key(key._key)
139+
assert redis_context.global_cache.redis.get(cache_key) is not None
140+
141+
patch = mock.patch("google.cloud.ndb._datastore_api._LookupBatch.add")
142+
patch.side_effect = Exception("Shouldn't call this")
143+
with patch:
135144
entity = key.get()
136145
assert isinstance(entity, SomeKind)
137146
assert entity.foo == 42
138147
assert entity.bar == "none"
139148
assert entity.baz == "night"
140149

141-
cache_key = _cache.global_cache_key(key._key)
142-
assert global_cache.redis.get(cache_key) is not None
143-
144-
patch = mock.patch("google.cloud.ndb._datastore_api._LookupBatch.add")
145-
patch.side_effect = Exception("Shouldn't call this")
146-
with patch:
147-
entity = key.get()
148-
assert isinstance(entity, SomeKind)
149-
assert entity.foo == 42
150-
assert entity.bar == "none"
151-
assert entity.baz == "night"
152-
153150

154151
@pytest.mark.usefixtures("client_context")
155152
def test_retrieve_entity_not_found(ds_entity):
@@ -500,33 +497,29 @@ class SomeKind(ndb.Model):
500497

501498

502499
@pytest.mark.skipif(not USE_REDIS_CACHE, reason="Redis is not configured")
503-
def test_insert_entity_with_redis_cache(dispose_of, client_context):
500+
def test_insert_entity_with_redis_cache(dispose_of, redis_context):
504501
class SomeKind(ndb.Model):
505502
foo = ndb.IntegerProperty()
506503
bar = ndb.StringProperty()
507504

508-
global_cache = global_cache_module.RedisCache.from_environment()
509-
with client_context.new(global_cache=global_cache).use() as context:
510-
context.set_global_cache_policy(None) # Use default
511-
512-
entity = SomeKind(foo=42, bar="none")
513-
key = entity.put()
514-
dispose_of(key._key)
515-
cache_key = _cache.global_cache_key(key._key)
516-
assert global_cache.redis.get(cache_key) is None
505+
entity = SomeKind(foo=42, bar="none")
506+
key = entity.put()
507+
dispose_of(key._key)
508+
cache_key = _cache.global_cache_key(key._key)
509+
assert redis_context.global_cache.redis.get(cache_key) is None
517510

518-
retrieved = key.get()
519-
assert retrieved.foo == 42
520-
assert retrieved.bar == "none"
511+
retrieved = key.get()
512+
assert retrieved.foo == 42
513+
assert retrieved.bar == "none"
521514

522-
assert global_cache.redis.get(cache_key) is not None
515+
assert redis_context.global_cache.redis.get(cache_key) is not None
523516

524-
entity.foo = 43
525-
entity.put()
517+
entity.foo = 43
518+
entity.put()
526519

527-
# This is py27 behavior. I can see a case being made for caching the
528-
# entity on write rather than waiting for a subsequent lookup.
529-
assert global_cache.redis.get(cache_key) is None
520+
# This is py27 behavior. I can see a case being made for caching the
521+
# entity on write rather than waiting for a subsequent lookup.
522+
assert redis_context.global_cache.redis.get(cache_key) is None
530523

531524

532525
@pytest.mark.usefixtures("client_context")
@@ -671,7 +664,7 @@ class SomeKind(ndb.Model):
671664

672665

673666
@pytest.mark.skipif(not USE_REDIS_CACHE, reason="Redis is not configured")
674-
def test_delete_entity_with_redis_cache(ds_entity, client_context):
667+
def test_delete_entity_with_redis_cache(ds_entity, redis_context):
675668
entity_id = test_utils.system.unique_resource_id()
676669
ds_entity(KIND, entity_id, foo=42)
677670

@@ -680,19 +673,17 @@ class SomeKind(ndb.Model):
680673

681674
key = ndb.Key(KIND, entity_id)
682675
cache_key = _cache.global_cache_key(key._key)
683-
global_cache = global_cache_module.RedisCache.from_environment()
684676

685-
with client_context.new(global_cache=global_cache).use():
686-
assert key.get().foo == 42
687-
assert global_cache.redis.get(cache_key) is not None
677+
assert key.get().foo == 42
678+
assert redis_context.global_cache.redis.get(cache_key) is not None
688679

689-
assert key.delete() is None
690-
assert global_cache.redis.get(cache_key) is None
680+
assert key.delete() is None
681+
assert redis_context.global_cache.redis.get(cache_key) is None
691682

692-
# This is py27 behavior. Not entirely sold on leaving _LOCKED value for
693-
# Datastore misses.
694-
assert key.get() is None
695-
assert global_cache.redis.get(cache_key) == b"0"
683+
# This is py27 behavior. Not entirely sold on leaving _LOCKED value for
684+
# Datastore misses.
685+
assert key.get() is None
686+
assert redis_context.global_cache.redis.get(cache_key) == b"0"
696687

697688

698689
@pytest.mark.usefixtures("client_context")
@@ -1113,3 +1104,30 @@ class SomeKind(ndb.Model):
11131104

11141105
retreived = key.get()
11151106
assert retreived.foo == ["", ""]
1107+
1108+
1109+
@pytest.mark.usefixtures("redis_context")
1110+
def test_multi_get_weirdness_with_redis(dispose_of):
1111+
"""Regression test for issue #294.
1112+
1113+
https://github.com/googleapis/python-ndb/issues/294
1114+
"""
1115+
1116+
class SomeKind(ndb.Model):
1117+
foo = ndb.StringProperty()
1118+
1119+
objects = [SomeKind(foo=str(i)) for i in range(10)]
1120+
keys = ndb.put_multi(objects)
1121+
for key in keys:
1122+
dispose_of(key._key)
1123+
ndb.get_multi(keys)
1124+
1125+
one_object = random.choice(keys).get()
1126+
one_object.foo = "CHANGED"
1127+
one_object.put()
1128+
1129+
objects_upd = ndb.get_multi(keys)
1130+
keys_upd = [obj.key for obj in objects_upd]
1131+
assert len(keys_upd) == len(keys)
1132+
assert len(set(keys_upd)) == len(set(keys))
1133+
assert set(keys_upd) == set(keys)

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def test_add_and_idle_and_done_callbacks(in_context):
9898
with in_context.new(global_cache=cache).use():
9999
batch.idle_callback()
100100

101-
cache.get.assert_called_once_with(batch.todo.keys())
101+
cache.get.assert_called_once_with(batch.keys)
102102
assert future1.result() == b"one"
103103
assert future2.result() == b"two"
104104
assert future3.result() == b"one"
@@ -118,7 +118,7 @@ def test_add_and_idle_and_done_callbacks_synchronous(in_context):
118118
with in_context.new(global_cache=cache).use():
119119
batch.idle_callback()
120120

121-
cache.get.assert_called_once_with(batch.todo.keys())
121+
cache.get.assert_called_once_with(batch.keys)
122122
assert future1.result() == b"one"
123123
assert future2.result() == b"two"
124124

@@ -139,7 +139,7 @@ def test_add_and_idle_and_done_callbacks_w_error(in_context):
139139
with in_context.new(global_cache=cache).use():
140140
batch.idle_callback()
141141

142-
cache.get.assert_called_once_with(batch.todo.keys())
142+
cache.get.assert_called_once_with(batch.keys)
143143
assert future1.exception() is error
144144
assert future2.exception() is error
145145

0 commit comments

Comments
 (0)