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

Commit f2528ae

Browse files
committed
combined wrapped predicate with wrapped exc factory
1 parent fcd3aaa commit f2528ae

3 files changed

Lines changed: 69 additions & 29 deletions

File tree

google/cloud/bigtable/data/_async/_read_rows.py

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -115,31 +115,17 @@ def start_operation(self) -> AsyncGenerator[Row, None]:
115115
self._operation_metrics.backoff_generator = sleep_generator
116116

117117
# Metrics:
118-
# track attempt failures using build_wrapped_predicate() for raised exceptions
119-
# and _metric_wrapped_exception_factory for operation timeouts
118+
# track attempt failures using build_wrapped_fn_handlers() for raised exceptions
119+
# and operation timeouts
120+
metric_predicate, metric_excs = self._operation_metrics.build_wrapped_fn_handlers(self._predicate)
120121
return retries.retry_target_stream_async(
121122
self._read_rows_attempt,
122-
self._operation_metrics.build_wrapped_predicate(self._predicate),
123+
metric_predicate,
123124
sleep_generator,
124125
self.operation_timeout,
125-
exception_factory=self._metric_wrapped_exception_factory,
126+
exception_factory=metric_excs,
126127
)
127128

128-
def _metric_wrapped_exception_factory(
129-
self,
130-
exc_list: list[Exception],
131-
reason: retries.RetryFailureReason,
132-
timeout_val: float | None,
133-
) -> tuple[Exception, Exception | None]:
134-
"""
135-
Wrap the retry exception builder to alert the metrics class
136-
when we are going to emit an operation timeout.
137-
"""
138-
exc, source = _retry_exception_factory(exc_list, reason, timeout_val)
139-
if reason != retries.RetryFailureReason.NON_RETRYABLE_ERROR:
140-
self._operation_metrics.end_with_status(exc)
141-
return exc, source
142-
143129
def _read_rows_attempt(self) -> AsyncGenerator[Row, None]:
144130
"""
145131
Attempt a single read_rows rpc call.

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

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@
3030
from google.cloud.bigtable.data.exceptions import MutationsExceptionGroup
3131
from google.cloud.bigtable.data.exceptions import ShardedReadRowsExceptionGroup
3232
from google.cloud.bigtable.data.exceptions import RetryExceptionGroup
33+
from google.cloud.bigtable.data._helpers import _retry_exception_factory
3334
from google.cloud.bigtable_v2.types.response_params import ResponseParams
3435
from google.protobuf.message import DecodeError
36+
from google.api_core.retry import RetryFailureReason
3537

3638
if TYPE_CHECKING:
3739
from google.cloud.bigtable.data._metrics.handlers._base import MetricsHandler
@@ -377,13 +379,17 @@ def end_with_success(self):
377379
"""
378380
return self.end_with_status(StatusCode.OK)
379381

