1919from grpc .aio import AioRpcError
2020from grpc .aio import Metadata
2121
22+ from google .api_core .exceptions import Aborted
2223from google .api_core .exceptions import GoogleAPICallError
24+ from google .api_core .exceptions import PermissionDenied
2325from google .cloud .bigtable_v2 .types import ResponseParams
2426from google .cloud .bigtable .data ._metrics .handlers ._base import MetricsHandler
2527from google .cloud .bigtable .data ._metrics .data_model import CompletedOperationMetric , CompletedAttemptMetric , ActiveOperationMetric , OperationState
@@ -66,19 +68,21 @@ def __repr__(self):
6668 return f"{ self .__class__ } (completed_operations={ len (self .completed_operations )} , cancelled_operations={ len (self .cancelled_operations )} , completed_attempts={ len (self .completed_attempts )} "
6769
6870
69- class _ErrorInjectorInterceptor (UnaryUnaryClientInterceptor ):
71+ class _ErrorInjectorInterceptor (UnaryUnaryClientInterceptor , UnaryStreamClientInterceptor ):
7072 """
7173 Gprc interceptor used to inject errors into rpc calls, to test failures
7274 """
7375
7476 def __init__ (self ):
7577 self ._exc_list = []
78+ self .fail_mid_stream = False
7679
7780 def push (self , exc : Exception ):
7881 self ._exc_list .append (exc )
7982
8083 def clear (self ):
8184 self ._exc_list .clear ()
85+ self .fail_mid_stream = False
8286
8387 async def intercept_unary_unary (
8488 self , continuation , client_call_details , request
@@ -87,6 +91,41 @@ async def intercept_unary_unary(
8791 raise self ._exc_list .pop (0 )
8892 return await continuation (client_call_details , request )
8993
94+ async def intercept_unary_stream (
95+ self , continuation , client_call_details , request
96+ ):
97+ if not self .fail_mid_stream and self ._exc_list :
98+ raise self ._exc_list .pop (0 )
99+
100+ response = await continuation (client_call_details , request )
101+
102+ if self .fail_mid_stream and self ._exc_list :
103+ exc = self ._exc_list .pop (0 )
104+
105+ class CallWrapper :
106+ def __init__ (self , call , exc_to_raise ):
107+ self ._call = call
108+ self ._exc = exc_to_raise
109+ self ._raised = False
110+
111+ def __aiter__ (self ):
112+ return self
113+
114+ async def __anext__ (self ):
115+ if not self ._raised :
116+ self ._raised = True
117+ if self ._exc :
118+ raise self ._exc
119+ return await self ._call .__anext__ ()
120+
121+ def __getattr__ (self , name ):
122+ return getattr (self ._call , name )
123+
124+ return CallWrapper (response , exc )
125+
126+ return response
127+
128+
90129@CrossSync .convert_class (sync_name = "TestMetrics" )
91130class TestMetricsAsync (SystemTestRunner ):
92131
@@ -123,6 +162,9 @@ async def client(self, error_injector):
123162 async with self ._make_client () as client :
124163 if CrossSync .is_async :
125164 client .transport .grpc_channel ._unary_unary_interceptors .append (error_injector )
165+ client .transport .grpc_channel ._unary_stream_interceptors .append (
166+ error_injector
167+ )
126168 yield client
127169
128170 @CrossSync .convert
@@ -679,41 +721,147 @@ async def test_sample_row_keys_failure_grpc(
679721 self , table , temp_rows , handler , error_injector
680722 ):
681723 """
682- Test failure in grpc layer by injecting an error into an interceptor
724+ Test failure in grpc layer by injecting errors into an interceptor
725+ test with retryable errors, then a terminal one
726+
727+ No headers expected
728+ """
729+ exc = Aborted ("injected" )
730+ num_retryable = 3
731+ for i in range (num_retryable ):
732+ error_injector .push (exc )
733+ error_injector .push (RuntimeError )
734+ with pytest .raises (RuntimeError ):
735+ await table .sample_row_keys (retryable_errors = [Aborted ])
736+ # validate counts
737+ assert len (handler .completed_operations ) == 1
738+ assert len (handler .completed_attempts ) == num_retryable + 1
739+ assert len (handler .cancelled_operations ) == 0
740+ # validate operation
741+ operation = handler .completed_operations [0 ]
742+ assert isinstance (operation , CompletedOperationMetric )
743+ assert operation .final_status .name == "UNKNOWN"
744+ assert operation .op_type .value == "SampleRowKeys"
745+ assert operation .is_streaming is False
746+ assert len (operation .completed_attempts ) == num_retryable + 1
747+ assert operation .completed_attempts [0 ] == handler .completed_attempts [0 ]
748+ assert operation .cluster_id == "unspecified"
749+ assert operation .zone == "global"
750+ # validate attempts
751+ for i in range (num_retryable ):
752+ attempt = handler .completed_attempts [i ]
753+ assert isinstance (attempt , CompletedAttemptMetric )
754+ assert attempt .end_status .name == "ABORTED"
755+ assert attempt .gfe_latency_ns is None
756+ final_attempt = handler .completed_attempts [num_retryable ]
757+ assert isinstance (final_attempt , CompletedAttemptMetric )
758+ assert final_attempt .end_status .name == "UNKNOWN"
759+ assert final_attempt .gfe_latency_ns is None
683760
761+ @CrossSync .pytest
762+ async def test_sample_row_keys_failure_grpc_eventual_success (
763+ self , table , temp_rows , handler , error_injector , cluster_config
764+ ):
765+ """
766+ Test failure in grpc layer by injecting errors into an interceptor
767+ test with retryable errors, then a success
768+
684769 No headers expected
685770 """
686- pass
771+ exc = Aborted ("injected" )
772+ num_retryable = 3
773+ for i in range (num_retryable ):
774+ error_injector .push (exc )
775+ await table .sample_row_keys (retryable_errors = [Aborted ])
776+ # validate counts
777+ assert len (handler .completed_operations ) == 1
778+ assert len (handler .completed_attempts ) == num_retryable + 1
779+ assert len (handler .cancelled_operations ) == 0
780+ # validate operation
781+ operation = handler .completed_operations [0 ]
782+ assert isinstance (operation , CompletedOperationMetric )
783+ assert operation .final_status .name == "OK"
784+ assert operation .op_type .value == "SampleRowKeys"
785+ assert operation .is_streaming is False
786+ assert len (operation .completed_attempts ) == num_retryable + 1
787+ assert operation .completed_attempts [0 ] == handler .completed_attempts [0 ]
788+ assert operation .cluster_id == next (iter (cluster_config .keys ()))
789+ assert operation .zone == cluster_config [operation .cluster_id ].location .split ("/" )[- 1 ]
790+ # validate attempts
791+ for i in range (num_retryable ):
792+ attempt = handler .completed_attempts [i ]
793+ assert isinstance (attempt , CompletedAttemptMetric )
794+ assert attempt .end_status .name == "ABORTED"
795+ assert attempt .gfe_latency_ns is None
796+ final_attempt = handler .completed_attempts [num_retryable ]
797+ assert isinstance (final_attempt , CompletedAttemptMetric )
798+ assert final_attempt .end_status .name == "OK"
799+ assert final_attempt .gfe_latency_ns > 0 and final_attempt .gfe_latency_ns < operation .duration_ns
687800
688801
689802 @CrossSync .pytest
690803 async def test_sample_row_keys_failure_timeout (
691- self , table , temp_rows , handler
804+ self , table , handler
692805 ):
693806 """
694807 Test failure in gapic layer by passing very low timeout
695808
696809 No grpc headers expected
697810 """
698- pass
699-
700- @CrossSync .pytest
701- async def test_sample_row_keys_failure_unauthorized (
702- self , handler , authorized_view , cluster_config
703- ):
704- """
705- Test failure in backend by accessing an unauthorized family
706- """
707- pass
811+ with pytest .raises (GoogleAPICallError ):
812+ await table .sample_row_keys (operation_timeout = 0.001 )
813+ # validate counts
814+ assert len (handler .completed_operations ) == 1
815+ assert len (handler .completed_attempts ) == 1
816+ assert len (handler .cancelled_operations ) == 0
817+ # validate operation
818+ operation = handler .completed_operations [0 ]
819+ assert isinstance (operation , CompletedOperationMetric )
820+ assert operation .final_status .name == "DEADLINE_EXCEEDED"
821+ assert operation .op_type .value == "SampleRowKeys"
822+ assert operation .is_streaming is False
823+ assert len (operation .completed_attempts ) == 1
824+ assert operation .cluster_id == "unspecified"
825+ assert operation .zone == "global"
826+ # validate attempt
827+ attempt = handler .completed_attempts [0 ]
828+ assert isinstance (attempt , CompletedAttemptMetric )
829+ assert attempt .end_status .name == "DEADLINE_EXCEEDED"
830+ assert attempt .gfe_latency_ns is None
708831
709832 @CrossSync .pytest
710833 async def test_sample_row_keys_failure_mid_stream (
711- self , table , temp_rows , handler , error_injector
834+ self , table , temp_rows , handler , error_injector , cluster_config
712835 ):
713836 """
714837 Test failure in grpc stream
715838 """
716- pass
839+ error_injector .fail_mid_stream = True
840+ error_injector .push (Aborted ("retryable" ))
841+ error_injector .push (PermissionDenied ("terminal" ))
842+ with pytest .raises (PermissionDenied ):
843+ await table .sample_row_keys (retryable_errors = [Aborted ])
844+ # validate counts
845+ assert len (handler .completed_operations ) == 1
846+ assert len (handler .completed_attempts ) == 2
847+ assert len (handler .cancelled_operations ) == 0
848+ # validate operation
849+ operation = handler .completed_operations [0 ]
850+ assert operation .final_status .name == "PERMISSION_DENIED"
851+ assert operation .op_type .value == "SampleRowKeys"
852+ assert operation .is_streaming is False
853+ assert len (operation .completed_attempts ) == 2
854+ assert operation .cluster_id == next (iter (cluster_config .keys ()))
855+ assert (
856+ operation .zone
857+ == cluster_config [operation .cluster_id ].location .split ("/" )[- 1 ]
858+ )
859+ # validate retried attempt
860+ attempt = handler .completed_attempts [0 ]
861+ assert attempt .end_status .name == "ABORTED"
862+ # validate final attempt
863+ final_attempt = handler .completed_attempts [- 1 ]
864+ assert final_attempt .end_status .name == "PERMISSION_DENIED"
717865
718866
719867 @CrossSync .pytest
@@ -969,4 +1117,4 @@ async def test_check_and_mutate_row_failure_unauthorized(
9691117 assert operation .zone == cluster_config [operation .cluster_id ].location .split ("/" )[- 1 ]
9701118 # validate attempt
9711119 attempt = handler .completed_attempts [0 ]
972- assert attempt .gfe_latency_ns >= 0 and attempt .gfe_latency_ns < operation .duration_ns
1120+ assert attempt .gfe_latency_ns >= 0 and attempt .gfe_latency_ns < operation .duration_ns
0 commit comments