Skip to content

Commit d5acc26

Browse files
feat: Add support for max commit delay (#1050)
* proto generation * max commit delay * Fix some errors * Unit tests * regenerate proto changes * Fix unit tests * Finish test_transaction.py * Finish test_batch.py * Formatting * Cleanup * Fix merge conflict * Add optional=True * Remove optional=True, try calling HasField. * Update HasField to be called on the protobuf. * Update to timedelta.duration instead of an int. * Cleanup * Changes from Sri to pipe value to top-level funcitons and to add integration tests. Thanks Sri * Run nox -s blacken * feat(spanner): remove unused imports and add line * feat(spanner): add empty line in python docs * Update comment with valid values. * Update comment with valid values. * feat(spanner): fix lint * feat(spanner): rever nox file changes --------- Co-authored-by: Sri Harsha CH <[email protected]> Co-authored-by: Sri Harsha CH <[email protected]>
1 parent 122ab36 commit d5acc26

File tree

7 files changed

+159
-19
lines changed

7 files changed

+159
-19
lines changed

google/cloud/spanner_v1/batch.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,9 @@ def _check_state(self):
146146
if self.committed is not None:
147147
raise ValueError("Batch already committed")
148148

149-
def commit(self, return_commit_stats=False, request_options=None):
149+
def commit(
150+
self, return_commit_stats=False, request_options=None, max_commit_delay=None
151+
):
150152
"""Commit mutations to the database.
151153
152154
:type return_commit_stats: bool
@@ -160,6 +162,11 @@ def commit(self, return_commit_stats=False, request_options=None):
160162
If a dict is provided, it must be of the same form as the protobuf
161163
message :class:`~google.cloud.spanner_v1.types.RequestOptions`.
162164
165+
:type max_commit_delay: :class:`datetime.timedelta`
166+
:param max_commit_delay:
167+
(Optional) The amount of latency this request is willing to incur
168+
in order to improve throughput.
169+
163170
:rtype: datetime
164171
:returns: timestamp of the committed changes.
165172
"""
@@ -188,6 +195,7 @@ def commit(self, return_commit_stats=False, request_options=None):
188195
mutations=self._mutations,
189196
single_use_transaction=txn_options,
190197
return_commit_stats=return_commit_stats,
198+
max_commit_delay=max_commit_delay,
191199
request_options=request_options,
192200
)
193201
with trace_call("CloudSpanner.Commit", self._session, trace_attributes):

