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

Commit 7fe169b

Browse files
committed
feat: add requestID info in error exceptions
1 parent 3b1792a commit 7fe169b

22 files changed

+867
-191
lines changed

google/cloud/spanner_v1/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
from .types.type import TypeCode
6666
from .data_types import JsonObject, Interval
6767
from .transaction import BatchTransactionId, DefaultTransactionOptions
68+
from .exceptions import SpannerError, wrap_with_request_id
6869

6970
from google.cloud.spanner_v1 import param_types
7071
from google.cloud.spanner_v1.client import Client
@@ -88,6 +89,9 @@
8889
# google.cloud.spanner_v1
8990
"__version__",
9091
"param_types",
92+
# google.cloud.spanner_v1.exceptions
93+
"SpannerError",
94+
"wrap_with_request_id",
9195
# google.cloud.spanner_v1.client
9296
"Client",
9397
# google.cloud.spanner_v1.keyset

google/cloud/spanner_v1/_helpers.py

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import threading
2323
import logging
2424
import uuid
25+
from contextlib import contextmanager
2526

2627
from google.protobuf.struct_pb2 import ListValue
2728
from google.protobuf.struct_pb2 import Value
@@ -34,8 +35,12 @@
3435
from google.cloud.spanner_v1.types import ExecuteSqlRequest
3536
from google.cloud.spanner_v1.types import TransactionOptions
3637
from google.cloud.spanner_v1.data_types import JsonObject, Interval
37-
from google.cloud.spanner_v1.request_id_header import with_request_id
38+
from google.cloud.spanner_v1.request_id_header import (
39+
with_request_id,
40+
with_request_id_metadata_only,
41+
)
3842
from google.cloud.spanner_v1.types import TypeCode
43+
from google.cloud.spanner_v1.exceptions import wrap_with_request_id
3944

4045
from google.rpc.error_details_pb2 import RetryInfo
4146

@@ -568,7 +573,10 @@ def _retry_on_aborted_exception(
568573
):
569574
"""
570575
Handles retry logic for Aborted exceptions, considering the deadline.
576+
Also handles SpannerError that wraps Aborted exceptions.
571577
"""
578+
from google.cloud.spanner_v1.exceptions import SpannerError
579+
572580
attempts = 0
573581
while True:
574582
try:
@@ -582,6 +590,17 @@ def _retry_on_aborted_exception(
582590
default_retry_delay=default_retry_delay,
583591
)
584592
continue
593+
except SpannerError as exc:
594+
# Check if the wrapped error is Aborted
595+
if isinstance(exc._error, Aborted):
596+
_delay_until_retry(
597+
exc._error,
598+
deadline=deadline,
599+
attempts=attempts,
600+
default_retry_delay=default_retry_delay,
601+
)
602+
continue
603+
raise
585604

586605

