Skip to content

Commit 4f6340b

Browse files
authored
feat: add retry and timeout for batch dml (#1107)
* feat(spanner): add retry, timeout for batch update * feat(spanner): add samples for retry, timeout * feat(spanner): update unittest * feat(spanner): update comments * feat(spanner): update code for retry * feat(spanner): update comment
1 parent 9c919fa commit 4f6340b

File tree

5 files changed

+110
-3
lines changed

5 files changed

+110
-3
lines changed

google/cloud/spanner_v1/transaction.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,14 @@ def execute_update(
410410

411411
return response.stats.row_count_exact
412412

413-
def batch_update(self, statements, request_options=None):
413+
def batch_update(
414+
self,
415+
statements,
416+
request_options=None,
417+
*,
418+
retry=gapic_v1.method.DEFAULT,
419+
timeout=gapic_v1.method.DEFAULT,
420+
):
414421
"""Perform a batch of DML statements via an ``ExecuteBatchDml`` request.
415422
416423
:type statements:
@@ -431,6 +438,12 @@ def batch_update(self, statements, request_options=None):
431438
If a dict is provided, it must be of the same form as the protobuf
432439
message :class:`~google.cloud.spanner_v1.types.RequestOptions`.
433440
441+
:type retry: :class:`~google.api_core.retry.Retry`
442+
:param retry: (Optional) The retry settings for this request.
443+
444+
:type timeout: float
445+
:param timeout: (Optional) The timeout for this request.
446+
434447
:rtype:
435448
Tuple(status, Sequence[int])
436449
:returns:
@@ -486,6 +499,8 @@ def batch_update(self, statements, request_options=None):
486499
api.execute_batch_dml,
487500
request=request,
488501
metadata=metadata,
502+
retry=retry,
503+
timeout=timeout,
489504
)
490505

491506
if self._transaction_id is None:

samples/samples/snippets.py

+50
Original file line numberDiff line numberDiff line change
@@ -3017,6 +3017,51 @@ def directed_read_options(
30173017
# [END spanner_directed_read]
30183018

30193019

3020+
def set_custom_timeout_and_retry(instance_id, database_id):
3021+
"""Executes a snapshot read with custom timeout and retry."""
3022+
# [START spanner_set_custom_timeout_and_retry]
3023+
from google.api_core import retry
3024+
from google.api_core import exceptions as core_exceptions
3025+
3026+
# instance_id = "your-spanner-instance"
3027+
# database_id = "your-spanner-db-id"
3028+
spanner_client = spanner.Client()
3029+
instance = spanner_client.instance(instance_id)
3030+
database = instance.database(database_id)
3031+
3032+
retry = retry.Retry(
3033+
# Customize retry with an initial wait time of 500 milliseconds.
3034+
initial=0.5,
3035+
# Customize retry with a maximum wait time of 16 seconds.
3036+
maximum=16,
3037+
# Customize retry with a wait time multiplier per iteration of 1.5.
3038+
multiplier=1.5,
3039+
# Customize retry with a timeout on
3040+
# how long a certain RPC may be retried in
3041+
# case the server returns an error.
3042+
timeout=60,
3043+
# Configure which errors should be retried.
3044+
predicate=retry.if_exception_type(
3045+
core_exceptions.ServiceUnavailable,
3046+
),
3047+
)
3048+
3049+
# Set a custom retry and timeout setting.
3050+
with database.snapshot() as snapshot:
3051+
results = snapshot.execute_sql(
3052+
"SELECT SingerId, AlbumId, AlbumTitle FROM Albums",
3053+
# Set custom retry setting for this request
3054+
retry=retry,
3055+
# Set custom timeout of 60 seconds for this request
3056+
timeout=60,
3057+
)
3058+
3059+
for row in results:
3060+
print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row))
3061+
3062+
# [END spanner_set_custom_timeout_and_retry]
3063+
3064+
30203065
if __name__ == "__main__": # noqa: C901
30213066
parser = argparse.ArgumentParser(
30223067
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
@@ -3157,6 +3202,9 @@ def directed_read_options(
31573202
)
31583203
enable_fine_grained_access_parser.add_argument("--title", default="condition title")
31593204
subparsers.add_parser("directed_read_options", help=directed_read_options.__doc__)
3205+
subparsers.add_parser(
3206+
"set_custom_timeout_and_retry", help=set_custom_timeout_and_retry.__doc__
3207+
)
31603208

31613209
args = parser.parse_args()
31623210

@@ -3290,3 +3338,5 @@ def directed_read_options(
32903338
)
32913339
elif args.command == "directed_read_options":
32923340
directed_read_options(args.instance_id, args.database_id)
3341+
elif args.command == "set_custom_timeout_and_retry":
3342+
set_custom_timeout_and_retry(args.instance_id, args.database_id)

samples/samples/snippets_test.py

+7
Original file line numberDiff line numberDiff line change
@@ -859,3 +859,10 @@ def test_directed_read_options(capsys, instance_id, sample_database):
859859
snippets.directed_read_options(instance_id, sample_database.database_id)
860860
out, _ = capsys.readouterr()
861861
assert "SingerId: 1, AlbumId: 1, AlbumTitle: Total Junk" in out
862+
863+
864+
@pytest.mark.dependency(depends=["insert_data"])
865+
def test_set_custom_timeout_and_retry(capsys, instance_id, sample_database):
866+
snippets.set_custom_timeout_and_retry(instance_id, sample_database.database_id)
867+
out, _ = capsys.readouterr()
868+
assert "SingerId: 1, AlbumId: 1, AlbumTitle: Total Junk" in out

tests/unit/test_spanner.py

+14
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,8 @@ def test_transaction_should_include_begin_with_first_batch_update(self):
556556
("google-cloud-resource-prefix", database.name),
557557
("x-goog-spanner-route-to-leader", "true"),
558558
],
559+
retry=RETRY,
560+
timeout=TIMEOUT,
559561
)
560562

561563
def test_transaction_should_use_transaction_id_if_error_with_first_batch_update(
@@ -574,6 +576,8 @@ def test_transaction_should_use_transaction_id_if_error_with_first_batch_update(
574576
("google-cloud-resource-prefix", database.name),
575577
("x-goog-spanner-route-to-leader", "true"),
576578
],
579+
retry=RETRY,
580+
timeout=TIMEOUT,
577581
)
578582
self._execute_update_helper(transaction=transaction, api=api)
579583
api.execute_sql.assert_called_once_with(
@@ -715,6 +719,8 @@ def test_transaction_should_use_transaction_id_returned_by_first_read(self):
715719
("google-cloud-resource-prefix", database.name),
716720
("x-goog-spanner-route-to-leader", "true"),
717721
],
722+
retry=RETRY,
723+
timeout=TIMEOUT,
718724
)
719725

720726
def test_transaction_should_use_transaction_id_returned_by_first_batch_update(self):
@@ -729,6 +735,8 @@ def test_transaction_should_use_transaction_id_returned_by_first_batch_update(se
729735
("google-cloud-resource-prefix", database.name),
730736
("x-goog-spanner-route-to-leader", "true"),
731737
],
738+
retry=RETRY,
739+
timeout=TIMEOUT,
732740
)
733741
self._read_helper(transaction=transaction, api=api)
734742
api.streaming_read.assert_called_once_with(
@@ -797,6 +805,8 @@ def test_transaction_for_concurrent_statement_should_begin_one_transaction_with_
797805
("google-cloud-resource-prefix", database.name),
798806
("x-goog-spanner-route-to-leader", "true"),
799807
],
808+
retry=RETRY,
809+
timeout=TIMEOUT,
800810
)
801811

802812
self.assertEqual(api.execute_sql.call_count, 2)
@@ -846,6 +856,8 @@ def test_transaction_for_concurrent_statement_should_begin_one_transaction_with_
846856
("google-cloud-resource-prefix", database.name),
847857
("x-goog-spanner-route-to-leader", "true"),
848858
],
859+
retry=RETRY,
860+
timeout=TIMEOUT,
849861
)
850862

851863
api.execute_batch_dml.assert_any_call(
@@ -854,6 +866,8 @@ def test_transaction_for_concurrent_statement_should_begin_one_transaction_with_
854866
("google-cloud-resource-prefix", database.name),
855867
("x-goog-spanner-route-to-leader", "true"),
856868
],
869+
retry=RETRY,
870+
timeout=TIMEOUT,
857871
)
858872

859873
self.assertEqual(api.execute_sql.call_count, 1)

tests/unit/test_transaction.py

+23-2
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,14 @@ def test_batch_update_other_error(self):
662662
with self.assertRaises(RuntimeError):
663663
transaction.batch_update(statements=[DML_QUERY])
664664

665-
def _batch_update_helper(self, error_after=None, count=0, request_options=None):
665+
def _batch_update_helper(
666+
self,
667+
error_after=None,
668+
count=0,
669+
request_options=None,
670+
retry=gapic_v1.method.DEFAULT,
671+
timeout=gapic_v1.method.DEFAULT,
672+
):
666673
from google.rpc.status_pb2 import Status
667674
from google.protobuf.struct_pb2 import Struct
668675
from google.cloud.spanner_v1 import param_types
@@ -716,7 +723,10 @@ def _batch_update_helper(self, error_after=None, count=0, request_options=None):
716723
request_options = RequestOptions(request_options)
717724

718725
status, row_counts = transaction.batch_update(
719-
dml_statements, request_options=request_options
726+
dml_statements,
727+
request_options=request_options,
728+
retry=retry,
729+
timeout=timeout,
720730
)
721731

722732
self.assertEqual(status, expected_status)
@@ -753,6 +763,8 @@ def _batch_update_helper(self, error_after=None, count=0, request_options=None):
753763
("google-cloud-resource-prefix", database.name),
754764
("x-goog-spanner-route-to-leader", "true"),
755765
],
766+
retry=retry,
767+
timeout=timeout,
756768
)
757769

758770
self.assertEqual(transaction._execute_sql_count, count + 1)
@@ -826,6 +838,15 @@ def test_batch_update_error(self):
826838

827839
self.assertEqual(transaction._execute_sql_count, 1)
828840

841+
def test_batch_update_w_timeout_param(self):
842+
self._batch_update_helper(timeout=2.0)
843+
844+
def test_batch_update_w_retry_param(self):
845+
self._batch_update_helper(retry=gapic_v1.method.DEFAULT)
846+
847+
def test_batch_update_w_timeout_and_retry_params(self):
848+
self._batch_update_helper(retry=gapic_v1.method.DEFAULT, timeout=2.0)
849+
829850
def test_context_mgr_success(self):
830851
import datetime
831852
from google.cloud.spanner_v1 import CommitResponse

0 commit comments

Comments
 (0)