Repository navigation
fix(bigtable): pack unpacked error details in _get_status and harden batcher error handling - #18770
Conversation
There was a problem hiding this comment.
Code Review
This pull request optimizes the mutations batcher by deferring status list creation until a user callback is present, and wraps the async task awaiting in a try-catch block to handle unexpected exceptions. It also updates error helper functions to correctly pack unpacked error details into protobuf Any messages and validate gRPC error status codes, accompanied by comprehensive unit tests. The review feedback suggests a valuable simplification in _wait_for_batch_results to directly assign the awaited task result in async mode instead of calling task.result() redundantly.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces several robustness improvements to the Bigtable mutations batcher and helper utilities. Key changes include extracting the terminal exception from a RetryExceptionGroup in the batcher, refactoring user batch completion callback execution to properly handle synchronous and asynchronous contexts, moving task awaiting inside try-except blocks to catch unexpected task exceptions, packing unpacked error details into status_pb2.Status objects, and validating gRPC error status codes in _get_error_type. Corresponding unit tests have been added to verify these behaviors. Feedback on the changes suggests defaulting exc.message to an empty string when constructing status_pb2.Status to prevent potential TypeError exceptions if the message is None.
| result.close() | ||
| raise TypeError( | ||
| "_user_batch_completed_callback must be a synchronous callable" | ||
| ) |
There was a problem hiding this comment.
Previously, this exception was only being logged as a warning. Now it actually shows up as a failed request. And IIRC, this method intends to return errors as a list, so this exception would remove that signal.
Is that intentional?
There was a problem hiding this comment.
Good point, reverted this change. Maybe we should do this check and construction time?
There was a problem hiding this comment.
I think this is passed after construction. Currently, it's only used as an internal attribute though I believe
| if isinstance(call_code, bool): | ||
| raise ValueError(f"Invalid gRPC error status code: {call_code!r}") | ||
| error_type = type(core_exceptions.from_grpc_status(call_code, "")) | ||
| if error_type is core_exceptions.GoogleAPICallError: |
There was a problem hiding this comment.
nit: a comment here could be useful.
On a skim, this may look like a standard isinstance check, but this is specifically checking for the base class, because of a problem we ran into
| result.close() | ||
| raise TypeError( | ||
| "_user_batch_completed_callback must be a synchronous callable" | ||
| ) |
There was a problem hiding this comment.
I think this is passed after construction. Currently, it's only used as an internal attribute though I believe
…batcher error handling
…RetryExceptionGroup in legacy batcher Change-Id: Id9737b9695e281f340fce06712362ee296ae71af
… TypeError and batcher fixes Change-Id: Iba6a2a1085355d70b436563c48b66cc7fd9082a7
…itting isinstance check Change-Id: I6cadc96645514c90bed567ce4bddd9e39c1a8cc8
…result in async mode Change-Id: I680fe5cb809dcbd01e293d9318d576e0df49d189
…llback Change-Id: Ic8922566c1e440681c8632e955b2cd50ba1e0eaf
…block Change-Id: I9dc3a3186216d6d5252246d9488b883753e0695b
Change-Id: I2bfee48b1bc534f1b6f088f5af05e4f2ffa446ce
4d27e7b to
ece90f3
Compare
|
Rebased onto No code changes — the diff against the base is identical apart from hunk offsets. New head: |
Summary
_get_statuspacks unpacked protobuf error details:google.api_core.exceptions.from_grpc_errorunpacksgoogle.protobuf.any_pb2.Anymessages fromgrpc-status-details-bininto concretegoogle.rpc.error_details_pb2objects (ErrorInfo,RetryInfo,DebugInfo, etc.) inexc.details. Previously_get_statuspasseddetails=exc.detailsstraight tostatus_pb2.Status(...), raisingTypeError: Parameter to initialize message field must be dict or instance of same class: expected <class 'google.protobuf.any_pb2.Any'> got ...whenever aMutateRowsRPC failed with rich gRPC error details._get_statusnow normalizes each item:any_pb2.Anyis kept as-is, a rawgoogle.protobuf.message.Messageis packed into anAny, and a proto-plus wrapper is packed via its._pb. Anything it cannot pack is skipped rather than failing the whole conversion, andexc.messagedefaults to""since protobuf string fields rejectNone._get_statusrenders exceptions safely in the UNKNOWN fallback:GoogleAPICallError.__str__readsexc.details, which itself raises when the exception was constructed with a malformeddetailsvalue — so the fallback path could raise while building the error it was meant to report. The new_exc_messagehelper falls back to the rawexc.message, then to the type name._get_error_typevalidates status codes:core_exceptions.from_grpc_status(call_code, "")maps0(grpc.StatusCode.OK) and unmapped integers to the base classGoogleAPICallError, which maderetries.if_exception_typematch — and therefore retry — everyGoogleAPICallErrorsubclass._get_error_typenow raisesValueErrorfor booleans and for any code that falls back to baseGoogleAPICallError(including0/grpc.StatusCode.OK).MutationsBatcherAsync._wait_for_batch_resultscatches unexpected task exceptions:await taskmoved inside thetry:block so an unexpected task exception is wrapped inFailedMutationEntryErrorrather than aborting_flush_internalbefore_add_exceptions— which previously causedbatcher.close()to silently drop failed mutations. This is async-only; the generated sync code is unchanged, sinceCrossSyncresolves the branch to the pre-existingtask.result()call.MutationsBatcherunwrapsRetryExceptionGroup: when surfacingFailedMutationEntryError.__cause__, aRetryExceptionGroupcause is replaced by its terminal exception so users see the actual final error instead of the retry-chain wrapper.Test plan
tests/unit/data/test__helpers.pyfor_get_statuspacking unpacked protobuf details,_get_statussurviving malformed details, and_get_error_typerejecting invalid status codes (0,grpc.StatusCode.OK, booleans, and unmapped codes).tests/unit/data/_async/test_mutations_batcher.py(and regeneratedtests/unit/data/_sync_autogen/test_mutations_batcher.py) for the batch-completion callback being invoked withUNKNOWNstatuses on an unexpected error, and for_wait_for_batch_resultswrapping unexpected task exceptions.tests/unitpasses (5509 passed, 8 skipped), includingtest_sync_up_to_date.py.batcher.pyRetryExceptionGroupunwrap is not yet covered by a test intests/unit/v2_client/test_batcher.py.