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

Commit 819e1ae

Browse files
committed
Moved retry trackers into own file
2 parents 3e0d134 + 54b7208 commit 819e1ae

5 files changed

Lines changed: 362 additions & 74 deletions

File tree

google/cloud/bigtable/data/_metrics/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,21 @@
1515
BigtableClientSideMetricsController,
1616
)
1717

18-
from google.cloud.bigtable.data._metrics.data_model import OperationType
1918
from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric
2019
from google.cloud.bigtable.data._metrics.data_model import ActiveAttemptMetric
2120
from google.cloud.bigtable.data._metrics.data_model import CompletedOperationMetric
2221
from google.cloud.bigtable.data._metrics.data_model import CompletedAttemptMetric
22+
from google.cloud.bigtable.data._metrics.data_model import OperationState
23+
from google.cloud.bigtable.data._metrics.data_model import OperationType
24+
from google.cloud.bigtable.data._metrics.tracked_retry import tracked_retry
2325

2426
__all__ = (
2527
"BigtableClientSideMetricsController",
2628
"OperationType",
29+
"OperationState",
2730
"ActiveOperationMetric",
2831
"ActiveAttemptMetric",
2932
"CompletedOperationMetric",
3033
"CompletedAttemptMetric",
34+
"tracked_retry",
3135
)

google/cloud/bigtable/data/_metrics/data_model.py

Lines changed: 1 addition & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414
from __future__ import annotations
1515

16-
from typing import Callable, ClassVar, List, Tuple, Optional, cast, TYPE_CHECKING
16+
from typing import ClassVar, Tuple, cast, TYPE_CHECKING
1717

1818
import time
1919
import re
@@ -29,12 +29,9 @@
2929
from grpc import RpcError
3030
from grpc.aio import AioRpcError
3131

32-
from google.api_core.exceptions import GoogleAPICallError
33-
from google.api_core.retry import RetryFailureReason
3432
import google.cloud.bigtable.data.exceptions as bt_exceptions
3533
from google.cloud.bigtable_v2.types.response_params import ResponseParams
3634
from google.cloud.bigtable.data._helpers import TrackedBackoffGenerator
37-
from google.cloud.bigtable.data.exceptions import _MutateRowsIncomplete
3835
from google.protobuf.message import DecodeError
3936

4037
if TYPE_CHECKING:
@@ -54,11 +51,6 @@
5451

5552
INVALID_STATE_ERROR = "Invalid state for {}: {}"
5653

57-
ExceptionFactoryType = Callable[
58-
[List[Exception], RetryFailureReason, Optional[float]],
59-
Tuple[Exception, Optional[Exception]],
60-
]
61-
6254

6355
class OperationType(Enum):
6456
"""Enum for the type of operation being performed."""
@@ -422,69 +414,6 @@ def _exc_to_status(exc: BaseException) -> StatusCode:
422414
return exc.code()
423415
return StatusCode.UNKNOWN
424416

