Skip to content
This repository was archived by the owner on Mar 31, 2026. It is now read-only.

Commit 4f1da67

Browse files
committed
feat(x-goog-spanner-request-id): implement request_id generation and propagation
Generates a request_id that is then injected inside metadata that's sent over to the Cloud Spanner backend. Fixes #1261
1 parent 054a186 commit 4f1da67

File tree

7 files changed

+109
-1
lines changed

7 files changed

+109
-1
lines changed

google/cloud/spanner_v1/_helpers.py

Lines changed: 30 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
@@ -437,3 +438,32 @@ def _metadata_with_leader_aware_routing(value, **kw):
437438
List[Tuple[str, str]]: RPC metadata with leader aware routing header
438439
"""
439440
return ("x-goog-spanner-route-to-leader", str(value).lower())
441+
442+
443+
class AtomicInt:
444+
def __init__(self, start_value=0):
445+
self.__lock = threading.Lock()
446+
self.__value = start_value
447+
448+
def __iadd__(self, n):
449+
res = 0
450+
with self.__lock:
451+
res = self.__value
452+
res += n
453+
self.__value = res
454+
return res
455+
456+
def __add__(self, n):
457+
res = 0
458+
with self.__lock:
459+
res = self.__value
460+
res += 0
461+
return res
462+
463+
@property
464+
def value(self):
465+
with self.__lock:
466+
return self.__value
467+
468+
def increment(self, value=1):
469+
return self.__iadd__(value)

google/cloud/spanner_v1/client.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import grpc
2727
import os
2828
import warnings
29+
import threading
2930

3031
from google.api_core.gapic_v1 import client_info
3132
from google.auth.credentials import AnonymousCredentials
@@ -48,6 +49,7 @@
4849
from google.cloud.spanner_v1._helpers import _merge_query_options
4950
from google.cloud.spanner_v1._helpers import _metadata_with_prefix
5051
from google.cloud.spanner_v1.instance import Instance
52+
from google.cloud.spanner_v1._helpers import AtomicInt
5153

5254
_CLIENT_INFO = client_info.ClientInfo(client_library_version=__version__)
5355
EMULATOR_ENV_VAR = "SPANNER_EMULATOR_HOST"
@@ -147,6 +149,8 @@ class Client(ClientWithProject):
147149
SCOPE = (SPANNER_ADMIN_SCOPE,)
148150
"""The scopes required for Google Cloud Spanner."""
149151

152+
NTH_CLIENT = AtomicInt()
153+
150154
def __init__(
151155
self,
152156
project=None,
@@ -199,6 +203,12 @@ def __init__(
199203
self._route_to_leader_enabled = route_to_leader_enabled
200204
self._directed_read_options = directed_read_options
201205
self._observability_options = observability_options
206+
self._nth_client_id = Client.NTH_CLIENT.increment()
207+
self._nth_request = AtomicInt()
208+
209+
@property
210+
def _next_nth_request(self):
211+
return self._nth_request.increment()
202212

203213
@property
204214
def credentials(self):

google/cloud/spanner_v1/database.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from google.cloud.spanner_v1 import SpannerClient
5151
from google.cloud.spanner_v1._helpers import _merge_query_options
5252
from google.cloud.spanner_v1._helpers import (
53+
AtomicInt,
5354
_metadata_with_prefix,
5455
_metadata_with_leader_aware_routing,
5556
)
@@ -693,8 +694,15 @@ def execute_partitioned_dml(
693694
_metadata_with_leader_aware_routing(self._route_to_leader_enabled)
694695
)
695696

697+
nth_request = self._next_nth_request()
698+
attempt = AtomicInt(1) # It'll be incremented inside _restart_on_unavailable
699+
696700
def execute_pdml():
697701
with SessionCheckout(self._pool) as session:
702+
channel_id = session._channel_id
703+
metadata = with_request_id(
704+
self._client._nth_client_id, nth_request, attempt.value, metadata
705+
)
698706
txn = api.begin_transaction(
699707
session=session.name, options=txn_options, metadata=metadata
700708
)
@@ -719,6 +727,7 @@ def execute_pdml():
719727
request=request,
720728
transaction_selector=txn_selector,
721729
observability_options=self.observability_options,
730+
attempt=attempt,
722731
)
723732

724733
result_set = StreamedResultSet(iterator)
@@ -728,6 +737,9 @@ def execute_pdml():
728737

729738
return _retry_on_aborted(execute_pdml, DEFAULT_RETRY_BACKOFF)()
730739

740+
def _next_nth_request(self):
741+
return self._instance._client._next_nth_request
742+
731743
def session(self, labels=None, database_role=None):
732744
"""Factory to create a session for this database.
733745