google/cloud/spanner_v1/database.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,7 @@ def snapshot(self, **kw):
721721
"""
722722
return SnapshotCheckout(self, **kw)
723723

724-
def batch(self, request_options=None):
724+
def batch(self, request_options=None, max_commit_delay=None):
725725
"""Return an object which wraps a batch.
726726
727727
The wrapper *must* be used as a context manager, with the batch
@@ -734,10 +734,16 @@ def batch(self, request_options=None):
734734
If a dict is provided, it must be of the same form as the protobuf
735735
message :class:`~google.cloud.spanner_v1.types.RequestOptions`.
736736
737+
:type max_commit_delay: :class:`datetime.timedelta`
738+
:param max_commit_delay:
739+
(Optional) The amount of latency this request is willing to incur
740+
in order to improve throughput. Value must be between 0ms and
741+
500ms.
742+
737743
:rtype: :class:`~google.cloud.spanner_v1.database.BatchCheckout`
738744
:returns: new wrapper
739745
"""
740-
return BatchCheckout(self, request_options)
746+
return BatchCheckout(self, request_options, max_commit_delay)
741747

742748
def mutation_groups(self):
743749
"""Return an object which wraps a mutation_group.
@@ -796,9 +802,13 @@ def run_in_transaction(self, func, *args, **kw):
796802
797803
:type kw: dict
798804
:param kw: (Optional) keyword arguments to be passed to ``func``.
799-
If passed, "timeout_secs" will be removed and used to
805+
If passed,
806+
"timeout_secs" will be removed and used to
800807
override the default retry timeout which defines maximum timestamp
801808
to continue retrying the transaction.
809+
"max_commit_delay" will be removed and used to set the
810+
max_commit_delay for the request. Value must be between
811+
0ms and 500ms.
802812
803813
:rtype: Any
804814
:returns: The return value of ``func``.
@@ -1035,9 +1045,14 @@ class BatchCheckout(object):
10351045
(Optional) Common options for the commit request.
10361046
If a dict is provided, it must be of the same form as the protobuf
10371047
message :class:`~google.cloud.spanner_v1.types.RequestOptions`.
1048+
1049+
:type max_commit_delay: :class:`datetime.timedelta`
1050+
:param max_commit_delay:
1051+
(Optional) The amount of latency this request is willing to incur
1052+
in order to improve throughput.
10381053
"""
10391054

1040-
def __init__(self, database, request_options=None):
1055+
def __init__(self, database, request_options=None, max_commit_delay=None):
10411056
self._database = database
10421057
self._session = self._batch = None
10431058
if request_options is None:
@@ -1046,6 +1061,7 @@ def __init__(self, database, request_options=None):
10461061
self._request_options = RequestOptions(request_options)
10471062
else:
10481063
self._request_options = request_options
1064+
self._max_commit_delay = max_commit_delay
10491065

10501066
def __enter__(self):
10511067
"""Begin ``with`` block."""
@@ -1062,6 +1078,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
10621078
self._batch.commit(
10631079
return_commit_stats=self._database.log_commit_stats,
10641080
request_options=self._request_options,
1081+
max_commit_delay=self._max_commit_delay,
10651082
)
10661083
finally:
10671084
if self._database.log_commit_stats and self._batch.commit_stats:

google/cloud/spanner_v1/session.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,8 @@ def run_in_transaction(self, func, *args, **kw):
363363
to continue retrying the transaction.
364364
"commit_request_options" will be removed and used to set the
365365
request options for the commit request.
366+
"max_commit_delay" will be removed and used to set the max commit delay for the request.
367+
"transaction_tag" will be removed and used to set the transaction tag for the request.
366368
367369
:rtype: Any
368370
:returns: The return value of ``func``.
@@ -372,6 +374,7 @@ def run_in_transaction(self, func, *args, **kw):
372374
"""
373375
deadline = time.time() + kw.pop("timeout_secs", DEFAULT_RETRY_TIMEOUT_SECS)
374376
commit_request_options = kw.pop("commit_request_options", None)
377+
max_commit_delay = kw.pop("max_commit_delay", None)
375378
transaction_tag = kw.pop("transaction_tag", None)
376379
attempts = 0
377380

@@ -400,6 +403,7 @@ def run_in_transaction(self, func, *args, **kw):
400403
txn.commit(
401404
return_commit_stats=self._database.log_commit_stats,
402405
request_options=commit_request_options,
406+
max_commit_delay=max_commit_delay,
403407
)
404408
except Aborted as exc:
405409
del self._transaction

google/cloud/spanner_v1/transaction.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,9 @@ def rollback(self):
180180
self.rolled_back = True
181181
del self._session._transaction
182182