587606
def _retry(
@@ -600,10 +619,13 @@ def _retry(
600619
delay: The delay in seconds between retries.
601620
allowed_exceptions: A tuple of exceptions that are allowed to occur without triggering a retry.
602621
Passing allowed_exceptions as None will lead to retrying for all exceptions.
622+
Also handles SpannerError wrapping allowed exceptions.
603623
604624
Returns:
605625
The result of the function if it is successful, or raises the last exception if all retries fail.
606626
"""
627+
from google.cloud.spanner_v1.exceptions import SpannerError
628+
607629
retries = 0
608630
while retries <= retry_count:
609631
if retries > 0 and before_next_retry:
@@ -612,14 +634,23 @@ def _retry(
612634
try:
613635
return func()
614636
except Exception as exc:
615-
if (
616-
allowed_exceptions is None or exc.__class__ in allowed_exceptions
617-
) and retries < retry_count:
637+
# Check if exception is allowed directly or wrapped in SpannerError
638+
exc_to_check = exc
639+
if isinstance(exc, SpannerError):
640+
exc_to_check = exc._error
641+
642+
is_allowed = (
643+
allowed_exceptions is None
644+
or exc_to_check.__class__ in allowed_exceptions
645+
)
646+
647+
if is_allowed and retries < retry_count:
618648
if (
619649
allowed_exceptions is not None
620-
and allowed_exceptions[exc.__class__] is not None
650+
and exc_to_check.__class__ in allowed_exceptions
651+
and allowed_exceptions[exc_to_check.__class__] is not None
621652
):
622-
allowed_exceptions[exc.__class__](exc)
653+
allowed_exceptions[exc_to_check.__class__](exc_to_check)
623654
time.sleep(delay)
624655
delay = delay * 2
625656
retries = retries + 1
@@ -767,9 +798,67 @@ def reset(self):
767798

768799

769800
def _metadata_with_request_id(*args, **kwargs):
801+
"""Return metadata with request ID header.
802+
803+
This function returns only the metadata list (not a tuple),
804+
maintaining backward compatibility with existing code.
805+
806+
Args:
807+
*args: Arguments to pass to with_request_id
808+
**kwargs: Keyword arguments to pass to with_request_id
809+
810+
Returns:
811+
list: gRPC metadata with request ID header
812+
"""
813+
return with_request_id_metadata_only(*args, **kwargs)
814+
815+
816+
def _metadata_with_request_id_and_req_id(*args, **kwargs):
817+
"""Return both metadata and request ID string.
818+
819+
This is used when we need to augment errors with the request ID.
820+
821+
Args:
822+
*args: Arguments to pass to with_request_id
823+
**kwargs: Keyword arguments to pass to with_request_id
824+
825+
Returns:
826+
tuple: (metadata, request_id)
827+
"""
770828
return with_request_id(*args, **kwargs)
771829

772830

831+
def _augment_error_with_request_id(error, request_id=None):
832+
"""Augment an error with request ID information.
833+
834+
Args:
835+
error: The error to augment (typically GoogleAPICallError)
836+
request_id (str): The request ID to include
837+
838+
Returns:
839+
The augmented error with request ID information
840+
"""
841+
return wrap_with_request_id(error, request_id)
842+
843+
844+
@contextmanager
845+
def _augment_errors_with_request_id(request_id):
846+
"""Context manager to augment exceptions with request ID.
847+
848+
Args:
849+
request_id (str): The request ID to include in exceptions
850+
851+
Yields:
852+
None
853+
"""
854+
try:
855+
yield
856+
except Exception as exc:
857+
augmented = _augment_error_with_request_id(exc, request_id)
858+
# Use exception chaining to preserve the original exception
859+
raise augmented from exc
860+
861+
773862
def _merge_Transaction_Options(
774863
defaultTransactionOptions: TransactionOptions,
775864
mergeTransactionOptions: TransactionOptions,

google/cloud/spanner_v1/batch.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -252,20 +252,22 @@ def wrapped_method():
252252
max_commit_delay=max_commit_delay,
253253
request_options=request_options,
254254
)
255+
# This code is retried due to ABORTED, hence nth_request
256+
# should be increased. attempt can only be increased if
257+
# we encounter UNAVAILABLE or INTERNAL.
258+
call_metadata, error_augmenter = database.with_error_augmentation(
259+
getattr(database, "_next_nth_request", 0),
260+
1,
261+
metadata,
262+
span,
263+
)
255264
commit_method = functools.partial(
256265
api.commit,
257266
request=commit_request,
258-
metadata=database.metadata_with_request_id(
259-
# This code is retried due to ABORTED, hence nth_request
260-
# should be increased. attempt can only be increased if
261-
# we encounter UNAVAILABLE or INTERNAL.
262-
getattr(database, "_next_nth_request", 0),
263-
1,
264-
metadata,
265-
span,
266-
),
267+
metadata=call_metadata,
267268
)
268-
return commit_method()
269+
with error_augmenter:
270+
return commit_method()
269271

270272
response = _retry_on_aborted_exception(
271273
wrapped_method,

google/cloud/spanner_v1/database.py

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525

2626
import google.auth.credentials
2727
from google.api_core.retry import Retry
28-
from google.api_core.retry import if_exception_type
2928
from google.cloud.exceptions import NotFound
3029
from google.api_core.exceptions import Aborted
3130
from google.api_core import gapic_v1
@@ -55,6 +54,8 @@
5554
_metadata_with_prefix,
5655
_metadata_with_leader_aware_routing,
5756
_metadata_with_request_id,
57+
_augment_errors_with_request_id,
58+
_metadata_with_request_id_and_req_id,
5859
)
5960
from google.cloud.spanner_v1.batch import Batch
6061
from google.cloud.spanner_v1.batch import MutationGroups
@@ -496,6 +497,66 @@ def metadata_with_request_id(
496497
span,
497498
)
498499

500+
def metadata_and_request_id(
501+
self, nth_request, nth_attempt, prior_metadata=[], span=None
502+
):
503+
"""Return metadata and request ID string.
504+
505+
This method returns both the gRPC metadata with request ID header
506+
and the request ID string itself, which can be used to augment errors.
507+
508+
Args:
509+
nth_request: The request sequence number
510+
nth_attempt: The attempt number (for retries)
511+
prior_metadata: Prior metadata to include
512+
span: Optional span for tracing
513+
514+
Returns:
515+
tuple: (metadata_list, request_id_string)
516+
"""
517+
if span is None:
518+
span = get_current_span()
519+
520+
return _metadata_with_request_id_and_req_id(
521+
self._nth_client_id,
522+
self._channel_id,
523+
nth_request,
524+
nth_attempt,
525+
prior_metadata,
526+
span,
527+
)
528+
529+
def with_error_augmentation(
530+
self, nth_request, nth_attempt, prior_metadata=[], span=None
531+
):
532+
"""Context manager for gRPC calls with error augmentation.
533+
534+
This context manager provides both metadata with request ID and
535+
automatically augments any exceptions with the request ID.
536+
537+
Args:
538+
nth_request: The request sequence number
539+
nth_attempt: The attempt number (for retries)
540+
prior_metadata: Prior metadata to include
541+
span: Optional span for tracing
542+
543+
Yields:
544+
tuple: (metadata_list, context_manager)
545+
"""
546+
if span is None:
547+
span = get_current_span()
548+
549+
metadata, request_id = _metadata_with_request_id_and_req_id(
550+
self._nth_client_id,
551+
self._channel_id,
552+
nth_request,
553+
nth_attempt,
554+
prior_metadata,
555+
span,
556+
)
557+
558+
return metadata, _augment_errors_with_request_id(request_id)
559+
499560
def __eq__(self, other):
500561
if not isinstance(other, self.__class__):
501562
return NotImplemented
@@ -783,16 +844,18 @@ def execute_pdml():
783844

784845
try:
785846
add_span_event(span, "Starting BeginTransaction")
786-
txn = api.begin_transaction(
787-
session=session.name,
788-
options=txn_options,
789-
metadata=self.metadata_with_request_id(
790-
self._next_nth_request,
791-
1,
792-
metadata,
793-
span,
794-
),
847+
call_metadata, error_augmenter = self.with_error_augmentation(
848+
self._next_nth_request,
849+
1,
850+
metadata,
851+
span,
795852
)
853+
with error_augmenter:
854+
txn = api.begin_transaction(
855+
session=session.name,
856+
options=txn_options,
857+
metadata=call_metadata,
858+
)
796859

797860
txn_selector = TransactionSelector(id=txn.id)
798861

@@ -2052,13 +2115,24 @@ def _retry_on_aborted(func, retry_config):
20522115
"""Helper for :meth:`Database.execute_partitioned_dml`.
20532116
20542117
Wrap function in a Retry that will retry on Aborted exceptions
2055-
with the retry config specified.
2118+
with the retry config specified. Also handles SpannerError that
2119+
wraps Aborted exceptions.
20562120
20572121
:type func: callable
20582122
:param func: the function to be retried on Aborted exceptions
20592123
20602124
:type retry_config: Retry
20612125
:param retry_config: retry object with the settings to be used
20622126
"""
2063-
retry = retry_config.with_predicate(if_exception_type(Aborted))
2127+
from google.cloud.spanner_v1.exceptions import SpannerError
2128+
2129+
def _is_aborted_or_wrapped_aborted(exc):
2130+
"""Check if exception is Aborted or SpannerError wrapping Aborted."""
2131+
if isinstance(exc, Aborted):
2132+
return True
2133+
if isinstance(exc, SpannerError) and isinstance(exc._error, Aborted):
2134+
return True
2135+
return False
2136+
2137+
retry = retry_config.with_predicate(_is_aborted_or_wrapped_aborted)
20642138
return retry(func)

0 commit comments

Comments
 (0)