Repository navigation
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements cancellation support for the asynchronous bulk mutation applier (AsyncBulkApplier) in the Bigtable client. It introduces thread-safe state management using a mutex to handle cancellation during rate-limiting, active RPCs, and backoff periods. Unit tests have been significantly expanded to cover these cancellation paths. The feedback suggests a minor optimization to move the future result in OnFinish to avoid an unnecessary copy of a StatusOr object.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #16648 +/- ##
==========================================
- Coverage 92.53% 92.50% -0.03%
==========================================
Files 2269 2269
Lines 222896 223365 +469
==========================================
+ Hits 206250 206622 +372
- Misses 16646 16743 +97 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
scotthart
left a comment
There was a problem hiding this comment.
Review Findings
1. [HIGH] Unconfirmed non-idempotent mutations still fail with kInternal (and OnDone can receive OK) when cancelled before OnFinish with an OK stream status
- File:
google/cloud/bigtable/internal/async_bulk_apply.cc(lines 125–144)
operation_context_->PostCall(*client_context, status);
state_.OnFinish(status);
if (!state_.HasPendingMutations()) {
operation_context_->OnDone(status);
SetPromise();
return;
}
if (cancelled) {
Status cancel_status = MakeCancelledStatus();
state_.OnCancel(cancel_status);
operation_context_->OnDone(status.ok() ? cancel_status : status);
SetPromise();
return;
}Problem:
When Cancel() is called while a MutateRows stream is in flight, keep_reading_ is set to false, causing PerformAsyncStreamingRead (AsyncStreamingReadImpl) to stop invoking OnRead and Discard() any remaining buffered responses before calling Finish(). If the server had already finished sending responses and closed the stream with grpc::Status::OK before cancellation took effect on the wire (or in the scenario tested by CancelAfterTransientEntryResponseBeforeOnFinish), OnFinish is invoked with status.ok() == true and cancelled == true while unconfirmed mutations still have annotation.has_mutation_result == false.
Because state_.OnFinish(status) is called with status (OK) before cancelled is handled:
- In
BulkMutatorState::OnFinish, only idempotent unconfirmed mutations are moved topending_mutations_. Any unconfirmed non-idempotent mutation (annotation.idempotency == Idempotency::kNonIdempotent) immediately takes theelsebranch inBulkMutatorState::OnFinish, seeslast_status_.ok() == true, and is pushed tofailures_withStatusCode::kInternal("The server never sent a confirmation for this mutation but the stream didn't fail either. This is most likely a bug..."). Callingstate_.OnCancel(cancel_status)afterward does not fix entries already added tofailures_. - If all remaining unconfirmed mutations were non-idempotent (e.g., mutation 0 was confirmed
OKin an earlierOnReadchunk and mutation 1 was non-idempotent and discarded afterCancel()),state_.HasPendingMutations()isfalseafterstate_.OnFinish(status).OnFinishthen returns early atif (!state_.HasPendingMutations()), passingStatus{}(OK) tooperation_context_->OnDone(status)instead ofkCancelled.
Passing effective_status = (cancelled && status.ok()) ? MakeCancelledStatus() : status into state_.OnFinish(...) (while keeping the raw status for operation_context_->PostCall) fixes both issues:
- Mutations that already received a response in
OnReadhaveannotation.has_mutation_result == trueand are skipped byBulkMutatorState::OnFinish. - Unconfirmed non-idempotent mutations record
kCancelledinfailures_instead of thekInternalserver-bug error. - Unconfirmed idempotent mutations are moved to
pending_mutations_withlast_status_ = kCancelled, whichOnRetryDone()reports askCancelled.
Suggested Fix (google/cloud/bigtable/internal/async_bulk_apply.cc lines 125–144):
operation_context_->PostCall(*client_context, status);
Status const effective_status =
cancelled && status.ok() ? MakeCancelledStatus() : status;
state_.OnFinish(effective_status);
if (!state_.HasPendingMutations() || cancelled) {
operation_context_->OnDone(effective_status);
SetPromise();
return;
}Suggested Test Update (google/cloud/bigtable/internal/async_bulk_apply_test.cc lines 688–693):
Adding a NonIdempotentMutation to CancelAfterTransientEntryResponseBeforeOnFinish (or a dedicated test where only a NonIdempotentMutation remains unconfirmed when Finish returns Status{}) will exercise this path:
TEST_F(AsyncBulkApplyTest, CancelAfterTransientEntryResponseBeforeOnFinish) {
std::vector<bigtable::FailedMutation> expected = {
{TransientError(), 1},
{Status(StatusCode::kCancelled, "cancelled"), 2},
{Status(StatusCode::kCancelled, "cancelled"), 3}};
bigtable::BulkMutation mut(IdempotentMutation("r0"), IdempotentMutation("r1"),
IdempotentMutation("r2"),
NonIdempotentMutation("r3"));Propagate future::cancel() across limiter acquisition, in-flight MutateRows client contexts, OnFinish retry transitions, and retry backoff timers so cancelled AsyncBulkApply calls never dispatch new MutateRows RPCs. Ensure BulkMutatorState records kCancelled for unconfirmed entries when cancelled without a prior stream error, and invoke OperationContext::OnDone consistently whenever PreCall has executed (including on backoff timer errors) without invoking OnDone before the first attempt starts.
6f651ff to
74e2bd0
Compare
|
Thanks for catching that! Fixed in
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces cancellation support to the AsyncBulkApplier class in the Cloud Bigtable C++ client. It adds thread-safe state management using a mutex to track cancellation, active operations, and pending futures. The BulkMutatorState class is updated with an OnCancel method to record cancellation status, and comprehensive unit tests are added to verify cancellation behavior during different stages of the bulk apply operation (such as during limiter acquisition, backoff, and in-flight RPCs). I have no feedback to provide as there are no review comments.
Summary
future::cancel()on the future returned byAsyncBulkApplier::Createacross rate-limiter acquisition (limiter_->AsyncAcquire()), in-flightMutateRowsRPCs (grpc::ClientContext::TryCancel()),OnFinishretry transitions, and retry backoff timers so cancelledAsyncBulkApplycalls abort promptly and never dispatch newMutateRowsRPCs.StatusCode::kCancelledinBulkMutatorState::OnCancelfor unconfirmed mutations when cancelled without an existing stream error instead of reporting an internal server-bug error.OperationContext::OnDoneis invoked consistently wheneverPreCallhas executed (including when the retry backoff timer fails or is cancelled), and never invoked if cancelled before the first RPC attempt starts.Testing
google/cloud/bigtable/internal/async_bulk_apply_test.ccandgoogle/cloud/bigtable/internal/bulk_mutator_test.cccovering cancellation before first attempt, mid-RPC before first read, beforeOnFinish, during backoff, racing with timer expiry, during retry limiter acquisition, and backoff timer errors.bazel test //google/cloud/bigtable:internal_async_bulk_apply_test //google/cloud/bigtable:internal_bulk_mutator_test.