183-
def commit(self, return_commit_stats=False, request_options=None):
183+
def commit(
184+
self, return_commit_stats=False, request_options=None, max_commit_delay=None
185+
):
184186
"""Commit mutations to the database.
185187
186188
:type return_commit_stats: bool
@@ -194,6 +196,12 @@ def commit(self, return_commit_stats=False, request_options=None):
194196
If a dict is provided, it must be of the same form as the protobuf
195197
message :class:`~google.cloud.spanner_v1.types.RequestOptions`.
196198
199+
:type max_commit_delay: :class:`datetime.timedelta`
200+
:param max_commit_delay:
201+
(Optional) The amount of latency this request is willing to incur
202+
in order to improve throughput.
203+
:class:`~google.cloud.spanner_v1.types.MaxCommitDelay`.
204+
197205
:rtype: datetime
198206
:returns: timestamp of the committed changes.
199207
:raises ValueError: if there are no mutations to commit.
@@ -228,6 +236,7 @@ def commit(self, return_commit_stats=False, request_options=None):
228236
mutations=self._mutations,
229237
transaction_id=self._transaction_id,
230238
return_commit_stats=return_commit_stats,
239+
max_commit_delay=max_commit_delay,
231240
request_options=request_options,
232241
)
233242
with trace_call("CloudSpanner.Commit", self._session, trace_attributes):

tests/system/test_database_api.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import datetime
1516
import time
1617
import uuid
1718

@@ -819,3 +820,42 @@ def _transaction_read(transaction):
819820

820821
with pytest.raises(exceptions.InvalidArgument):
821822
shared_database.run_in_transaction(_transaction_read)
823+
824+
825+
def test_db_batch_insert_w_max_commit_delay(shared_database):
826+
_helpers.retry_has_all_dll(shared_database.reload)()
827+
sd = _sample_data
828+
829+
with shared_database.batch(
830+
max_commit_delay=datetime.timedelta(milliseconds=100)
831+
) as batch:
832+
batch.delete(sd.TABLE, sd.ALL)
833+
batch.insert(sd.TABLE, sd.COLUMNS, sd.ROW_DATA)
834+
835+
with shared_database.snapshot(read_timestamp=batch.committed) as snapshot:
836+
from_snap = list(snapshot.read(sd.TABLE, sd.COLUMNS, sd.ALL))
837+
838+
sd._check_rows_data(from_snap)
839+
840+
841+
def test_db_run_in_transaction_w_max_commit_delay(shared_database):
842+
_helpers.retry_has_all_dll(shared_database.reload)()
843+
sd = _sample_data
844+
845+
with shared_database.batch() as batch:
846+
batch.delete(sd.TABLE, sd.ALL)
847+
848+
def _unit_of_work(transaction, test):
849+
rows = list(transaction.read(test.TABLE, test.COLUMNS, sd.ALL))
850+
assert rows == []
851+
852+
transaction.insert_or_update(test.TABLE, test.COLUMNS, test.ROW_DATA)
853+
854+
shared_database.run_in_transaction(
855+
_unit_of_work, test=sd, max_commit_delay=datetime.timedelta(milliseconds=100)
856+
)
857+
858+
with shared_database.snapshot() as after:
859+
rows = list(after.execute_sql(sd.SQL))
860+
861+
sd._check_rows_data(rows)

tests/unit/test_batch.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,14 @@ def test_commit_ok(self):
233233
self.assertEqual(committed, now)
234234
self.assertEqual(batch.committed, committed)
235235

236-
(session, mutations, single_use_txn, request_options, metadata) = api._committed
236+
(
237+
session,
238+
mutations,
239+
single_use_txn,
240+
request_options,
241+
max_commit_delay,
242+
metadata,
243+
) = api._committed
237244
self.assertEqual(session, self.SESSION_NAME)
238245
self.assertEqual(mutations, batch._mutations)
239246
self.assertIsInstance(single_use_txn, TransactionOptions)
@@ -246,12 +253,13 @@ def test_commit_ok(self):
246253
],
247254
)
248255
self.assertEqual(request_options, RequestOptions())
256+
self.assertEqual(max_commit_delay, None)
249257

250258
self.assertSpanAttributes(
251259
"CloudSpanner.Commit", attributes=dict(BASE_ATTRIBUTES, num_mutations=1)
252260
)
253261

254-
def _test_commit_with_request_options(self, request_options=None):
262+
def _test_commit_with_options(self, request_options=None, max_commit_delay_in=None):
255263
import datetime
256264
from google.cloud.spanner_v1 import CommitResponse
257265
from google.cloud.spanner_v1 import TransactionOptions
@@ -267,7 +275,9 @@ def _test_commit_with_request_options(self, request_options=None):
267275
batch = self._make_one(session)
268276
batch.transaction_tag = self.TRANSACTION_TAG
269277
batch.insert(TABLE_NAME, COLUMNS, VALUES)
270-
committed = batch.commit(request_options=request_options)
278+
committed = batch.commit(
279+
request_options=request_options, max_commit_delay=max_commit_delay_in
280+
)
271281

272282
self.assertEqual(committed, now)
273283
self.assertEqual(batch.committed, committed)
@@ -284,6 +294,7 @@ def _test_commit_with_request_options(self, request_options=None):
284294
mutations,
285295
single_use_txn,
286296
actual_request_options,
297+
max_commit_delay,
287298
metadata,
288299
) = api._committed
289300
self.assertEqual(session, self.SESSION_NAME)
@@ -303,33 +314,46 @@ def _test_commit_with_request_options(self, request_options=None):
303314
"CloudSpanner.Commit", attributes=dict(BASE_ATTRIBUTES, num_mutations=1)
304315
)
305316

317+
self.assertEqual(max_commit_delay_in, max_commit_delay)
318+
306319
def test_commit_w_request_tag_success(self):
307320
request_options = RequestOptions(
308321
request_tag="tag-1",
309322
)
310-
self._test_commit_with_request_options(request_options=request_options)
323+
self._test_commit_with_options(request_options=request_options)
311324

312325
def test_commit_w_transaction_tag_success(self):
313326
request_options = RequestOptions(
314327
transaction_tag="tag-1-1",
315328
)
316-
self._test_commit_with_request_options(request_options=request_options)
329+
self._test_commit_with_options(request_options=request_options)
317330

318331
def test_commit_w_request_and_transaction_tag_success(self):
319332
request_options = RequestOptions(
320333
request_tag="tag-1",
321334
transaction_tag="tag-1-1",
322335
)
323-
self._test_commit_with_request_options(request_options=request_options)
336+
self._test_commit_with_options(request_options=request_options)
324337

325338
def test_commit_w_request_and_transaction_tag_dictionary_success(self):
326339
request_options = {"request_tag": "tag-1", "transaction_tag": "tag-1-1"}
327-
self._test_commit_with_request_options(request_options=request_options)
340+
self._test_commit_with_options(request_options=request_options)
328341

329342
def test_commit_w_incorrect_tag_dictionary_error(self):
330343
request_options = {"incorrect_tag": "tag-1-1"}
331344
with self.assertRaises(ValueError):
332-
self._test_commit_with_request_options(request_options=request_options)
345+
self._test_commit_with_options(request_options=request_options)
346+
347+
def test_commit_w_max_commit_delay(self):
348+
import datetime
349+
350+
request_options = RequestOptions(
351+
request_tag="tag-1",
352+
)
353+
self._test_commit_with_options(
354+
request_options=request_options,
355+
max_commit_delay_in=datetime.timedelta(milliseconds=100),
356+
)
333357

334358
def test_context_mgr_already_committed(self):
335359
import datetime
@@ -368,7 +392,14 @@ def test_context_mgr_success(self):
368392

369393
self.assertEqual(batch.committed, now)
370394

371-
(session, mutations, single_use_txn, request_options, metadata) = api._committed
395+
(
396+
session,
397+
mutations,
398+
single_use_txn,
399+
request_options,
400+
_,
401+
metadata,
402+
) = api._committed
372403
self.assertEqual(session, self.SESSION_NAME)
373404
self.assertEqual(mutations, batch._mutations)
374405
self.assertIsInstance(single_use_txn, TransactionOptions)
@@ -565,12 +596,17 @@ def commit(
565596
):
566597
from google.api_core.exceptions import Unknown
567598

599+
max_commit_delay = None
600+
if type(request).pb(request).HasField("max_commit_delay"):
601+
max_commit_delay = request.max_commit_delay
602+
568603
assert request.transaction_id == b""
569604
self._committed = (
570605
request.session,
571606
request.mutations,
572607
request.single_use_transaction,
573608
request.request_options,
609+
max_commit_delay,
574610
metadata,
575611
)
576612
if self._rpc_error:

0 commit comments

Comments
 (0)