425-
def track_retryable_error(self, exc: Exception) -> None:
426-
"""
427-
Used as input to api_core.Retry classes, to track when retryable errors are encountered
428-
429-
Should be passed as on_error callback
430-
"""
431-
try:
432-
# record metadata from failed rpc
433-
if isinstance(exc, GoogleAPICallError) and exc.errors:
434-
rpc_error = exc.errors[-1]
435-
metadata = list(rpc_error.trailing_metadata()) + list(
436-
rpc_error.initial_metadata()
437-
)
438-
self.add_response_metadata({k: v for k, v in metadata})
439-
except Exception:
440-
# ignore errors in metadata collection
441-
pass
442-
if isinstance(exc, _MutateRowsIncomplete):
443-
# _MutateRowsIncomplete represents a successful rpc with some failed mutations
444-
# mark the attempt as successful
445-
self.end_attempt_with_status(StatusCode.OK)
446-
else:
447-
self.end_attempt_with_status(exc)
448-
449-
def track_terminal_error(
450-
self, exception_factory: ExceptionFactoryType
451-
) -> ExceptionFactoryType:
452-
"""
453-
Used as input to api_core.Retry classes, to track when terminal errors are encountered
454-
455-
Should be used as a wrapper over an exception_factory callback
456-
"""
457-
458-
def wrapper(
459-
exc_list: list[Exception],
460-
reason: RetryFailureReason,
461-
timeout_val: float | None,
462-
) -> tuple[Exception, Exception | None]:
463-
source_exc, cause_exc = exception_factory(exc_list, reason, timeout_val)
464-
try:
465-
# record metadata from failed rpc
466-
if isinstance(source_exc, GoogleAPICallError) and source_exc.errors:
467-
rpc_error = source_exc.errors[-1]
468-
metadata = list(rpc_error.trailing_metadata()) + list(
469-
rpc_error.initial_metadata()
470-
)
471-
self.add_response_metadata({k: v for k, v in metadata})
472-
except Exception:
473-
# ignore errors in metadata collection
474-
pass
475-
if (
476-
reason == RetryFailureReason.TIMEOUT
477-
and self.state == OperationState.ACTIVE_ATTEMPT
478-
and exc_list
479-
):
480-
# record ending attempt for timeout failures
481-
attempt_exc = exc_list[-1]
482-
self.track_retryable_error(attempt_exc)
483-
self.end_with_status(source_exc)
484-
return source_exc, cause_exc
485-
486-
return wrapper
487-
488417
@staticmethod
489418
def _handle_error(message: str) -> None:
490419
"""
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Copyright 2025 Google LLC
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+
from typing import Callable, List, Optional, Tuple, TypeVar
15+
16+
from grpc import StatusCode
17+
from google.api_core.exceptions import GoogleAPICallError
18+
from google.api_core.retry import RetryFailureReason
19+
from google.cloud.bigtable.data.exceptions import _MutateRowsIncomplete
20+
from google.cloud.bigtable.data._helpers import _retry_exception_factory
21+
from google.cloud.bigtable.data._metrics import ActiveOperationMetric
22+
from google.cloud.bigtable.data._metrics import OperationState
23+
24+
25+
T = TypeVar("T")
26+
27+
28+
ExceptionFactoryType = Callable[
29+
[List[Exception], RetryFailureReason, Optional[float]],
30+
Tuple[Exception, Optional[Exception]],
31+
]
32+
33+
34+
def _track_retryable_error(
35+
operation: ActiveOperationMetric,
36+
) -> Callable[[Exception], None]:
37+
"""
38+
Used as input to api_core.Retry classes, to track when retryable errors are encountered
39+
40+
Should be passed as on_error callback
41+
"""
42+
43+
def wrapper(exc: Exception) -> None:
44+
try:
45+
# record metadata from failed rpc
46+
if isinstance(exc, GoogleAPICallError) and exc.errors:
47+
rpc_error = exc.errors[-1]
48+
metadata = list(rpc_error.trailing_metadata()) + list(
49+
rpc_error.initial_metadata()
50+
)
51+
operation.add_response_metadata({k: v for k, v in metadata})
52+
except Exception:
53+
# ignore errors in metadata collection
54+
pass
55+
if isinstance(exc, _MutateRowsIncomplete):
56+
# _MutateRowsIncomplete represents a successful rpc with some failed mutations
57+
# mark the attempt as successful
58+
operation.end_attempt_with_status(StatusCode.OK)
59+
else:
60+
operation.end_attempt_with_status(exc)
61+
62+
return wrapper
63+
64+
65+
def _track_terminal_error(
66+
operation: ActiveOperationMetric, exception_factory: ExceptionFactoryType
67+
) -> ExceptionFactoryType:
68+
"""
69+
Used as input to api_core.Retry classes, to track when terminal errors are encountered
70+
71+
Should be used as a wrapper over an exception_factory callback
72+
"""
73+
74+
def wrapper(
75+
exc_list: list[Exception],
76+
reason: RetryFailureReason,
77+
timeout_val: float | None,
78+
) -> tuple[Exception, Exception | None]:
79+
source_exc, cause_exc = exception_factory(exc_list, reason, timeout_val)
80+
try:
81+
# record metadata from failed rpc
82+
if isinstance(source_exc, GoogleAPICallError) and source_exc.errors:
83+
rpc_error = source_exc.errors[-1]
84+
metadata = list(rpc_error.trailing_metadata()) + list(
85+
rpc_error.initial_metadata()
86+
)
87+
operation.add_response_metadata({k: v for k, v in metadata})
88+
except Exception:
89+
# ignore errors in metadata collection
90+
pass
91+
if (
92+
reason == RetryFailureReason.TIMEOUT
93+
and operation.state == OperationState.ACTIVE_ATTEMPT
94+
and exc_list
95+
):
96+
# record ending attempt for timeout failures
97+
attempt_exc = exc_list[-1]
98+
_track_retryable_error(operation)(attempt_exc)
99+
operation.end_with_status(source_exc)
100+
return source_exc, cause_exc
101+
102+
return wrapper
103+
104+
105+
def tracked_retry(
106+
*,
107+
retry_fn: Callable[..., T],
108+
operation: ActiveOperationMetric,
109+
**kwargs,
110+
) -> T:
111+
"""
112+
Wrapper for retry_rarget or retry_target_stream, which injects methods to
113+
track the lifecycle of the retry using the provided ActiveOperationMetric
114+
"""
115+
in_exception_factory = kwargs.pop("exception_factory", _retry_exception_factory)
116+
kwargs.pop("on_error", None)
117+
kwargs.pop("sleep_generator", None)
118+
return retry_fn(
119+
sleep_generator=operation.backoff_generator,
120+
on_error=_track_retryable_error(operation),
121+
exception_factory=_track_terminal_error(operation, in_exception_factory),
122+
**kwargs,
123+
)

tests/unit/data/_metrics/test_metrics_controller.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2023 Google LLC
1+
# Copyright 2025 Google LLC
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.

0 commit comments

Comments
 (0)