@@ -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