Skip to content

fix(bigtable): pack unpacked error details in _get_status and harden batcher error handling - #18770

Merged
mutianf merged 9 commits into
googleapis:mainfrom
mutianf:fix-get-status-unpacked-details
Oct 27, 2026
Merged

mutianf merged 9 commits into
googleapis:mainfrom
mutianf:fix-get-status-unpacked-details

Conversation

@mutianf

@mutianf mutianf commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • _get_status packs unpacked protobuf error details: google.api_core.exceptions.from_grpc_error unpacks google.protobuf.any_pb2.Any messages from grpc-status-details-bin into concrete google.rpc.error_details_pb2 objects (ErrorInfo, RetryInfo, DebugInfo, etc.) in exc.details. Previously _get_status passed details=exc.details straight to status_pb2.Status(...), raising TypeError: Parameter to initialize message field must be dict or instance of same class: expected <class 'google.protobuf.any_pb2.Any'> got ... whenever a MutateRows RPC failed with rich gRPC error details. _get_status now normalizes each item: any_pb2.Any is kept as-is, a raw google.protobuf.message.Message is packed into an Any, and a proto-plus wrapper is packed via its ._pb. Anything it cannot pack is skipped rather than failing the whole conversion, and exc.message defaults to "" since protobuf string fields reject None.
  • _get_status renders exceptions safely in the UNKNOWN fallback: GoogleAPICallError.__str__ reads exc.details, which itself raises when the exception was constructed with a malformed details value — so the fallback path could raise while building the error it was meant to report. The new _exc_message helper falls back to the raw exc.message, then to the type name.
  • _get_error_type validates status codes: core_exceptions.from_grpc_status(call_code, "") maps 0 (grpc.StatusCode.OK) and unmapped integers to the base class GoogleAPICallError, which made retries.if_exception_type match — and therefore retry — every GoogleAPICallError subclass. _get_error_type now raises ValueError for booleans and for any code that falls back to base GoogleAPICallError (including 0 / grpc.StatusCode.OK).
  • MutationsBatcherAsync._wait_for_batch_results catches unexpected task exceptions: await task moved inside the try: block so an unexpected task exception is wrapped in FailedMutationEntryError rather than aborting _flush_internal before _add_exceptions — which previously caused batcher.close() to silently drop failed mutations. This is async-only; the generated sync code is unchanged, since CrossSync resolves the branch to the pre-existing task.result() call.
  • Legacy MutationsBatcher unwraps RetryExceptionGroup: when surfacing FailedMutationEntryError.__cause__, a RetryExceptionGroup cause is replaced by its terminal exception so users see the actual final error instead of the retry-chain wrapper.

Test plan

  • Added unit tests in tests/unit/data/test__helpers.py for _get_status packing unpacked protobuf details, _get_status surviving malformed details, and _get_error_type rejecting invalid status codes (0, grpc.StatusCode.OK, booleans, and unmapped codes).
  • Added unit tests in tests/unit/data/_async/test_mutations_batcher.py (and regenerated tests/unit/data/_sync_autogen/test_mutations_batcher.py) for the batch-completion callback being invoked with UNKNOWN statuses on an unexpected error, and for _wait_for_batch_results wrapping unexpected task exceptions.
  • tests/unit passes (5509 passed, 8 skipped), including test_sync_up_to_date.py.
  • The legacy batcher.py RetryExceptionGroup unwrap is not yet covered by a test in tests/unit/v2_client/test_batcher.py.

@mutianf
mutianf requested a review from a team as a code owner October 19, 2026 18:26

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@mutianf

mutianf commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/google-cloud-bigtable/google/cloud/bigtable/data/_helpers.py Outdated
result.close()
raise TypeError(
"_user_batch_completed_callback must be a synchronous callable"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, reverted this change. Maybe we should do this check and construction time?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is passed after construction. Currently, it's only used as an internal attribute though I believe

@daniel-sanche daniel-sanche left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is passed after construction. Currently, it's only used as an internal attribute though I believe

…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
@mutianf
mutianf force-pushed the fix-get-status-unpacked-details branch from 4d27e7b to ece90f3 Compare October 27, 2026 13:18
@mutianf

mutianf commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto upstream/main (now d54a6e0) to clear the failing GitHub Actions Scan check. The zizmor known-vulnerable-actions audit was flagging dorny/paths-filter@v4.0.1 in the workflows at this PR's old base; main has since bumped it to v4.0.3.

No code changes — the diff against the base is identical apart from hunk offsets. New head: ece90f38263.

@mutianf
mutianf merged commit 525e740 into googleapis:main Oct 27, 2026
57 checks passed
@mutianf
mutianf deleted the fix-get-status-unpacked-details branch October 27, 2026 14:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants