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

Commit 759d198

Browse files
committed
added bulk_mutate_row tests
1 parent b52c871 commit 759d198

3 files changed

Lines changed: 133 additions & 20 deletions

File tree

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
from typing import Sequence, TYPE_CHECKING
1818

19+
from functools import partial
20+
1921
from google.api_core import exceptions as core_exceptions
2022
from google.api_core import retry as retries
2123
import google.cloud.bigtable_v2.types.bigtable as types_pb
@@ -106,7 +108,9 @@ def __init__(
106108
self.is_retryable,
107109
metric.backoff_generator,
108110
operation_timeout,
109-
exception_factory=_retry_exception_factory,
111+
exception_factory=partial(
112+
_retry_exception_factory, operation=metric
113+
),
110114
)
111115
# initialize state
112116
self.timeout_generator = _attempt_timeout_generator(

google/cloud/bigtable/data/_metrics/data_model.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -418,8 +418,11 @@ def __exit__(self, exc_type, exc_val, exc_tb):
418418
419419
The operation is automatically ended on exit, with the status determined
420420
by the exception type and value.
421+
422+
If operation was already ended manually, do nothing.
421423
"""
422-
if exc_val is None:
423-
self.end_with_success()
424-
else:
425-
self.end_with_status(exc_val)
424+
if not self.state == OperationState.COMPLETED:
425+
if exc_val is None:
426+
self.end_with_success()
427+
else:
428+
self.end_with_status(exc_val)

tests/system/data/test_metrics_async.py

Lines changed: 121 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -830,23 +830,35 @@ async def test_read_rows_sharded_failure_mid_stream(
830830
query2 = ReadRowsQuery(row_keys=[b"b"])
831831
handler.clear()
832832
error_injector.fail_mid_stream = True
833-
error_injector.push(PermissionDenied("terminal"))
833+
error_injector.push(Aborted("retryable"))
834834
error_injector.push(PermissionDenied("terminal"))
835835
with pytest.raises(ShardedReadRowsExceptionGroup) as e:
836-
await table.read_rows_sharded([query1, query2])
837-
assert len(e.value.exceptions) == 2
836+
await table.read_rows_sharded([query1, query2], retryable_errors=[Aborted])
837+
assert len(e.value.exceptions) == 1
838838
assert isinstance(e.value.exceptions[0].__cause__, PermissionDenied)
839+
# one shard will fail, the other will succeed
840+
# the failing shard will have one retry
839841
assert len(handler.completed_operations) == 2
840-
assert len(handler.completed_attempts) == 2
842+
assert len(handler.completed_attempts) == 3
841843
assert len(handler.cancelled_operations) == 0
842-
for operation in handler.completed_operations:
843-
assert operation.final_status.name == "PERMISSION_DENIED"
844-
assert operation.op_type.value == "ReadRows"
845-
assert operation.is_streaming is True
846-
assert len(operation.completed_attempts) == 1
847-
# validate attempt
848-
attempt = operation.completed_attempts[0]
849-
assert attempt.end_status.name == "PERMISSION_DENIED"
844+
# sort operations by status
845+
failed_op = next(op for op in handler.completed_operations if op.final_status.name != "OK")
846+
success_op = next(op for op in handler.completed_operations if op.final_status.name == "OK")
847+
# validate failed operation
848+
assert failed_op.final_status.name == "PERMISSION_DENIED"
849+
assert failed_op.op_type.value == "ReadRows"
850+
assert failed_op.is_streaming is True
851+
assert len(failed_op.completed_attempts) == 2
852+
# validate retried attempt
853+
attempt = failed_op.completed_attempts[0]
854+
assert attempt.end_status.name == "ABORTED"
855+
# validate final attempt
856+
final_attempt = failed_op.completed_attempts[-1]
857+
assert final_attempt.end_status.name == "PERMISSION_DENIED"
858+
# validate successful operation
859+
assert success_op.final_status.name == "OK"
860+
assert len(success_op.completed_attempts) == 1
861+
assert success_op.completed_attempts[0].end_status.name == "OK"
850862

851863
@CrossSync.pytest
852864
async def test_bulk_mutate_rows(self, table, temp_rows, handler, cluster_config):
@@ -896,7 +908,45 @@ async def test_bulk_mutate_rows_failure_grpc(
896908
897909
No headers expected
898910
"""
899-
pass
911+
from google.cloud.bigtable.data.mutations import RowMutationEntry, SetCell
912+
from google.cloud.bigtable.data.exceptions import MutationsExceptionGroup
913+
914+
row_key = b"row_key_1"
915+
mutation = SetCell(TEST_FAMILY, b"q", b"v")
916+
entry = RowMutationEntry(row_key, [mutation])
917+
assert entry.is_idempotent()
918+
919+
handler.clear()
920+
exc = Aborted("injected")
921+
num_retryable = 2
922+
for i in range(num_retryable):
923+
error_injector.push(exc)
924+
error_injector.push(PermissionDenied("terminal"))
925+
with pytest.raises(MutationsExceptionGroup) as e:
926+
await table.bulk_mutate_rows([entry], retryable_errors=[Aborted])
927+
# validate counts
928+
assert len(handler.completed_operations) == 1
929+
assert len(handler.completed_attempts) == num_retryable + 1
930+
assert len(handler.cancelled_operations) == 0
931+
# validate operation
932+
operation = handler.completed_operations[0]
933+
assert isinstance(operation, CompletedOperationMetric)
934+
assert operation.final_status.name == "PERMISSION_DENIED"
935+
assert operation.op_type.value == "MutateRows"
936+
assert operation.is_streaming is False
937+
assert len(operation.completed_attempts) == num_retryable + 1
938+
assert operation.cluster_id == "unspecified"
939+
assert operation.zone == "global"
940+
# validate attempts
941+
for i in range(num_retryable):
942+
attempt = handler.completed_attempts[i]
943+
assert isinstance(attempt, CompletedAttemptMetric)
944+
assert attempt.end_status.name == "ABORTED"
945+
assert attempt.gfe_latency_ns is None
946+
final_attempt = handler.completed_attempts[num_retryable]
947+
assert isinstance(final_attempt, CompletedAttemptMetric)
948+
assert final_attempt.end_status.name == "PERMISSION_DENIED"
949+
assert final_attempt.gfe_latency_ns is None
900950

901951
@CrossSync.pytest
902952
async def test_bulk_mutate_rows_failure_timeout(
@@ -907,7 +957,35 @@ async def test_bulk_mutate_rows_failure_timeout(
907957
908958
No grpc headers expected
909959
"""
910-
pass
960+
from google.cloud.bigtable.data.mutations import RowMutationEntry, SetCell
961+
from google.cloud.bigtable.data.exceptions import MutationsExceptionGroup
962+
from google.api_core.exceptions import DeadlineExceeded
963+
964+
row_key = b"row_key_1"
965+
mutation = SetCell(TEST_FAMILY, b"q", b"v")
966+
entry = RowMutationEntry(row_key, [mutation])
967+
968+
handler.clear()
969+
with pytest.raises(MutationsExceptionGroup) as e:
970+
await table.bulk_mutate_rows([entry], operation_timeout=0.001)
971+
# validate counts
972+
assert len(handler.completed_operations) == 1
973+
assert len(handler.completed_attempts) == 1
974+
assert len(handler.cancelled_operations) == 0
975+
# validate operation
976+
operation = handler.completed_operations[0]
977+
assert isinstance(operation, CompletedOperationMetric)
978+
assert operation.final_status.name == "DEADLINE_EXCEEDED"
979+
assert operation.op_type.value == "MutateRows"
980+
assert operation.is_streaming is False
981+
assert len(operation.completed_attempts) == 1
982+
assert operation.cluster_id == "unspecified"
983+
assert operation.zone == "global"
984+
# validate attempt
985+
attempt = handler.completed_attempts[0]
986+
assert isinstance(attempt, CompletedAttemptMetric)
987+
assert attempt.end_status.name == "DEADLINE_EXCEEDED"
988+
assert attempt.gfe_latency_ns is None
911989

912990
@CrossSync.pytest
913991
async def test_bulk_mutate_rows_failure_unauthorized(
@@ -916,7 +994,35 @@ async def test_bulk_mutate_rows_failure_unauthorized(
916994
"""
917995
Test failure in backend by accessing an unauthorized family
918996
"""
919-
pass
997+
from google.cloud.bigtable.data.mutations import RowMutationEntry, SetCell
998+
from google.cloud.bigtable.data.exceptions import MutationsExceptionGroup
999+
1000+
row_key = b"row_key_1"
1001+
mutation = SetCell("unauthorized", b"q", b"v")
1002+
entry = RowMutationEntry(row_key, [mutation])
1003+
1004+
handler.clear()
1005+
with pytest.raises(MutationsExceptionGroup) as e:
1006+
await authorized_view.bulk_mutate_rows([entry])
1007+
assert len(e.value.exceptions) == 1
1008+
assert isinstance(e.value.exceptions[0].__cause__, GoogleAPICallError)
1009+
assert e.value.exceptions[0].__cause__.grpc_status_code.name == "PERMISSION_DENIED"
1010+
# validate counts
1011+
assert len(handler.completed_operations) == 1
1012+
assert len(handler.completed_attempts) == 1
1013+
assert len(handler.cancelled_operations) == 0
1014+
# validate operation
1015+
operation = handler.completed_operations[0]
1016+
assert operation.final_status.name == "PERMISSION_DENIED"
1017+
assert operation.op_type.value == "MutateRows"
1018+
assert operation.is_streaming is False
1019+
assert len(operation.completed_attempts) == 1
1020+
assert operation.cluster_id == next(iter(cluster_config.keys()))
1021+
assert operation.zone == cluster_config[operation.cluster_id].location.split("/")[-1]
1022+
# validate attempt
1023+
attempt = handler.completed_attempts[0]
1024+
assert attempt.end_status.name == "PERMISSION_DENIED"
1025+
assert attempt.gfe_latency_ns >= 0 and attempt.gfe_latency_ns < operation.duration_ns
9201026

9211027
@CrossSync.pytest
9221028
async def test_mutate_rows_batcher(self, table, temp_rows, handler, cluster_config):

0 commit comments

Comments
 (0)