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

Commit 6aca433

Browse files
committed
addressed review feedback
1 parent f47e606 commit 6aca433

10 files changed

Lines changed: 107 additions & 56 deletions

File tree

google/cloud/bigtable/batcher.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,13 @@ def __init__(
8888
batch_completed_callback=None,
8989
):
9090
self.table = table
91-
self._flush_count = flush_count
92-
self._max_row_bytes = max_row_bytes
93-
self._flush_interval = flush_interval
91+
self._batcher_kwargs = {
92+
"flush_interval": flush_interval,
93+
"flush_limit_mutation_count": flush_count,
94+
"flush_limit_bytes": max_row_bytes,
95+
"flow_control_max_mutation_count": MAX_OUTSTANDING_ELEMENTS,
96+
"flow_control_max_bytes": MAX_OUTSTANDING_BYTES,
97+
}
9498
self._user_batch_completed_callback = batch_completed_callback
9599
self._init_batcher()
96100
atexit.register(self.close)
@@ -105,11 +109,7 @@ def max_row_bytes(self):
105109
return self._max_row_bytes
106110

107111
def _init_batcher(self):
108-
self._batcher = self.table._table_impl.mutations_batcher(
109-
flush_interval=self._flush_interval,
110-
flush_limit_mutation_count=self._flush_count,
111-
flush_limit_bytes=self._max_row_bytes,
112-
)
112+
self._batcher = self.table._table_impl.mutations_batcher(**self._batcher_kwargs)
113113
self._batcher._user_batch_completed_callback = (
114114
self._user_batch_completed_callback
115115
)
@@ -119,8 +119,8 @@ def _close_batcher(self):
119119
self._batcher.close()
120120
except MutationsExceptionGroup as exc_group:
121121
for error in exc_group.exceptions:
122-
# Return the cause of the FailedMutationEntryError to the user,
123-
# as this might be more what they're expecting.
122+
# Unpack the root cause of the FailedMutationEntryError
123+
# and return that error to the user.
124124
self._exceptions.put(error.__cause__)
125125

