Skip to content

Commit 9f60e4c

Browse files
author
Chris Rossi
authored
fix: use thread-safe iterator to generate context ids (#716)
Fixes #715
1 parent b75df9c commit 9f60e4c

File tree

2 files changed

+56
-17
lines changed

2 files changed

+56
-17
lines changed

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

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,30 +28,36 @@
2828
from google.cloud.ndb import key as key_module
2929

3030

31-
def _generate_context_ids():
32-
"""Generate a sequence of context ids.
31+
class _ContextIds:
32+
"""Iterator which generates a sequence of context ids.
3333
3434
Useful for debugging complicated interactions among concurrent processes and
3535
threads.
3636
37-
The return value is a generator for strings that include the machine's "node",
38-
acquired via `uuid.getnode()`, the current process id, and a sequence number which
39-
increases monotonically starting from one in each process. The combination of all
40-
three is sufficient to uniquely identify the context in which a particular piece of
41-
code is being run. Each context, as it is created, is assigned the next id in this
42-
sequence. The context id is used by `utils.logging_debug` to grant insight into
43-
where a debug logging statement is coming from in a cloud evironment.
44-
45-
Returns:
46-
Generator[str]: Sequence of context ids.
37+
Each value in the sequence is a string that include the machine's "node", acquired
38+
via `uuid.getnode()`, the current process id, and a sequence number which increases
39+
monotonically starting from one in each process. The combination of all three is
40+
sufficient to uniquely identify the context in which a particular piece of code is
41+
being run. Each context, as it is created, is assigned the next id in this sequence.
42+
The context id is used by `utils.logging_debug` to grant insight into where a debug
43+
logging statement is coming from in a cloud evironment.
4744
"""
48-
prefix = "{}-{}-".format(uuid.getnode(), os.getpid())
49-
for sequence_number in itertools.count(1): # pragma NO BRANCH
50-
# pragma is required because this loop never exits (infinite sequence)
51-
yield prefix + str(sequence_number)
45+
46+
def __init__(self):
47+
self.prefix = "{}-{}-".format(uuid.getnode(), os.getpid())
48+
self.counter = itertools.count(1)
49+
self.lock = threading.Lock()
50+
51+
def __next__(self):
52+
with self.lock:
53+
sequence_number = next(self.counter)
54+
55+
return self.prefix + str(sequence_number)
56+
57+
next = __next__ # Python 2.7
5258

5359

54-
_context_ids = _generate_context_ids()
60+
_context_ids = _ContextIds()
5561

5662

5763
try: # pragma: NO PY2 COVER

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import pytest
16+
import threading
1617

1718
try:
1819
from unittest import mock
@@ -75,6 +76,38 @@ def test_constructor_defaults(self):
7576
assert context.batches == {}
7677
assert context.transaction is None
7778

79+
node1, pid1, sequence_no1 = context.id.split("-")
80+
node2, pid2, sequence_no2 = context_module.Context("client").id.split("-")
81+
assert node1 == node2
82+
assert pid1 == pid2
83+
assert int(sequence_no2) - int(sequence_no1) == 1
84+
85+
def test_constructuor_concurrent_instantiation(self):
86+
"""Regression test for #716
87+
88+
This test non-deterministically tests a potential concurrency issue. Before the
89+
bug this is a test for was fixed, it failed most of the time.
90+
91+
https://github.com/googleapis/python-ndb/issues/715
92+
"""
93+
errors = []
94+
95+
def make_some():
96+
try:
97+
for _ in range(10000):
98+
context_module.Context("client")
99+
except Exception as error: # pragma: NO COVER
100+
errors.append(error)
101+
102+
thread1 = threading.Thread(target=make_some)
103+
thread2 = threading.Thread(target=make_some)
104+
thread1.start()
105+
thread2.start()
106+
thread1.join()
107+
thread2.join()
108+
109+
assert not errors
110+
78111
def test_constructor_overrides(self):
79112
context = context_module.Context(
80113
client="client",

0 commit comments

Comments
 (0)