Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement cache policy. #116

Merged
merged 1 commit into from
Jun 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
("py:class", "google.cloud.datastore_v1.proto.entity_pb2.Entity"),
("py:class", "_datastore_query.Cursor"),
("py:meth", "_datastore_query.Cursor.urlsafe"),
("py:class", "google.cloud.ndb.context._Context"),
("py:class", "google.cloud.ndb.metadata._BaseMetadata"),
("py:class", "google.cloud.ndb._options.ReadOptions"),
("py:class", "QueryIterator"),
Expand Down
7 changes: 7 additions & 0 deletions docs/context.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#######
Context
#######

.. automodule:: google.cloud.ndb.context
:members:
:show-inheritance:
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
:maxdepth: 2

client
context
key
model
query
Expand Down
9 changes: 7 additions & 2 deletions src/google/cloud/ndb/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def __init__(self, project=None, namespace=None, credentials=None):
self.secure = not emulator

@contextlib.contextmanager
def context(self):
def context(self, cache_policy=None):
"""Establish a context for a set of NDB calls.

This method provides a context manager which establishes the runtime
Expand Down Expand Up @@ -121,8 +121,13 @@ def context(self):
In a web application, it is recommended that a single context be used
per HTTP request. This can typically be accomplished in a middleware
layer.

Arguments:
cache_policy (Optional[Callable[[key.Key], bool]]): The
cache policy to use in this context. See:
:meth:`~google.cloud.ndb.context.Context.set_cache_policy`.
"""
context = context_module.Context(self)
context = context_module.Context(self, cache_policy=cache_policy)
with context.use():
yield context

Expand Down
121 changes: 76 additions & 45 deletions src/google/cloud/ndb/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from google.cloud.ndb import _datastore_api
from google.cloud.ndb import _eventloop
from google.cloud.ndb import exceptions
from google.cloud.ndb import model


__all__ = [
Expand All @@ -32,20 +33,6 @@
]


_ContextTuple = collections.namedtuple(
"_ContextTuple",
[
"client",
"eventloop",
"stub",
"batches",
"commit_batches",
"transaction",
"cache",
],
)


class _LocalState(threading.local):
"""Thread local state."""

Expand Down Expand Up @@ -97,6 +84,41 @@ def get_and_validate(self, key):
raise KeyError(key)


def _default_cache_policy(key):
"""The default cache policy.

Defers to ``_use_cache`` on the Model class for the key's kind.

See: :meth:`~google.cloud.ndb.context.Context.set_cache_policy`
"""
flag = None
if key is not None:
modelclass = model.Model._kind_map.get(key.kind())
if modelclass is not None:
policy = getattr(modelclass, "_use_cache", None)
if policy is not None:
if isinstance(policy, bool):
flag = policy
else:
flag = policy(key)

return flag


_ContextTuple = collections.namedtuple(
"_ContextTuple",
[
"client",
"eventloop",
"stub",
"batches",
"commit_batches",
"transaction",
"cache",
],
)


class _Context(_ContextTuple):
"""Current runtime state.

Expand All @@ -106,8 +128,8 @@ class _Context(_ContextTuple):
loop. A new context can be derived from an existing context using
:meth:`new`.

:class:`Context` is a subclass of :class:`_Context` which provides
only publicly facing interface. The use of two classes is only to provide a
:class:`Context` is a subclass of :class:`_Context` which provides only
publicly facing interface. The use of two classes is only to provide a
distinction between public and private API.

Arguments:
Expand All @@ -123,6 +145,7 @@ def __new__(
commit_batches=None,
transaction=None,
cache=None,
cache_policy=None,
):
if eventloop is None:
eventloop = _eventloop.EventLoop()
Expand All @@ -145,7 +168,7 @@ def __new__(
else:
cache = _Cache()

return super(_Context, cls).__new__(
context = super(_Context, cls).__new__(
cls,
client=client,
eventloop=eventloop,
Expand All @@ -156,6 +179,10 @@ def __new__(
cache=cache,
)

context.set_cache_policy(cache_policy)

return context

def new(self, **kwargs):
"""Create a new :class:`_Context` instance.

Expand Down Expand Up @@ -205,9 +232,9 @@ def get_cache_policy(self):
Callable: A function that accepts a
:class:`~google.cloud.ndb.key.Key` instance as a single
positional argument and returns a ``bool`` indicating if it
should be cached. May be :data:`None`.
should be cached. May be :data:`None`.
"""
raise NotImplementedError
return self.cache_policy

def get_datastore_policy(self):
"""Return the current context datastore policy function.
Expand Down Expand Up @@ -238,7 +265,7 @@ def get_memcache_timeout_policy(self):
Callable: A function that accepts a
:class:`~google.cloud.ndb.key.Key` instance as a single
positional argument and returns an ``int`` indicating the
timeout, in seconds, for the key. :data:`0` implies the default
timeout, in seconds, for the key. ``0`` implies the default
timeout. May be :data:`None`.
"""
raise NotImplementedError
Expand All @@ -252,7 +279,16 @@ def set_cache_policy(self, policy):
positional argument and returns a ``bool`` indicating if it
should be cached. May be :data:`None`.
"""
raise NotImplementedError
if policy is None:
policy = _default_cache_policy

