Skip to content

Commit f2483e1

Browse files
odeke-emolavloite
andauthored
feat(x-goog-spanner-request-id): introduce AtomicCounter (#1275)
* feat(x-goog-spanner-request-id): introduce AtomicCounter This change introduces AtomicCounter, a concurrency/thread-safe counter do deal with the multi-threaded nature of variables. It permits operations: * atomic_counter += 1 * value = atomic_counter + 1 * atomic_counter.value that'll be paramount to bringing in the logic for x-goog-spanner-request-id in much reduced changelists. Updates #1261 Carved out from PR #1264 * Tests for with_request_id * chore: remove sleep * chore: remove unused import --------- Co-authored-by: Knut Olav Løite <koloite@gmail.com>
1 parent ad69c48 commit f2483e1

File tree

3 files changed

+164
-0
lines changed

3 files changed

+164
-0
lines changed

google/cloud/spanner_v1/_helpers.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import math
2020
import time
2121
import base64
22+
import threading
2223

2324
from google.protobuf.struct_pb2 import ListValue
2425
from google.protobuf.struct_pb2 import Value
@@ -30,6 +31,7 @@
3031
from google.cloud.spanner_v1 import TypeCode
3132
from google.cloud.spanner_v1 import ExecuteSqlRequest
3233
from google.cloud.spanner_v1 import JsonObject
34+
from google.cloud.spanner_v1.request_id_header import with_request_id
3335

3436
# Validation error messages
3537
NUMERIC_MAX_SCALE_ERR_MSG = (
@@ -525,3 +527,45 @@ def _metadata_with_leader_aware_routing(value, **kw):
525527
List[Tuple[str, str]]: RPC metadata with leader aware routing header
526528
"""
527529
return ("x-goog-spanner-route-to-leader", str(value).lower())
530+
531+
532+
class AtomicCounter:
533+
def __init__(self, start_value=0):
534+
self.__lock = threading.Lock()
535+
self.__value = start_value
536+
537+
@property
538+
def value(self):
539+
with self.__lock:
540+
return self.__value
541+
542+
def increment(self, n=1):
543+
with self.__lock:
544+
self.__value += n
545+
return self.__value
546+
547+
def __iadd__(self, n):
548+
"""
549+
Defines the inplace += operator result.
550+
"""
551+
with self.__lock:
552+
self.__value += n
553+
return self
554+
555+
def __add__(self, n):
556+
"""
557+
Defines the result of invoking: value = AtomicCounter + addable
558+
"""
559+
with self.__lock:
560+
n += self.__value
561+
return n
562+
563+
def __radd__(self, n):
564+
"""
565+
Defines the result of invoking: value = addable + AtomicCounter
566+
"""
567+
return self.__add__(n)
568+
569+
570+
def _metadata_with_request_id(*args, **kwargs):
571+
return with_request_id(*args, **kwargs)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright 2024 Google LLC All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
17+
REQ_ID_VERSION = 1 # The version of the x-goog-spanner-request-id spec.
18+
REQ_ID_HEADER_KEY = "x-goog-spanner-request-id"
19+
20+
21+
def generate_rand_uint64():
22+
b = os.urandom(8)
23+
return (
24+
b[7] & 0xFF
25+
| (b[6] & 0xFF) << 8
26+
| (b[5] & 0xFF) << 16
27+
| (b[4] & 0xFF) << 24
28+
| (b[3] & 0xFF) << 32
29+
| (b[2] & 0xFF) << 36
30+
| (b[1] & 0xFF) << 48
31+
| (b[0] & 0xFF) << 56
32+
)
33+
34+
35+
REQ_RAND_PROCESS_ID = generate_rand_uint64()
36+
37+
38+
def with_request_id(client_id, channel_id, nth_request, attempt, other_metadata=[]):
39+
req_id = f"{REQ_ID_VERSION}.{REQ_RAND_PROCESS_ID}.{client_id}.{channel_id}.{nth_request}.{attempt}"
40+
all_metadata = other_metadata.copy()
41+
all_metadata.append((REQ_ID_HEADER_KEY, req_id))
42+
return all_metadata

tests/unit/test_atomic_counter.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright 2024 Google LLC All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import random
16+
import threading
17+
import unittest
18+
from google.cloud.spanner_v1._helpers import AtomicCounter
19+
20+
21+
class TestAtomicCounter(unittest.TestCase):
22+
def test_initialization(self):
23+
ac_default = AtomicCounter()
24+
assert ac_default.value == 0
25+
26+
ac_1 = AtomicCounter(1)
27+
assert ac_1.value == 1
28+
29+
ac_negative_1 = AtomicCounter(-1)
30+
assert ac_negative_1.value == -1
31+
32+
def test_increment(self):
33+
ac = AtomicCounter()
34+
result_default = ac.increment()
35+
assert result_default == 1
36+
assert ac.value == 1
37+
38+
result_with_value = ac.increment(2)
39+
assert result_with_value == 3
40+
assert ac.value == 3
41+
result_plus_100 = ac.increment(100)
42+
assert result_plus_100 == 103
43+
44+
def test_plus_call(self):
45+
ac = AtomicCounter()
46+
ac += 1
47+
assert ac.value == 1
48+
49+
n = ac + 2
50+
assert n == 3
51+
assert ac.value == 1
52+
53+
n = 200 + ac
54+
assert n == 201
55+
assert ac.value == 1
56+
57+
def test_multiple_threads_incrementing(self):
58+
ac = AtomicCounter()
59+
n = 200
60+
m = 10
61+
62+
def do_work():
63+
for i in range(m):
64+
ac.increment()
65+
66+
threads = []
67+
for i in range(n):
68+
th = threading.Thread(target=do_work)
69+
threads.append(th)
70+
th.start()
71+
72+
random.shuffle(threads)
73+
for th in threads:
74+
th.join()
75+
assert not th.is_alive()
76+
77+
# Finally the result should be n*m
78+
assert ac.value == n * m

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy