|
27 | 27 | from google.protobuf.internal.enum_type_wrapper import EnumTypeWrapper
|
28 | 28 |
|
29 | 29 | from google.api_core import datetime_helpers
|
| 30 | +from google.api_core.exceptions import Aborted |
30 | 31 | from google.cloud._helpers import _date_from_iso8601_date
|
31 | 32 | from google.cloud.spanner_v1 import TypeCode
|
32 | 33 | from google.cloud.spanner_v1 import ExecuteSqlRequest
|
33 | 34 | from google.cloud.spanner_v1 import JsonObject
|
34 | 35 | from google.cloud.spanner_v1.request_id_header import with_request_id
|
| 36 | +from google.rpc.error_details_pb2 import RetryInfo |
| 37 | + |
| 38 | +import random |
35 | 39 |
|
36 | 40 | # Validation error messages
|
37 | 41 | NUMERIC_MAX_SCALE_ERR_MSG = (
|
@@ -460,6 +464,23 @@ def _metadata_with_prefix(prefix, **kw):
|
460 | 464 | return [("google-cloud-resource-prefix", prefix)]
|
461 | 465 |
|
462 | 466 |
|
| 467 | +def _retry_on_aborted_exception( |
| 468 | + func, |
| 469 | + deadline, |
| 470 | +): |
| 471 | + """ |
| 472 | + Handles retry logic for Aborted exceptions, considering the deadline. |
| 473 | + """ |
| 474 | + attempts = 0 |
| 475 | + while True: |
| 476 | + try: |
| 477 | + attempts += 1 |
| 478 | + return func() |
| 479 | + except Aborted as exc: |
| 480 | + _delay_until_retry(exc, deadline=deadline, attempts=attempts) |
| 481 | + continue |
| 482 | + |
| 483 | + |
463 | 484 | def _retry(
|
464 | 485 | func,
|
465 | 486 | retry_count=5,
|
@@ -529,6 +550,60 @@ def _metadata_with_leader_aware_routing(value, **kw):
|
529 | 550 | return ("x-goog-spanner-route-to-leader", str(value).lower())
|
530 | 551 |
|
531 | 552 |
|
| 553 | +def _delay_until_retry(exc, deadline, attempts): |
| 554 | + """Helper for :meth:`Session.run_in_transaction`. |
| 555 | +
|
| 556 | + Detect retryable abort, and impose server-supplied delay. |
| 557 | +
|
| 558 | + :type exc: :class:`google.api_core.exceptions.Aborted` |
| 559 | + :param exc: exception for aborted transaction |
| 560 | +
|
| 561 | + :type deadline: float |
| 562 | + :param deadline: maximum timestamp to continue retrying the transaction. |
| 563 | +
|
| 564 | + :type attempts: int |
| 565 | + :param attempts: number of call retries |
| 566 | + """ |
| 567 | + |
| 568 | + cause = exc.errors[0] |
| 569 | + now = time.time() |
| 570 | + if now >= deadline: |
| 571 | + raise |
| 572 | + |
| 573 | + delay = _get_retry_delay(cause, attempts) |
| 574 | + if delay is not None: |
| 575 | + if now + delay > deadline: |
| 576 | + raise |
| 577 | + |
| 578 | + time.sleep(delay) |
| 579 | + |
| 580 | + |
| 581 | +def _get_retry_delay(cause, attempts): |
| 582 | + """Helper for :func:`_delay_until_retry`. |
| 583 | +
|
| 584 | + :type exc: :class:`grpc.Call` |
| 585 | + :param exc: exception for aborted transaction |
| 586 | +
|
| 587 | + :rtype: float |
| 588 | + :returns: seconds to wait before retrying the transaction. |
| 589 | +
|
| 590 | + :type attempts: int |
| 591 | + :param attempts: number of call retries |
| 592 | + """ |
| 593 | + if hasattr(cause, "trailing_metadata"): |
| 594 | + metadata = dict(cause.trailing_metadata()) |
| 595 | + else: |
| 596 | + metadata = {} |
| 597 | + retry_info_pb = metadata.get("google.rpc.retryinfo-bin") |
| 598 | + if retry_info_pb is not None: |
| 599 | + retry_info = RetryInfo() |
| 600 | + retry_info.ParseFromString(retry_info_pb) |
| 601 | + nanos = retry_info.retry_delay.nanos |
| 602 | + return retry_info.retry_delay.seconds + nanos / 1.0e9 |
| 603 | + |
| 604 | + return 2**attempts + random.random() |
| 605 | + |
| 606 | + |
532 | 607 | class AtomicCounter:
|
533 | 608 | def __init__(self, start_value=0):
|
534 | 609 | self.__lock = threading.Lock()
|
|
0 commit comments