elif isinstance(policy, bool):
flag = policy

def policy(key):
return flag

self.cache_policy = policy

def set_datastore_policy(self, policy):
"""Set the context datastore policy function.
Expand Down Expand Up @@ -283,7 +319,7 @@ def set_memcache_timeout_policy(self, policy):
policy (Callable): A function that accepts a
:class:`~google.cloud.ndb.key.Key` instance as a single
positional argument and returns an ``int`` indicating the
timeout, in seconds, for the key. :data:`0` implies the default
timeout, in seconds, for the key. ``0`` implies the default
timout. May be :data:`None`.
"""
raise NotImplementedError
Expand Down Expand Up @@ -319,59 +355,45 @@ def in_transaction(self):
"""
return self.transaction is not None

@staticmethod
def default_cache_policy(key):
"""Default cache policy.

This defers to :meth:`~google.cloud.ndb.model.Model._use_cache`.

Args:
key (google.cloud.ndb.model.key.Key): The key.

Returns:
Union[bool, NoneType]: Whether to cache the key.
"""
raise NotImplementedError

@staticmethod
def default_datastore_policy(key):
"""Default cache policy.

This defers to :meth:`~google.cloud.ndb.model.Model._use_datastore`.
This defers to ``Model._use_datastore``.

Args:
key (google.cloud.ndb.model.key.Key): The key.
key (google.cloud.ndb.key.Key): The key.

Returns:
Union[bool, NoneType]: Whether to use datastore.
Union[bool, None]: Whether to use datastore.
"""
raise NotImplementedError

@staticmethod
def default_memcache_policy(key):
"""Default memcache policy.

This defers to :meth:`~google.cloud.ndb.model.Model._use_memcache`.
This defers to ``Model._use_memcache``.

Args:
key (google.cloud.ndb.model.key.Key): The key.
key (google.cloud.ndb.key.Key): The key.

Returns:
Union[bool, NoneType]: Whether to cache the key.
Union[bool, None]: Whether to cache the key.
"""
raise NotImplementedError

@staticmethod
def default_memcache_timeout_policy(key):
"""Default memcache timeout policy.

This defers to :meth:`~google.cloud.ndb.model.Model._memcache_timeout`.
This defers to ``Model._memcache_timeout``.

Args:
key (google.cloud.ndb.model.key.Key): The key.
key (google.cloud.ndb.key.Key): The key.

Returns:
Union[int, NoneType]: Memcache timeout to use.
Union[int, None]: Memcache timeout to use.
"""
raise NotImplementedError

Expand Down Expand Up @@ -416,6 +438,15 @@ def urlfetch(self, *args, **kwargs):
"""Fetch a resource using HTTP."""
raise NotImplementedError

def _use_cache(self, key, options):
"""Return whether to use the context cache for this key."""
flag = options.use_cache
if flag is None:
flag = self.cache_policy(key)
if flag is None:
flag = True
return flag


class ContextOptions:
__slots__ = ()
Expand Down
20 changes: 12 additions & 8 deletions src/google/cloud/ndb/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -842,12 +842,13 @@ def get_async(

@tasklets.tasklet
def get():
if _options.use_cache:
context = context_module.get_context()
use_cache = context._use_cache(self, _options)

if use_cache:
try:
# This result may be None, if None is cached for this key.
return context_module.get_context().cache.get_and_validate(
self
)
return context.cache.get_and_validate(self)
except KeyError:
pass

Expand All @@ -857,8 +858,8 @@ def get():
else:
result = None

if _options.use_cache:
context_module.get_context().cache[self] = result
if use_cache:
context.cache[self] = result

return result

Expand Down Expand Up @@ -971,8 +972,11 @@ def delete_async(
@tasklets.tasklet
def delete():
result = yield _datastore_api.delete(self._key, _options)
if _options.use_cache:
context_module.get_context().cache[self] = None

context = context_module.get_context()
if context._use_cache(self, _options):
context.cache[self] = None

return result

future = delete()
Expand Down
5 changes: 3 additions & 2 deletions src/google/cloud/ndb/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -4751,8 +4751,9 @@ def put(self):
ds_key = helpers.key_from_protobuf(key_pb)
self._key = key_module.Key._from_ds_key(ds_key)

if _options.use_cache:
context_module.get_context().cache[self._key] = self
context = context_module.get_context()
if context._use_cache(self._key, _options):
context.cache[self._key] = self

return self._key

Expand Down
4 changes: 2 additions & 2 deletions tests/system/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,5 @@ def namespace():
@pytest.fixture
def client_context(namespace):
client = ndb.Client(namespace=namespace)
with client.context():
yield
with client.context(cache_policy=False) as the_context:
yield the_context
Loading