fix(spanner): release single-use channel hints for mutation-only transactions (P1-2, P1-3)#9
Open
fornwall wants to merge 2 commits into
Open
fix(spanner): release single-use channel hints for mutation-only transactions (P1-2, P1-3)#9fornwall wants to merge 2 commits into
fornwall wants to merge 2 commits into
Conversation
…sactions Single-use transactions created for writeAtLeastOnceWithOptions and batchWriteAtLeastOnce reserved a channel hint (a bit in the process-wide channelUsage BitSet plus the numCurrentSingleUseTransactions counter), but the hint was only ever released in onReadDone(), which mutation-only operations never call. After numChannels blind writes over the process lifetime, every bit was permanently stuck and every subsequent single-use transaction got NO_CHANNEL_HINT, silently disabling the channel-spreading optimization. A commit that threw also leaked the hint, as onTransactionDone() was only invoked on success. Release the hint (idempotently) from both onReadDone() and onTransactionDone(), and invoke onTransactionDone() from a finally block in writeAtLeastOnceWithOptions. Also fix two related bookkeeping defects: - onReadDone() cleared the shared, non-thread-safe BitSet without the monitor that getSingleUseChannelHint() uses; the release now takes the same monitor. - The counter was incremented even when no hint was reserved (and decremented by transactions that never incremented it, e.g. delayed transactions), letting it drift. The counter now strictly tracks transactions that hold a hint. Note that channel hints are only used when the gRPC-GCP extension is disabled; the extension is enabled by default in this repository, so this only affects clients that call disableGrpcGcpExtension(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QkZzotf4wMQKXEhEpTLRKL
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QkZzotf4wMQKXEhEpTLRKL
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.
Issues
P1-2 — Multiplexed single-use channel-hint slots leak permanently on mutation-only writes and P1-3 —
channelUsageBitSet mutated with inconsistent locking, from the Spanner Java client review. Fixed together, as the review suggests, since they touch the same bookkeeping.writeAtLeastOnceWithOptions/batchWriteAtLeastOncecreate single-useMultiplexedSessionTransactions that reserve a channel hint: a bit in the process-widechannelUsageBitSet (shared by all clients of aSpannerImpl) plus thenumCurrentSingleUseTransactionscounter. The hint was only released inonReadDone(), which mutation-only operations never call — they callonTransactionDone()instead. AfternumChannelsblind writes over the process lifetime, all bits were permanently stuck and the counter permanently ≥numChannels, so every subsequent single-use transaction gotNO_CHANNEL_HINT— the channel-spreading optimization was dead after warm-up in any workload doing blind writes. Separately,onReadDone()cleared the shared, non-thread-safe BitSet with no lock, racinggetSingleUseChannelHint()'ssynchronizedmutations (P1-3).Investigation also surfaced two defects adjacent to the review text:
MultiplexedSessionTransaction.writeAtLeastOnceWithOptionsonly calledonTransactionDone()after a successfulsupercall, so a failed blind write (e.g.FAILED_PRECONDITION) leaked its slot even with the review's suggested fix.getSingleUseChannelHint()incremented it even when returningNO_CHANNEL_HINT, while delayed transactions (created before the multiplexed session exists) and grpc-gcp-enabled transactions decremented it inonReadDone()without ever having incremented — so it could drift both up and down.Severity caveat: channel hints are only used when the gRPC-GCP extension is disabled, and this repository enables the extension by default (
SpannerOptions.Builder.grpcGcpExtensionEnabled = true). The leak therefore only affects clients that calldisableGrpcGcpExtension(). (Note: the review's P2-10 entry describes grpc-gcp as "default-off" — that is stale for currentmainhere.)Fix
MultiplexedSessionTransaction.releaseSingleUseChannelHint(), called from whichever ofonReadDone()/onTransactionDone()fires first (guarded by a flag under the same monitor as the existingdoneflag). Mutation-only ops release viaonTransactionDone(); reads keep releasing viaonReadDone().writeAtLeastOnceWithOptionsnow invokesonTransactionDone()from afinallyblock, so failed commits release the hint (and are counted innumSessionsReleased).synchronized (channelUsage)— the same monitorgetSingleUseChannelHint()uses (P1-3).numCurrentSingleUseTransactionsnow strictly tracks transactions that hold a hint: transient increments are rolled back before returningNO_CHANNEL_HINT, and release only decrements when a hint was actually held. This makes the acquire/release pair self-consistent for all paths (direct, delayed, grpc-gcp).Tests
Three new mock-server tests in
MultiplexedSessionDatabaseClientMockServerTest, using a Spanner instance withdisableGrpcGcpExtension()(the default test config never exercises the hint path):testWriteAtLeastOnceReleasesSingleUseChannelHint—numChannels + 1blind writes leave zero bits set and the counter at 0, and a subsequent single-use read still acquires (and then releases) a hint.testFailedWriteAtLeastOnceReleasesSingleUseChannelHint— aFAILED_PRECONDITIONcommit releases its slot.testBatchWriteAtLeastOnceReleasesSingleUseChannelHint—batchWriteAtLeastOncereleases its slot.All three verified to fail against unfixed
mainand pass with the fix. Fullgoogle-cloud-spannermodule suite: 9777 tests, 0 failures (JDK 21).fmt:checkclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01QkZzotf4wMQKXEhEpTLRKL