fix(spanner): recover inline-begin read-only transaction after failed first statement#3
Open
fornwall wants to merge 1 commit into
Open
fix(spanner): recover inline-begin read-only transaction after failed first statement#3fornwall wants to merge 1 commit into
fornwall wants to merge 1 commit into
Conversation
Owner
Author
|
Verified end-to-end against the Spanner emulator: https://github.com/fornwall/spanner-java-issues/tree/fix/p0-3-readonly-inline-begin-poisoned The reproducer test |
… first statement The transactionIdFuture of a MultiUseReadOnlyTransaction with BeginTransactionOption.INLINE was created once and never reset. If the statement carrying the inline BeginTransaction failed (or its ResultSet was closed before a transaction id was returned), every subsequent statement on the transaction rethrew the stale error of that first statement forever, and concurrently waiting statements failed with an error belonging to an unrelated statement. Reset transactionIdFuture when it is failed, and retry the selector loop in getTransactionSelector, so the next (or a concurrently waiting) statement attempts a new inline BeginTransaction, matching the recovery behavior of the explicit BeginTransaction path. Signed-off-by: Fredrik Fornwall <fredrik@fornwall.net>
fornwall
force-pushed
the
fix/p0-3-readonly-inline-begin-poisoned
branch
from
July 18, 2026 19:01
d1589dd to
958ac11
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A
MultiUseReadOnlyTransactionusingOptions.BeginTransactionOption.INLINE(added in googleapis#13233) is permanently poisoned when the statement that carries the inlineBeginTransactionfails before a transaction is returned.AbstractReadContext.MultiUseReadOnlyTransaction.transactionIdFutureis created once by the first statement and never reset. If that begin-carrying statement fails (bad SQL, non-retryable RPC error, no transaction returned in the response metadata, or itsResultSetbeing closed before the transaction id arrived),failTransactionIdFuturesets the exception on the future but leaves it in place. Consequences:INVALID_ARGUMENT: bad query).The EXPLICIT begin path (the default) does not have this problem:
initTransaction()simply re-attempts theBeginTransactionRPC on the next statement. The read/write inline-begin path recovers, too, by simulating anABORTEDerror soTransactionRunnerretries the whole transaction — but the read-only path has no runner and therefore no retry mechanism at all.Note: the poisoning can only occur when no transaction id was ever received, i.e. no data was read under any transaction, so re-attempting the begin cannot violate read-timestamp consistency.
Fix
In
AbstractReadContext.MultiUseReadOnlyTransaction:failTransactionIdFuturenow resetstransactionIdFuturetonull(undertxnLock) after setting the exception, so the next statement attempts a fresh inlineBeginTransaction— mirroring the explicit-begin recovery behavior.getTransactionSelectornow retries its selector logic in a loop (bounded by the existing 60s overall deadline) when waiting on the future fails withExecutionException: a concurrently waiting statement takes over the inline begin itself (or waits for another statement that already has) instead of failing with the error of the unrelated first statement. The error still propagates to the failed statement itself. Read-only transactions take no locks, so unlike the read/write path there is no need to abort/retry the entire transaction.Tests
SessionImplTest:multiUseReadOnlyTransactionInlineBeginRetriesBeginAfterFailedFirstStatement(rewritten frommultiUseReadOnlyTransactionInlineBeginFirstQueryErrorPropagates, which asserted the poisoned behavior): first query fails withINVALID_ARGUMENTand propagates; the next statement issues a new request withTransactionSelector.begin, and the statement after that uses the returned transaction id.multiUseReadOnlyTransactionInlineBeginRecoversWhenNoTransactionIsReturned: the begin-carrying statement gets a response without transaction metadata (FAILED_PRECONDITION); the next statement re-attempts the inline begin instead of rethrowing.multiUseReadOnlyTransactionInlineBeginConcurrentStatementTakesOverFailedBegin: statement B is blocked waiting for statement A's inline begin; A fails; B takes over with its ownTransactionSelector.beginrequest and succeeds, while A's caller gets A's actual error.All existing inline-begin tests,
SessionImplTest, and the fullgoogle-cloud-spannerunit test suite pass.