2121import google .cloud .bigtable_v2 .types .bigtable as types_pb
2222import google .cloud .bigtable .data .exceptions as bt_exceptions
2323from google .cloud .bigtable .data ._helpers import _attempt_timeout_generator
24- from google .cloud .bigtable .data ._helpers import _retry_exception_factory
24+ from google .cloud .bigtable .data ._metrics import tracked_retry
2525
2626# mutate_rows requests are limited to this number of mutations
2727from google .cloud .bigtable .data .mutations import _MUTATE_ROWS_REQUEST_MUTATION_LIMIT
3131
3232if TYPE_CHECKING :
3333 from google .cloud .bigtable .data .mutations import RowMutationEntry
34+ from google .cloud .bigtable .data ._metrics import ActiveOperationMetric
3435
3536 if CrossSync .is_async :
3637 from google .cloud .bigtable_v2 .services .bigtable .async_client import (
@@ -68,6 +69,8 @@ class _MutateRowsOperationAsync:
6869 operation_timeout: the timeout to use for the entire operation, in seconds.
6970 attempt_timeout: the timeout to use for each mutate_rows attempt, in seconds.
7071 If not specified, the request will run until operation_timeout is reached.
72+ metric: the metric object representing the active operation
73+ retryable_exceptions: a list of exceptions that should be retried
7174 """
7275
7376 @CrossSync .convert
@@ -78,6 +81,7 @@ def __init__(
7881 mutation_entries : list ["RowMutationEntry" ],
7982 operation_timeout : float ,
8083 attempt_timeout : float | None ,
84+ metric : ActiveOperationMetric ,
8185 retryable_exceptions : Sequence [type [Exception ]] = (),
8286 ):
8387 # check that mutations are within limits
@@ -97,13 +101,12 @@ def __init__(
97101 # Entry level errors
98102 bt_exceptions ._MutateRowsIncomplete ,
99103 )
100- sleep_generator = retries .exponential_sleep_generator (0.01 , 2 , 60 )
101- self ._operation = lambda : CrossSync .retry_target (
102- self ._run_attempt ,
103- self .is_retryable ,
104- sleep_generator ,
105- operation_timeout ,
106- exception_factory = _retry_exception_factory ,
104+ self ._operation = lambda : tracked_retry (
105+ retry_fn = CrossSync .retry_target ,
106+ operation = metric ,
107+ target = self ._run_attempt ,
108+ predicate = self .is_retryable ,
109+ timeout = operation_timeout ,
107110 )
108111 # initialize state
109112 self .timeout_generator = _attempt_timeout_generator (
@@ -112,6 +115,8 @@ def __init__(
112115 self .mutations = [_EntryWithProto (m , m ._to_pb ()) for m in mutation_entries ]
113116 self .remaining_indices = list (range (len (self .mutations )))
114117 self .errors : dict [int , list [Exception ]] = {}
118+ # set up metrics
119+ self ._operation_metric = metric
115120
116121 @CrossSync .convert
117122 async def start (self ):
@@ -121,34 +126,35 @@ async def start(self):
121126 Raises:
122127 MutationsExceptionGroup: if any mutations failed
123128 """
124- try :
125- # trigger mutate_rows
126- await self ._operation ()
127- except Exception as exc :
128- # exceptions raised by retryable are added to the list of exceptions for all unfinalized mutations
129- incomplete_indices = self .remaining_indices .copy ()
130- for idx in incomplete_indices :
131- self ._handle_entry_error (idx , exc )
132- finally :
133- # raise exception detailing incomplete mutations
134- all_errors : list [Exception ] = []
135- for idx , exc_list in self .errors .items ():
136- if len (exc_list ) == 0 :
137- raise core_exceptions .ClientError (
138- f"Mutation { idx } failed with no associated errors"
129+ with self ._operation_metric :
130+ try :
131+ # trigger mutate_rows
132+ await self ._operation ()
133+ except Exception as exc :
134+ # exceptions raised by retryable are added to the list of exceptions for all unfinalized mutations
135+ incomplete_indices = self .remaining_indices .copy ()
136+ for idx in incomplete_indices :
137+ self ._handle_entry_error (idx , exc )
138+ finally :
139+ # raise exception detailing incomplete mutations
140+ all_errors : list [Exception ] = []
141+ for idx , exc_list in self .errors .items ():
142+ if len (exc_list ) == 0 :
143+ raise core_exceptions .ClientError (
144+ f"Mutation { idx } failed with no associated errors"
145+ )
146+ elif len (exc_list ) == 1 :
147+ cause_exc = exc_list [0 ]
148+ else :
149+ cause_exc = bt_exceptions .RetryExceptionGroup (exc_list )
150+ entry = self .mutations [idx ].entry
151+ all_errors .append (
152+ bt_exceptions .FailedMutationEntryError (idx , entry , cause_exc )
153+ )
154+ if all_errors :
155+ raise bt_exceptions .MutationsExceptionGroup (
156+ all_errors , len (self .mutations )
139157 )
140- elif len (exc_list ) == 1 :
141- cause_exc = exc_list [0 ]
142- else :
143- cause_exc = bt_exceptions .RetryExceptionGroup (exc_list )
144- entry = self .mutations [idx ].entry
145- all_errors .append (
146- bt_exceptions .FailedMutationEntryError (idx , entry , cause_exc )
147- )
148- if all_errors :
149- raise bt_exceptions .MutationsExceptionGroup (
150- all_errors , len (self .mutations )
151- )
152158
153159 @CrossSync .convert
154160 async def _run_attempt (self ):
@@ -160,6 +166,8 @@ async def _run_attempt(self):
160166 retry after the attempt is complete
161167 GoogleAPICallError: if the gapic rpc fails
162168 """
169+ # register attempt start
170+ self ._operation_metric .start_attempt ()
163171 request_entries = [self .mutations [idx ].proto for idx in self .remaining_indices ]
164172 # track mutations in this request that have not been finalized yet
165173 active_request_indices = {
0 commit comments