Skip to content

fix(spanner): fail transactionIdFuture when inline-begin R/W statement fails to start (P0-4)#4

Open
fornwall wants to merge 1 commit into
mainfrom
fix/p0-4-rw-rollback-hang-on-start-failed
Open

fix(spanner): fail transactionIdFuture when inline-begin R/W statement fails to start (P0-4)#4
fornwall wants to merge 1 commit into
mainfrom
fix/p0-4-rw-rollback-hang-on-start-failed

Conversation

@fornwall

Copy link
Copy Markdown
Owner

Issue (review P0-4)

A read-write TransactionRunnerImpl.TransactionContextImpl overrides onError/onDone but not onStartFailed (the base AbstractReadContext.onStartFailed is a no-op).

When the begin-carrying first statement of an inline-begin read-write transaction fails to even start its streaming RPC — e.g. the client was closed, the executor rejected the task, or an interceptor threw — AbstractReadContext#startStream reports it through onStartFailed(withBeginTransaction, t), not through onError/onDone:

try {
  call = rpc.executeQuery(/* … */);      // throws synchronously
} catch (RuntimeException | Error t) {
  onStartFailed(withBeginTransaction, t); // no-op for R/W transactions
  throw t;
}

Because onStartFailed is a no-op for the R/W path, the transactionIdFuture that getTransactionSelector() created for the inline begin never completes. The synchronous throw propagates out of the user's TransactionCallable, and runInternal then calls txn.rollback(). rollbackAsync() waits on the never-completing transactionIdFuture, and rollback()'s unbounded get() blocks the application thread forever. commit()'s waiter has the same exposure.

The read-only MultiUseReadOnlyTransaction already handles this exact case by overriding onStartFailed to fail its transactionIdFuture.

Fix

Override onStartFailed in TransactionContextImpl to complete transactionIdFuture exceptionally when withBeginTransaction is set, mirroring the existing onError/onDone handling. Waiters (and rollback()/commit()) are then released. Since no transaction was ever begun on the backend, rollbackAsync() short-circuits to an empty result via its existing catching(...) and issues no Rollback RPC.

Test

Adds TransactionContextImplTest#testRollbackDoesNotHangWhenInlineBeginStatementFailsToStart, which reproduces the production sequence (getTransactionSelector()onStartFailed(true, t)) on an inline-begin context and asserts:

  • transactionIdFuture is completed exceptionally with the start failure, and
  • rollbackAsync() returns promptly (empty result, no Rollback RPC sent).

Without the fix the test fails deterministically at the isDone() assertion (and rollback() would otherwise hang). Verified failing on unmodified code and passing with the fix.

TransactionContextImplTest, TransactionRunnerImplTest, and InlineBeginTransactionTest (109 tests) all pass; both changed files pass google-java-format.

🤖 Generated with Claude Code

…ils to start

A read-write TransactionContextImpl overrides onError/onDone but not
onStartFailed. When the begin-carrying first statement of an inline-begin
read-write transaction fails to even start its RPC (client closed, executor
rejected the task, interceptor threw), AbstractReadContext#startStream calls
onStartFailed(withBeginTransaction, t) — a no-op in the base class — rather than
onError/onDone. The transactionIdFuture that getTransactionSelector() created
for the inline begin therefore never completes, so a subsequent rollback()
(and commit()) blocks forever on its unbounded get() of that future, hanging
the application thread. The read-only MultiUseReadOnlyTransaction already
handles this via onStartFailed.

Override onStartFailed in TransactionContextImpl to complete the future
exceptionally, mirroring the existing onError/onDone handling, so waiters and
rollback/commit are released. Adds a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TS3yFcC6F1aDapSL26q7yb
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.

1 participant