|
28 | 28 | from grpc import RpcError |
29 | 29 | from grpc.aio import AioRpcError |
30 | 30 |
|
| 31 | +from google.api_core.exceptions import GoogleAPICallError |
31 | 32 | import google.cloud.bigtable.data.exceptions as bt_exceptions |
32 | 33 | from google.cloud.bigtable_v2.types.response_params import ResponseParams |
33 | 34 | from google.cloud.bigtable.data._helpers import TrackedBackoffGenerator |
| 35 | +from google.cloud.bigtable.data.exceptions import _MutateRowsIncomplete |
| 36 | +from google.cloud.bigtable.data.exceptions import RetryExceptionGroup |
34 | 37 | from google.protobuf.message import DecodeError |
35 | 38 |
|
36 | 39 | if TYPE_CHECKING: |
37 | 40 | from google.cloud.bigtable.data._metrics.handlers._base import MetricsHandler |
| 41 | + from google.api_core.retry import RetryFailureReason |
38 | 42 |
|
39 | 43 |
|
40 | 44 | LOGGER = logging.getLogger(__name__) |
@@ -395,6 +399,66 @@ def _exc_to_status(exc: BaseException) -> StatusCode: |
395 | 399 | return exc.code() |
396 | 400 | return StatusCode.UNKNOWN |
397 | 401 |
|
| 402 | + def track_retryable_error(self) -> callable[[Exception], None]: |
| 403 | + """ |
| 404 | + Used as input to api_core.Retry classes, to track when retryable errors are encountered |
| 405 | +
|
| 406 | + Should be passed as on_error callback |
| 407 | + """ |
| 408 | + |
| 409 | + def wrapper(exc: Exception) -> None: |
| 410 | + try: |
| 411 | + # record metadata from failed rpc |
| 412 | + if ( |
| 413 | + isinstance(exc, GoogleAPICallError) |
| 414 | + and exc.errors |
| 415 | + ): |
| 416 | + rpc_error = exc.errors[-1] |
| 417 | + metadata = list(rpc_error.trailing_metadata()) + list( |
| 418 | + rpc_error.initial_metadata() |
| 419 | + ) |
| 420 | + self.add_response_metadata({k: v for k, v in metadata}) |
| 421 | + except Exception: |
| 422 | + # ignore errors in metadata collection |
| 423 | + pass |
| 424 | + if isinstance(exc, _MutateRowsIncomplete): |
| 425 | + # _MutateRowsIncomplete represents a successful rpc with some failed mutations |
| 426 | + # mark the attempt as successful |
| 427 | + self.end_attempt_with_status(StatusCode.OK) |
| 428 | + else: |
| 429 | + self.end_attempt_with_status(exc) |
| 430 | + return wrapper |
| 431 | + |
| 432 | + def track_terminal_error(self, exception_factory:callable[ |
| 433 | + [list[Exception], RetryFailureReason, float | None],tuple[Exception, Exception | None], |
| 434 | + ]) -> callable[[list[Exception], RetryFailureReason, float | None], None]: |
| 435 | + """ |
| 436 | + Used as input to api_core.Retry classes, to track when terminal errors are encountered |
| 437 | +
|
| 438 | + Should be used as a wrapper over an exception_factory callback |
| 439 | + """ |
| 440 | + def wrapper( |
| 441 | + exc_list: list[Exception], reason: RetryFailureReason, timeout_val: float | None |
| 442 | + ) -> tuple[Exception, Exception | None]: |
| 443 | + source_exc, cause_exc = exception_factory(exc_list, reason, timeout_val) |
| 444 | + try: |
| 445 | + # record metadata from failed rpc |
| 446 | + if ( |
| 447 | + isinstance(source_exc, GoogleAPICallError) |
| 448 | + and source_exc.errors |
| 449 | + ): |
| 450 | + rpc_error = source_exc.errors[-1] |
| 451 | + metadata = list(rpc_error.trailing_metadata()) + list( |
| 452 | + rpc_error.initial_metadata() |
| 453 | + ) |
| 454 | + self.add_response_metadata({k: v for k, v in metadata}) |
| 455 | + except Exception: |
| 456 | + # ignore errors in metadata collection |
| 457 | + pass |
| 458 | + self.end_with_status(source_exc) |
| 459 | + return source_exc, cause_exc |
| 460 | + return wrapper |
| 461 | + |
398 | 462 | @staticmethod |
399 | 463 | def _handle_error(message: str) -> None: |
400 | 464 | """ |
|
0 commit comments