Skip to content

Commit a79fb81

Browse files
author
Chris Rossi
authored
Merge pull request #30 from chrisrossi/context-changes-in-tasklets
Handle contexts being changed internally to a tasklet.
2 parents a8be4e2 + 09949c2 commit a79fb81

File tree

16 files changed

+147
-153
lines changed

16 files changed

+147
-153
lines changed

packages/google-cloud-ndb/src/google/cloud/ndb/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@
125125
from google.cloud.ndb.context import AutoBatcher
126126
from google.cloud.ndb.context import Context
127127
from google.cloud.ndb.context import ContextOptions
128+
from google.cloud.ndb.context import get_context
128129
from google.cloud.ndb.context import TransactionOptions
129130
from google.cloud.ndb._datastore_api import EVENTUAL
130131
from google.cloud.ndb._datastore_api import EVENTUAL_CONSISTENCY
@@ -206,7 +207,6 @@
206207
from google.cloud.ndb.query import RepeatedStructuredPropertyPredicate
207208
from google.cloud.ndb.tasklets import add_flow_exception
208209
from google.cloud.ndb.tasklets import Future
209-
from google.cloud.ndb.tasklets import get_context
210210
from google.cloud.ndb.tasklets import make_context
211211
from google.cloud.ndb.tasklets import make_default_context
212212
from google.cloud.ndb.tasklets import QueueFuture

packages/google-cloud-ndb/src/google/cloud/ndb/_datastore_api.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
from google.cloud.datastore_v1.proto import datastore_pb2_grpc
2525
from google.cloud.datastore_v1.proto import entity_pb2
2626

27+
from google.cloud.ndb import context as context_module
2728
from google.cloud.ndb import _eventloop
28-
from google.cloud.ndb import _runstate
2929
from google.cloud.ndb import tasklets
3030