google/cloud/spanner_v1/pool.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import datetime
1818
import queue
19+
import threading
1920

2021
from google.cloud.exceptions import NotFound
2122
from google.cloud.spanner_v1 import BatchCreateSessionsRequest
@@ -47,6 +48,8 @@ def __init__(self, labels=None, database_role=None):
4748
labels = {}
4849
self._labels = labels
4950
self._database_role = database_role
51+
self.__lock = threading.lock()
52+
self._session_id_to_channel_id = dict()
5053

5154
@property
5255
def labels(self):
@@ -122,10 +125,17 @@ def _new_session(self):
122125
:rtype: :class:`~google.cloud.spanner_v1.session.Session`
123126
:returns: new session instance.
124127
"""
125-
return self._database.session(
128+
session = self._database.session(
126129
labels=self.labels, database_role=self.database_role
127130
)
128131

132+
with self.__lock:
133+
channel_id = len(self._session_id_to_channel_id) + 1
134+
self._session_id_to_channel_id[session._session.id] = channel_id
135+
session._channel_id = channel_id
136+
137+
return session
138+
129139
def session(self, **kwargs):
130140
"""Check out a session from the pool.
131141
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+
import threading
17+
18+
REQ_ID_VERSION = 1 # The version of the x-goog-spanner-request-id spec.
19+
REQ_ID_HEADER_KEY = "x-goog-spanner-request-id"
20+
21+
22+
def generate_rand_uint64():
23+
b = os.urandom(8)
24+
return (
25+
b[7] & 0xFF
26+
| (b[6] & 0xFF) << 8
27+
| (b[5] & 0xFF) << 16
28+
| (b[4] & 0xFF) << 24
29+
| (b[3] & 0xFF) << 32
30+
| (b[2] & 0xFF) << 36
31+
| (b[1] & 0xFF) << 48
32+
| (b[0] & 0xFF) << 56
33+
)
34+
35+
36+
REQ_RAND_PROCESS_ID = generate_rand_uint64()
37+
38+
39+
def with_request_id(client_id, nth_request, attempt, other_metadata=[]):
40+
req_id = f"{REQ_ID_VERSION}.{REQ_RAND_PROCESS_ID}.{client_id}.{channel_id}.{nth_request}.{attempt}"
41+
other_metadata.append((REQ_ID_HEADER_KEY, req_id))
42+
return other_metadata

google/cloud/spanner_v1/session.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def __init__(self, database, labels=None, database_role=None):
6969
labels = {}
7070
self._labels = labels
7171
self._database_role = database_role
72+
self.__channel_id = 0
7273

7374
def __lt__(self, other):
7475
return self._session_id < other._session_id
@@ -203,6 +204,7 @@ def delete(self):
203204
raise ValueError("Session ID not set by back-end")
204205
api = self._database.spanner_api
205206
metadata = _metadata_with_prefix(self._database.name)
207+
# Generate the request_id
206208
observability_options = getattr(self._database, "observability_options", None)
207209
with trace_call(
208210
"CloudSpanner.DeleteSession",

google/cloud/spanner_v1/snapshot.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def _restart_on_unavailable(
5757
transaction=None,
5858
transaction_selector=None,
5959
observability_options=None,
60+
attempt=0,
6061
):
6162
"""Restart iteration after :exc:`.ServiceUnavailable`.
6263
@@ -91,6 +92,7 @@ def _restart_on_unavailable(
9192
):
9293
iterator = method(request=request)
9394
while True:
95+
attempt += 1
9496
try:
9597
for item in iterator:
9698
item_buffer.append(item)

0 commit comments

Comments
 (0)