Skip to content

Commit

Permalink
Write unittests, minor debugging.
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris Rossi committed Jul 30, 2019
1 parent 18623c4 commit 27471ff
Show file tree
Hide file tree
Showing 12 changed files with 985 additions and 43 deletions.
14 changes: 7 additions & 7 deletions src/google/cloud/ndb/_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class ContextCache(collections.UserDict):
returning a result, in order to handle cases where the entity's key was
modified but the cache's key was not updated.
"""

def get_and_validate(self, key):
"""Verify that the entity's key has not changed since it was added
to the cache. If it has changed, consider this a cache miss.
Expand Down Expand Up @@ -137,6 +138,7 @@ class _InProcessGlobalCache(GlobalCache):
Relies on atomicity of ``__setitem__`` for thread safety. See:
http://effbot.org/pyfaq/what-kinds-of-global-value-mutation-are-thread-safe.htm
"""

def __init__(self):
self._watch_keys = {}

Expand All @@ -148,29 +150,29 @@ def get(self, keys):
for result in results:
if result is not None:
entity_pb, expires = result
if expires and expires > now:
if expires and expires < now:
entity_pb = None
else:
entity_pb = None

entity_pbs.append(entity_pb)

return entity_pbs
return _future_result(entity_pbs)

def set(self, items, expires=None):
"""Implements :meth:`GlobalCache.set`."""
if expires:
expires = time.time() + expires

for key, value in items.items():
self.cache[key] = (value, expires) # Supposedly threadsafe
self.cache[key] = (value, expires) # Supposedly threadsafe

return _future_result(None)

def delete(self, keys):
"""Implements :meth:`GlobalCache.delete`."""
for key in keys:
self.cache.pop(key, None) # Threadsafe?
self.cache.pop(key, None) # Threadsafe?

return _future_result(None)

Expand All @@ -196,7 +198,6 @@ def compare_and_swap(self, items, expires=None):


class _GlobalCacheBatch:

def idle_callback(self):
"""Get keys from the global cache."""
cache_call = self.make_call()
Expand Down Expand Up @@ -299,10 +300,9 @@ def global_set(key, value, expires=None):
Returns:
tasklets.Future: Eventual result will be ``None``.
"""
options = {}
if expires:
options = {"expires": expires}
else:
options = None

batch = _batch.get_batch(_GlobalCacheSetBatch, options)
return batch.add(key, value)
Expand Down
9 changes: 6 additions & 3 deletions src/google/cloud/ndb/_datastore_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def lookup(key, options):
if use_global_cache:
cache_key = _cache.global_cache_key(key)
result = yield _cache.global_get(cache_key)
key_locked == _cache.is_locked_value(key)
key_locked = _cache.is_locked_value(result)
if not key_locked:
if result is not None:
entity_pb = entity_pb2.Entity()
Expand All @@ -161,8 +161,9 @@ def lookup(key, options):
if use_global_cache and not key_locked and entity_pb is not _NOT_FOUND:
expires = context._global_cache_timeout(key, options)
serialized = entity_pb.SerializeToString()
yield _cache.global_compare_and_swap(cache_key, serialized,
expires=expires)
yield _cache.global_compare_and_swap(
cache_key, serialized, expires=expires
)

return entity_pb

Expand Down Expand Up @@ -762,8 +763,10 @@ def _complete(key_pb):
A new key may be left incomplete so that the id can be allocated by the
database. A key is considered incomplete if the last element of the path
has neither a ``name`` or an ``id``.
Args:
key_pb (entity_pb2.Key): The key to check.
Returns:
boolean: :data:`True` if key is incomplete, otherwise :data:`False`.
"""
Expand Down
21 changes: 19 additions & 2 deletions src/google/cloud/ndb/_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ class Options:
"timeout",
"use_cache",
"use_global_cache",
"use_memcache", # backwards compatibility
"global_cache_timeout",
"memcache_timeout", # backwards compatibility
# Not yet implemented
"use_datastore",
# Might or might not implement
Expand Down Expand Up @@ -120,6 +118,25 @@ def __init__(self, config=None, **kwargs):
raise TypeError("Can't specify both 'deadline' and 'timeout'")
kwargs["timeout"] = deadline

memcache_timeout = kwargs.pop("memcache_timeout", None)
if memcache_timeout is not None:
global_cache_timeout = kwargs.get("global_cache_timeout")
if global_cache_timeout is not None:
raise TypeError(
"Can't specify both 'memcache_timeout' and "
"'global_cache_timeout'"
)
kwargs["global_cache_timeout"] = memcache_timeout

use_memcache = kwargs.pop("use_memcache", None)
if use_memcache is not None:
use_global_cache = kwargs.get("use_global_cache")
if use_global_cache is not None:
raise TypeError(
"Can't specify both 'use_memcache' and 'use_global_cache'"
)
kwargs["use_global_cache"] = use_memcache

for key in self.slots():
default = getattr(config, key, None) if config else None
setattr(self, key, kwargs.pop(key, default))
Expand Down
6 changes: 4 additions & 2 deletions src/google/cloud/ndb/_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def _transaction_async(context, callback, read_only=False):
read_only, retries=0
)

with context.new(transaction=transaction_id).use():
with context.new(transaction=transaction_id).use() as tx_context:
try:
# Run the callback
result = callback()
Expand All @@ -114,6 +114,8 @@ def _transaction_async(context, callback, read_only=False):
yield _datastore_api.rollback(transaction_id)
raise

tx_context._clear_global_cache()

return result


Expand Down Expand Up @@ -154,7 +156,7 @@ def transactional_async(
retries=_retry._DEFAULT_RETRIES, read_only=False, xg=True, propagation=None
):
"""A decorator to run a function in an async transaction.
Usage example:
@transactional_async(retries=1, read_only=False)
Expand Down
2 changes: 1 addition & 1 deletion src/google/cloud/ndb/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ def _clear_global_cache(self):
are affected.
"""
keys = [
_cache.global_cache_key(key)
_cache.global_cache_key(key._key)
for key in self.cache
if self._use_global_cache(key)
]
Expand Down
4 changes: 1 addition & 3 deletions src/google/cloud/ndb/tasklets.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,7 @@ def _advance_tasklet(self, send_value=None, error=None):
# Send the next value or exception into the generator
if error:
self.generator.throw(
type(error),
error,
error.__traceback__
type(error), error, error.__traceback__
)

# send_value will be None if this is the first time
Expand Down
11 changes: 11 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,14 @@ def in_context(context):
with context.use():
yield context
assert not context_module._state.context


@pytest.fixture
def global_cache(context):
assert not context_module._state.context

global_cache = _cache._InProcessGlobalCache()
with context.new(global_cache=global_cache).use():
yield global_cache

assert not context_module._state.context
46 changes: 46 additions & 0 deletions tests/unit/test__batch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest

from google.cloud.ndb import _batch
from google.cloud.ndb import _eventloop


@pytest.mark.usefixtures("in_context")
class Test_get_batch:
def test_it(self):
options = {"foo": "bar"}
batch = _batch.get_batch(MockBatch, options)
assert batch.options is options
assert not batch.idle_called

different_options = {"food": "barn"}
assert _batch.get_batch(MockBatch, different_options) is not batch

assert _batch.get_batch(MockBatch) is not batch

assert _batch.get_batch(MockBatch, options) is batch

_eventloop.run()
assert batch.idle_called


class MockBatch:
def __init__(self, options):
self.options = options
self.idle_called = False

def idle_callback(self):
self.idle_called = True
Loading

0 comments on commit 27471ff

Please sign in to comment.