380-
def build_wrapped_predicate(
381-
self, inner_predicate: Callable[[Exception], bool]
382+
def build_wrapped_fn_handlers(
383+
self,
384+
inner_predicate: Callable[[Exception], bool],
382385
) -> Callable[[Exception], bool]:
383386
"""
384-
Wrapps a predicate to include metrics tracking. Any call to the resulting predicate
385-
is assumed to be an rpc failure, and will either mark the end of the active attempt
386-
or the end of the operation.
387+
One way to track metrics is by wrapping the `predicate` and `exception_factory`
388+
arguments of `api_core.Retry`. This will notify us when an exception occurs so
389+
we can track it.
390+
391+
This function retruns wrapped versions of the `predicate` and `exception_factory`
392+
to be passed down when building the `Retry` object.
387393
388394
Args:
389395
- predicate: The predicate to wrap.
@@ -397,7 +403,17 @@ def wrapped_predicate(exc: Exception) -> bool:
397403
self.end_with_status(exc)
398404
return inner_result
399405

400-
return wrapped_predicate
406+
def wrapped_exception_factory(
407+
exc_list: list[Exception],
408+
reason: RetryFailureReason,
409+
timeout_val: float | None,
410+
) -> tuple[Exception, Exception | None]:
411+
exc, source = _retry_exception_factory(exc_list, reason, timeout_val)
412+
if reason != RetryFailureReason.NON_RETRYABLE_ERROR:
413+
self.end_with_status(exc)
414+
return exc, source
415+
416+
return wrapped_predicate, wrapped_exception_factory
401417

402418
@staticmethod
403419
def _exc_to_status(exc: Exception) -> StatusCode:

tests/unit/data/_metrics/test_data_model.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ def test_end_on_empty_operation(self):
601601
assert final_op.final_status == StatusCode.OK
602602
assert final_op.completed_attempts == []
603603

604-
def test_build_wrapped_predicate(self):
604+
def test_build_wrapped_fn_handlers_predicate(self):
605605
"""
606606
predicate generated by object should terminate attempt or operation
607607
based on passed in predicate
@@ -610,23 +610,61 @@ def test_build_wrapped_predicate(self):
610610
cls = type(self._make_one(object()))
611611
# ensure predicate is called with the exception
612612
mock_predicate = mock.Mock()
613-
cls.build_wrapped_predicate(mock.Mock(), mock_predicate)(input_exc)
613+
pred, _ = cls.build_wrapped_fn_handlers(mock.Mock(), mock_predicate)
614+
pred(input_exc)
614615
assert mock_predicate.call_count == 1
615616
assert mock_predicate.call_args[0][0] == input_exc
616617
assert len(mock_predicate.call_args[0]) == 1
617618
# if predicate is true, end the attempt
618619
mock_instance = mock.Mock()
619-
cls.build_wrapped_predicate(mock_instance, lambda x: True)(input_exc)
620+
pred, _ = cls.build_wrapped_fn_handlers(mock_instance, lambda x: True)
621+
pred(input_exc)
620622
assert mock_instance.end_attempt_with_status.call_count == 1
621623
assert mock_instance.end_attempt_with_status.call_args[0][0] == input_exc
622624
assert len(mock_instance.end_attempt_with_status.call_args[0]) == 1
623625
# if predicate is false, end the operation
624626
mock_instance = mock.Mock()
625-
cls.build_wrapped_predicate(mock_instance, lambda x: False)(input_exc)
627+
pred, _ = cls.build_wrapped_fn_handlers(mock_instance, lambda x: False)
628+
pred(input_exc)
626629
assert mock_instance.end_with_status.call_count == 1
627630
assert mock_instance.end_with_status.call_args[0][0] == input_exc
628631
assert len(mock_instance.end_with_status.call_args[0]) == 1
629632

633+
def test_build_wrapped_fn_handlers_exc_factory(self):
634+
"""
635+
exception factory generated by object should terminate operation
636+
on timeout
637+
"""
638+
from google.api_core.retry import RetryFailureReason
639+
from google.api_core.exceptions import DeadlineExceeded
640+
641+
cls = type(self._make_one(object()))
642+
# ensure inner factory is called with the exception
643+
_, factory = cls.build_wrapped_fn_handlers(mock.Mock(), None)
644+
with mock.patch(
645+
"google.cloud.bigtable.data._metrics.data_model._retry_exception_factory"
646+
) as mock_factory:
647+
expected_return = (object(), object())
648+
mock_factory.return_value = expected_return
649+
args = ("a", "b", "c")
650+
got_result = factory(*args)
651+
assert expected_return == got_result
652+
assert mock_factory.call_count == 1
653+
assert mock_factory.call_args[0] == args
654+
655+
# if called with reason == TIMEOUT, end the operation
656+
mock_instance = mock.Mock()
657+
_, factory = cls.build_wrapped_fn_handlers(mock_instance, None)
658+
factory([], RetryFailureReason.TIMEOUT, None)
659+
assert mock_instance.end_with_status.call_count == 1
660+
assert type(mock_instance.end_with_status.call_args[0][0]) == DeadlineExceeded
661+
662+
# if called with reason==NON_RETRYABLE_ERROR, do not
663+
mock_instance = mock.Mock()
664+
_, factory = cls.build_wrapped_fn_handlers(mock_instance, None)
665+
factory([], RetryFailureReason.NON_RETRYABLE_ERROR, None)
666+
assert mock_instance.end_with_status.call_count == 0
667+
630668
def test__exc_to_status(self):
631669
"""
632670
Should return grpc_status_code if grpc error, otherwise UNKNOWN

0 commit comments

Comments
 (0)