126126
def __enter__(self):
@@ -178,7 +178,7 @@ def flush(self):
178178
:dedent: 4
179179
180180
:raises:
181-
* :exc:`.batcherMutationsBatchError` if there's any error in the mutations.
181+
* :exc:`~batcher.MutationsBatchError` if there's any error in the mutations.
182182
"""
183183
self._close_batcher()
184184
self._init_batcher()
@@ -192,7 +192,7 @@ def close(self):
192192
Any errors will be raised.
193193
194194
:raises:
195-
* :exc:`.batcherMutationsBatchError` if there's any error in the mutations.
195+
* :exc:`~batcher.MutationsBatchError` if there's any error in the mutations.
196196
"""
197197
self._close_batcher()
198198
atexit.unregister(self.close)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ async def start(self):
131131
self._handle_entry_error(idx, exc)
132132
finally:
133133
# raise exception detailing incomplete mutations
134-
all_errors: list[Exception] = []
134+
all_errors: list[bt_exceptions.FailedMutationEntryError] = []
135135
for idx, exc_list in self.errors.items():
136136
if len(exc_list) == 0:
137137
raise core_exceptions.ClientError(

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

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -272,10 +272,11 @@ def __init__(
272272
self._exceptions_since_last_raise: int = 0
273273
# keep track of the first and last _exception_list_limit exceptions
274274
self._exception_list_limit: int = 10
275-
self._oldest_exceptions: list[Exception] = []
276-
self._newest_exceptions: deque[Exception] = deque(
275+
self._oldest_exceptions: list[FailedMutationEntryError] = []
276+
self._newest_exceptions: deque[FailedMutationEntryError] = deque(
277277
maxlen=self._exception_list_limit
278278
)
279+
# only used by the shim right now.
279280
self._user_batch_completed_callback: Optional[
280281
Callable[[list[status_pb2.Status]], None]
281282
] = None
@@ -363,13 +364,17 @@ async def _flush_internal(self, new_entries: list[RowMutationEntry]):
363364
"""
364365
# flush new entries
365366
in_process_requests: list[CrossSync.Future[list[FailedMutationEntryError]]] = []
367+
in_process_batches: list[list[RowMutationEntry]] = []
366368
async for batch in self._flow_control.add_to_flow(new_entries):
367369
batch_task = CrossSync.create_task(
368370
self._execute_mutate_rows, batch, sync_executor=self._sync_rpc_executor
369371
)
370372
in_process_requests.append(batch_task)
373+
in_process_batches.append(batch)
371374
# wait for all inflight requests to complete
372-
found_exceptions = await self._wait_for_batch_results(*in_process_requests)
375+
found_exceptions = await self._wait_for_batch_results(
376+
in_process_requests, in_process_batches
377+
)
373378
# update exception data to reflect any new errors
374379
self._entries_processed_since_last_raise += len(new_entries)
375380
self._add_exceptions(found_exceptions)
@@ -419,7 +424,7 @@ async def _execute_mutate_rows(
419424
self._user_batch_completed_callback(statuses)
420425
return []
421426

422-
def _add_exceptions(self, excs: list[Exception]):
427+
def _add_exceptions(self, excs: list[FailedMutationEntryError]):
423428
"""
424429
Add new list of exceptions to internal store. To avoid unbounded memory,
425430
the batcher will store the first and last _exception_list_limit exceptions,
@@ -519,26 +524,27 @@ def _on_exit(self):
519524
@staticmethod
520525
@CrossSync.convert
521526
async def _wait_for_batch_results(
522-
*tasks: CrossSync.Future[list[FailedMutationEntryError]]
523-
| CrossSync.Future[None],
524-
) -> list[Exception]:
527+
tasks: Sequence[
528+
CrossSync.Future[list[FailedMutationEntryError]] | CrossSync.Future[None]
529+
],
530+
batches: Sequence[list[RowMutationEntry]],
531+
) -> list[FailedMutationEntryError]:
525532
"""
526533
Takes in a list of futures representing _execute_mutate_rows tasks,
527534
waits for them to complete, and returns a list of errors encountered.
528535
529536
Args:
530537
*tasks: futures representing _execute_mutate_rows or _flush_internal tasks
531538
Returns:
532-
list[Exception]:
533-
list of Exceptions encountered by any of the tasks. Errors are expected
534-
to be FailedMutationEntryError, representing a failed mutation operation.
535-
If a task fails with a different exception, it will be included in the
536-
output list. Successful tasks will not be represented in the output list.
539+
list[FailedMutationEntryError]:
540+
list of FailedMutationEntryError encountered by any of the tasks,
541+
representing a failed mutation operation.
542+
Successful tasks will not be represented in the output list.
537543
"""
538544
if not tasks:
539545
return []
540-
exceptions: list[Exception] = []
541-
for task in tasks:
546+
exceptions: list[FailedMutationEntryError] = []
547+
for task, batch in list(zip(tasks, batches)):
542548
if CrossSync.is_async:
543549
# futures don't need to be awaited in sync mode
544550
await task
@@ -550,6 +556,16 @@ async def _wait_for_batch_results(
550556
# strip index information
551557
exc.index = None
552558
exceptions.extend(exc_list)
553-
except Exception as e:
559+
except FailedMutationEntryError as e:
554560
exceptions.append(e)
561+
except Exception as e:
562+
exceptions.extend(
563+
[
564+
FailedMutationEntryError(
565+
failed_idx=None, failed_mutation_entry=entry, cause=e
566+
)
567+
for entry in batch
568+
]
569+
)
570+
555571
return exceptions

google/cloud/bigtable/data/_sync_autogen/_mutate_rows.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def start(self):
102102
for idx in incomplete_indices:
103103
self._handle_entry_error(idx, exc)
104104
finally:
105-
all_errors: list[Exception] = []
105+
all_errors: list[bt_exceptions.FailedMutationEntryError] = []
106106
for idx, exc_list in self.errors.items():
107107
if len(exc_list) == 0:
108108
raise core_exceptions.ClientError(

google/cloud/bigtable/data/_sync_autogen/mutations_batcher.py

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,8 @@ def __init__(
234234
self._entries_processed_since_last_raise: int = 0
235235
self._exceptions_since_last_raise: int = 0
236236
self._exception_list_limit: int = 10
237-
self._oldest_exceptions: list[Exception] = []
238-
self._newest_exceptions: deque[Exception] = deque(
237+
self._oldest_exceptions: list[FailedMutationEntryError] = []
238+
self._newest_exceptions: deque[FailedMutationEntryError] = deque(
239239
maxlen=self._exception_list_limit
240240
)
241241
self._user_batch_completed_callback: Optional[
@@ -310,12 +310,16 @@ def _flush_internal(self, new_entries: list[RowMutationEntry]):
310310
in_process_requests: list[
311311
CrossSync._Sync_Impl.Future[list[FailedMutationEntryError]]
312312
] = []
313+
in_process_batches: list[list[RowMutationEntry]] = []
313314
for batch in self._flow_control.add_to_flow(new_entries):
314315
batch_task = CrossSync._Sync_Impl.create_task(
315316
self._execute_mutate_rows, batch, sync_executor=self._sync_rpc_executor
316317
)
317318
in_process_requests.append(batch_task)
318-
found_exceptions = self._wait_for_batch_results(*in_process_requests)
319+
in_process_batches.append(batch)
320+
found_exceptions = self._wait_for_batch_results(
321+
in_process_requests, in_process_batches
322+
)
319323
self._entries_processed_since_last_raise += len(new_entries)
320324
self._add_exceptions(found_exceptions)
321325

@@ -356,7 +360,7 @@ def _execute_mutate_rows(
356360
self._user_batch_completed_callback(statuses)
357361
return []
358362

359-
def _add_exceptions(self, excs: list[Exception]):
363+
def _add_exceptions(self, excs: list[FailedMutationEntryError]):
360364
"""Add new list of exceptions to internal store. To avoid unbounded memory,
361365
the batcher will store the first and last _exception_list_limit exceptions,
362366
and discard any in between.
@@ -435,31 +439,41 @@ def _on_exit(self):
435439

436440
@staticmethod
437441
def _wait_for_batch_results(
438-
*tasks: CrossSync._Sync_Impl.Future[list[FailedMutationEntryError]]
439-
| CrossSync._Sync_Impl.Future[None],
440-
) -> list[Exception]:
442+
tasks: Sequence[
443+
CrossSync._Sync_Impl.Future[list[FailedMutationEntryError]]
444+
| CrossSync._Sync_Impl.Future[None]
445+
],
446+
batches: Sequence[list[RowMutationEntry]],
447+
) -> list[FailedMutationEntryError]:
441448
"""Takes in a list of futures representing _execute_mutate_rows tasks,
442449
waits for them to complete, and returns a list of errors encountered.
443450
444451
Args:
445452
*tasks: futures representing _execute_mutate_rows or _flush_internal tasks
446453
Returns:
447-
list[Exception]:
448-
list of Exceptions encountered by any of the tasks. Errors are expected
449-
to be FailedMutationEntryError, representing a failed mutation operation.
450-
If a task fails with a different exception, it will be included in the
451-
output list. Successful tasks will not be represented in the output list.
452-
"""
454+
list[FailedMutationEntryError]:
455+
list of FailedMutationEntryError encountered by any of the tasks,
456+
representing a failed mutation operation.
457+
Successful tasks will not be represented in the output list."""
453458
if not tasks:
454459
return []
455-
exceptions: list[Exception] = []
456-
for task in tasks:
460+
exceptions: list[FailedMutationEntryError] = []
461+
for task, batch in list(zip(tasks, batches)):
457462
try:
458463
exc_list = task.result()
459464
if exc_list:
460465
for exc in exc_list:
461466
exc.index = None
462467
exceptions.extend(exc_list)
463-
except Exception as e:
468+
except FailedMutationEntryError as e:
464469
exceptions.append(e)
470+
except Exception as e:
471+
exceptions.extend(
472+
[
473+
FailedMutationEntryError(
474+
failed_idx=None, failed_mutation_entry=entry, cause=e
475+
)
476+
for entry in batch
477+
]
478+
)
465479
return exceptions

google/cloud/bigtable/data/exceptions.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ class MutationsExceptionGroup(_BigtableExceptionGroup):
153153

154154
@staticmethod
155155
def _format_message(
156-
excs: list[Exception], total_entries: int, exc_count: int | None = None
156+
excs: list[FailedMutationEntryError],
157+
total_entries: int,
158+
exc_count: int | None = None,
157159
) -> str:
158160
"""
159161
Format a message for the exception group
@@ -171,7 +173,10 @@ def _format_message(
171173
return f"{exc_count} failed {entry_str} from {total_entries} attempted."
172174

173175
def __init__(
174-
self, excs: list[Exception], total_entries: int, message: str | None = None
176+
self,
177+
excs: list[FailedMutationEntryError],
178+
total_entries: int,
179+
message: str | None = None,
175180
):
176181
"""
177182
Args:
@@ -189,7 +194,10 @@ def __init__(
189194
self.total_entries_attempted = total_entries
190195

191196
def __new__(
192-
cls, excs: list[Exception], total_entries: int, message: str | None = None
197+
cls,
198+
excs: list[FailedMutationEntryError],
199+
total_entries: int,
200+
message: str | None = None,
193201
):
194202
"""
195203
Args:
@@ -209,8 +217,8 @@ def __new__(
209217
@classmethod
210218
def from_truncated_lists(
211219
cls,
212-
first_list: list[Exception],
213-
last_list: list[Exception],
220+
first_list: list[FailedMutationEntryError],
221+
last_list: list[FailedMutationEntryError],
214222
total_excs: int,
215223
entry_count: int,
216224
) -> MutationsExceptionGroup:

tests/unit/data/_async/test_mutations_batcher.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,10 @@ async def mock_call(*args, **kwargs):
640640
for _ in range(num_entries):
641641
await instance.append(self._make_mutation(size=1))
642642
# let any flush jobs finish
643-
await instance._wait_for_batch_results(*instance._flush_jobs)
643+
jobs = instance._flush_jobs
644+
await instance._wait_for_batch_results(
645+
jobs, [mock.MagicMock()] * len(jobs)
646+
)
644647
# should have only flushed once, with large mutation and first mutation in loop
645648
assert op_mock.call_count == 1
646649
sent_batch = op_mock.call_args[0][0]
@@ -740,7 +743,10 @@ async def mock_call(*args, **kwargs):
740743
)
741744
await CrossSync.sleep(0.01)
742745
# allow flushes to complete
743-
await instance._wait_for_batch_results(*instance._flush_jobs)
746+
jobs = instance._flush_jobs
747+
await instance._wait_for_batch_results(
748+
jobs, [mock.MagicMock()] * len(jobs)
749+
)
744750
duration = time.monotonic() - start_time
745751
assert len(instance._oldest_exceptions) == 0
746752
assert len(instance._newest_exceptions) == 0

tests/unit/data/_sync_autogen/test_mutations_batcher.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,8 @@ def mock_call(*args, **kwargs):
556556
num_entries = 10
557557
for _ in range(num_entries):
558558
instance.append(self._make_mutation(size=1))
559-
instance._wait_for_batch_results(*instance._flush_jobs)
559+
jobs = instance._flush_jobs
560+
instance._wait_for_batch_results(jobs, [mock.MagicMock()] * len(jobs))
560561
assert op_mock.call_count == 1
561562
sent_batch = op_mock.call_args[0][0]
562563
assert len(sent_batch) == 2
@@ -646,7 +647,8 @@ def mock_call(*args, **kwargs):
646647
[self._make_mutation(count=1)]
647648
)
648649
CrossSync._Sync_Impl.sleep(0.01)
649-
instance._wait_for_batch_results(*instance._flush_jobs)
650+
jobs = instance._flush_jobs
651+
instance._wait_for_batch_results(jobs, [mock.MagicMock()] * len(jobs))
650652
duration = time.monotonic() - start_time
651653
assert len(instance._oldest_exceptions) == 0
652654
assert len(instance._newest_exceptions) == 0

tests/unit/v2_client/test_batcher.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ def _atexit_mock():
5959

6060

6161
def test_mutations_batcher_constructor(_setup_batcher, _atexit_mock):
62+
from google.cloud.bigtable.batcher import MAX_OUTSTANDING_ELEMENTS
63+
from google.cloud.bigtable.batcher import MAX_OUTSTANDING_BYTES
64+
6265
flush_count = 5
6366
flush_interval = 0.1
6467
max_row_bytes = 10000
@@ -77,6 +80,8 @@ def test_mutations_batcher_constructor(_setup_batcher, _atexit_mock):
7780
flush_interval=flush_interval,
7881
flush_limit_mutation_count=flush_count,
7982
flush_limit_bytes=max_row_bytes,
83+
flow_control_max_mutation_count=MAX_OUTSTANDING_ELEMENTS,
84+
flow_control_max_bytes=MAX_OUTSTANDING_BYTES,
8085
)
8186
assert mutation_batcher.close in _atexit_mock._functions
8287

tests/unit/v2_client/test_table.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -976,8 +976,8 @@ def test_table_mutations_batcher_factory():
976976
)
977977

978978
assert mutation_batcher.table.table_id == TABLE_ID
979-
assert mutation_batcher.flush_count == flush_count
980-
assert mutation_batcher.max_row_bytes == max_row_bytes
979+
assert mutation_batcher._batcher_kwargs["flush_limit_mutation_count"] == flush_count
980+
assert mutation_batcher._batcher_kwargs["flush_limit_bytes"] == max_row_bytes
981981

982982

983983
def test_table_get_iam_policy():

0 commit comments

Comments
 (0)