3131
EVENTUAL = datastore_pb2.ReadOptions.EVENTUAL
@@ -42,8 +42,8 @@ def stub():
4242
:class:`~google.cloud.datastore_v1.proto.datastore_pb2_grpc.DatastoreStub`:
4343
The stub instance.
4444
"""
45-
state = _runstate.current()
46-
return state.stub
45+
context = context_module.get_context()
46+
return context.stub
4747

4848

4949
def make_stub(client):
@@ -126,7 +126,7 @@ def lookup(key, **options):
126126
def _get_batch(batch_cls, options):
127127
"""Gets a data structure for storing batched calls to Datastore Lookup.
128128
129-
The batch data structure is stored in the current run state. If there is
129+
The batch data structure is stored in the current context. If there is
130130
not already a batch started, a new structure is created and an idle
131131
callback is added to the current event loop which will eventually perform
132132
the batch look up.
@@ -141,10 +141,10 @@ def _get_batch(batch_cls, options):
141141
Returns:
142142
batch_cls: An instance of the batch class.
143143
"""
144-
state = _runstate.current()
145-
batches = state.batches.get(batch_cls)
144+
context = context_module.get_context()
145+
batches = context.batches.get(batch_cls)
146146
if batches is None:
147-
state.batches[batch_cls] = batches = {}
147+
context.batches[batch_cls] = batches = {}
148148

149149
options_key = tuple(sorted(options.items()))
150150
batch = batches.get(options_key)
@@ -266,7 +266,7 @@ def _datastore_lookup(keys, read_options):
266266
Returns:
267267
RemoteCall: Future object for eventual result of lookup.
268268
"""
269-
client = _runstate.current().client
269+
client = context_module.get_context().client
270270
request = datastore_pb2.LookupRequest(
271271
project_id=client.project,
272272
keys=[key for key in keys],
@@ -323,8 +323,8 @@ def _get_transaction(options):
323323
Returns:
324324
Union[bytes, NoneType]: The transaction identifier, or :data:`None`.
325325
"""
326-
state = _runstate.current()
327-
return options.get("transaction", state.transaction)
326+
context = context_module.get_context()
327+
return options.get("transaction", context.transaction)
328328

329329

330330
def put(entity_pb, **options):
@@ -445,7 +445,7 @@ def _datastore_commit(mutations, transaction):
445445
else:
446446
mode = datastore_pb2.CommitRequest.TRANSACTIONAL
447447

448-
client = _runstate.current().client
448+
client = context_module.get_context().client
449449
request = datastore_pb2.CommitRequest(
450450
project_id=client.project,
451451
mode=mode,

packages/google-cloud-ndb/src/google/cloud/ndb/_eventloop.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import uuid
2222
import time
2323

24-
from google.cloud.ndb import _runstate
24+
from google.cloud.ndb import context as context_module
2525

2626
__all__ = [
2727
"add_idle",
@@ -369,7 +369,7 @@ def get_event_loop():
369369
Returns:
370370
EventLoop: The event loop for the current context.
371371
"""
372-
context = _runstate.current()
372+
context = context_module.get_context()
373373
return context.eventloop
374374

375375

packages/google-cloud-ndb/src/google/cloud/ndb/_runstate.py

Lines changed: 0 additions & 61 deletions
This file was deleted.

packages/google-cloud-ndb/src/google/cloud/ndb/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def context(self):
119119
layer.
120120
"""
121121
context = context_module.Context(self)
122-
with context:
122+
with context.use():
123123
yield context
124124

125125
# Finish up any work left to do on the event loop

packages/google-cloud-ndb/src/google/cloud/ndb/context.py

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,60 @@
1515
"""Context for currently running tasks and transactions."""
1616

1717
import collections
18+
import contextlib
19+
import threading
1820

1921
from google.cloud.ndb import _datastore_api
2022
from google.cloud.ndb import _eventloop
2123
from google.cloud.ndb import exceptions
22-
from google.cloud.ndb import _runstate
2324

2425

25-
__all__ = ["AutoBatcher", "Context", "ContextOptions", "TransactionOptions"]
26+
__all__ = [
27+
"AutoBatcher",
28+
"Context",
29+
"ContextOptions",
30+
"get_context",
31+
"TransactionOptions",
32+
]
2633

2734

2835
_ContextTuple = collections.namedtuple(
2936
"_ContextTuple", ["client", "eventloop", "stub", "batches", "transaction"]
3037
)
3138

3239

40+
class _LocalState(threading.local):
41+
"""Thread local state."""
42+
43+
__slots__ = ("context",)
44+
45+
def __init__(self):
46+
self.context = None
47+
48+
49+
_state = _LocalState()
50+
51+
52+
def get_context():
53+
"""Get the current context.
54+
55+
This function should be called within a context established by
56+
:meth:`google.cloud.ndb.client.Client.context`.
57+
58+
Returns:
59+
Context: The current context.
60+
61+
Raises:
62+
.ContextError: If called outside of a context
63+
established by :meth:`google.cloud.ndb.client.Client.context`.
64+
"""
65+
context = _state.context
66+
if context:
67+
return context
68+
69+
raise exceptions.ContextError()
70+
71+
3372
class _Context(_ContextTuple):
3473
"""Current runtime state.
3574
@@ -39,10 +78,6 @@ class _Context(_ContextTuple):
3978
loop. A new context can be derived from an existing context using
4079
:meth:`new`.
4180
42-
``_Context`` instances can be used as context managers which push
43-
themselves onto the thread local stack in ``_runstate`` and then pop
44-
themselves back off on exit.
45-
4681
:class:`Context` is a subclass of :class:`_Context` which provides
4782
only publicly facing interface. The use of two classes is only to provide a
4883
distinction between public and private API.
@@ -82,17 +117,20 @@ def new(self, **kwargs):
82117
state.update(kwargs)
83118
return type(self)(**state)
84119

85-
def __enter__(self):
86-
_runstate.contexts.push(self)
87-
return self
120+
@contextlib.contextmanager
121+
def use(self):
122+
"""Use this context as the current context.
88123
89-
def __exit__(self, *exc_info):
90-
popped = _runstate.contexts.pop()
91-
92-
# If we've done this right, this will never happen. Including this
93-
# check in an abundance of caution.
94-
if popped is not self:
95-
raise RuntimeError("Contexts stack is corrupted")
124+
This method returns a context manager for use with the ``with``
125+
statement. Code inside the ``with`` context will see this context as
126+
the current context.
127+
"""
128+
prev_context = _state.context
129+
_state.context = self
130+
try:
131+
yield self
132+
finally:
133+
_state.context = prev_context
96134

97135

98136
class Context(_Context):

packages/google-cloud-ndb/src/google/cloud/ndb/key.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,9 @@
9191
from google.cloud.datastore import key as _key_module
9292
import google.cloud.datastore
9393

94+
from google.cloud.ndb import context as context_module
9495
from google.cloud.ndb import _datastore_api
9596
from google.cloud.ndb import exceptions
96-
from google.cloud.ndb import _runstate
9797
from google.cloud.ndb import tasklets
9898

9999

@@ -137,7 +137,7 @@ class Key:
137137
from unittest import mock
138138
from google.cloud.ndb import context as context_module
139139
client = mock.Mock(project="testing", spec=("project",))
140-
context = context_module.Context(client, stub=mock.Mock(spec=()))
140+
context = context_module.Context(client, stub=mock.Mock(spec=())).use()
141141
context.__enter__()
142142
kind1, id1 = "Parent", "C"
143143
kind2, id2 = "Child", 42
@@ -816,7 +816,7 @@ def _project_from_app(app, allow_empty=False):
816816
if app is None:
817817
if allow_empty:
818818
return None
819-
client = _runstate.current().client
819+
client = context_module.get_context().client
820820
app = client.project
821821

822822
# NOTE: This is the same behavior as in the helper

packages/google-cloud-ndb/src/google/cloud/ndb/model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from google.cloud.ndb import context as context_module
2222
2323
client = mock.Mock(project="testing", spec=("project",))
24-
context = context_module.Context(client, stub=mock.Mock(spec=()))
24+
context = context_module.Context(client, stub=mock.Mock(spec=())).use()
2525
context.__enter__()
2626
2727
.. testcleanup:: *

packages/google-cloud-ndb/src/google/cloud/ndb/tasklets.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,12 @@
2222

2323
import grpc
2424

25+
from google.cloud.ndb import context as context_module
2526
from google.cloud.ndb import _eventloop
26-
from google.cloud.ndb import _runstate
2727

2828
__all__ = [
2929
"add_flow_exception",
3030
"Future",
31-
"get_context",
3231
"make_context",
3332
"make_default_context",
3433
"QueueFuture",
@@ -248,14 +247,17 @@ def __init__(self, generator, context, info="Unknown"):
248247
def _advance_tasklet(self, send_value=None, error=None):
249248
"""Advance a tasklet one step by sending in a value or error."""
250249
try:
251-
with self.context:
250+
with self.context.use():
252251
# Send the next value or exception into the generator
253252
if error:
254253
self.generator.throw(type(error), error)
255254

256255
# send_value will be None if this is the first time
257256
yielded = self.generator.send(send_value)
258257

258+
# Context may have changed in tasklet
259+
self.context = context_module.get_context()
260+
259261
except StopIteration as stop:
260262
# Generator has signalled exit, get the return value. This tasklet
261263
# has finished.
@@ -380,7 +382,7 @@ def tasklet_wrapper(*args, **kwargs):
380382
# and create a future object and set the result to the function's
381383
# return value so that from the user perspective there is no problem.
382384
# This permissive behavior is inherited from legacy NDB.
383-
context = _runstate.current()
385+
context = context_module.get_context()
384386

385387
try:
386388
returned = wrapped(*args, **kwargs)
@@ -470,10 +472,6 @@ def add_flow_exception(*args, **kwargs):
470472
raise NotImplementedError
471473

472474

473-
def get_context(*args, **kwargs):
474-
raise NotImplementedError
475-
476-
477475
def make_context(*args, **kwargs):
478476
raise NotImplementedError
479477

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from google.cloud import environment_vars
2626
from google.cloud.ndb import context as context_module
2727
from google.cloud.ndb import model
28-
from google.cloud.ndb import _runstate
2928

3029
import pytest
3130

@@ -45,7 +44,6 @@ def reset_state(environ):
4544
yield
4645
model.Property._FIND_METHODS_CACHE.clear()
4746
model.Model._kind_map.clear()
48-
del _runstate.contexts.stack[:]
4947

5048

5149
@pytest.fixture
@@ -84,5 +82,7 @@ def context():
8482

8583
@pytest.fixture
8684
def in_context(context):
87-
with context:
85+
assert not context_module._state.context
86+
with context.use():
8887
yield context
88+
assert not context_module._state.context

0 commit comments

Comments
 (0)