Skip to content

fix(spanner): release single-use channel hints for mutation-only transactions (P1-2, P1-3)#9

Open
fornwall wants to merge 2 commits into
mainfrom
fix/p1-2-single-use-channel-hint-leak
Open

fix(spanner): release single-use channel hints for mutation-only transactions (P1-2, P1-3)#9
fornwall wants to merge 2 commits into
mainfrom
fix/p1-2-single-use-channel-hint-leak

Conversation

@fornwall

Copy link
Copy Markdown
Owner

Issues

P1-2 — Multiplexed single-use channel-hint slots leak permanently on mutation-only writes and P1-3 — channelUsage BitSet mutated with inconsistent locking, from the Spanner Java client review. Fixed together, as the review suggests, since they touch the same bookkeeping.

writeAtLeastOnceWithOptions / batchWriteAtLeastOnce create single-use MultiplexedSessionTransactions that reserve a channel hint: a bit in the process-wide channelUsage BitSet (shared by all clients of a SpannerImpl) plus the numCurrentSingleUseTransactions counter. The hint was only released in onReadDone(), which mutation-only operations never call — they call onTransactionDone() instead. After numChannels blind writes over the process lifetime, all bits were permanently stuck and the counter permanently ≥ numChannels, so every subsequent single-use transaction got NO_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, racing getSingleUseChannelHint()'s synchronized mutations (P1-3).

Investigation also surfaced two defects adjacent to the review text:

  • A commit that throws also leaked the hint: MultiplexedSessionTransaction.writeAtLeastOnceWithOptions only called onTransactionDone() after a successful super call, so a failed blind write (e.g. FAILED_PRECONDITION) leaked its slot even with the review's suggested fix.
  • The counter drifted independently of the bits: getSingleUseChannelHint() incremented it even when returning NO_CHANNEL_HINT, while delayed transactions (created before the multiplexed session exists) and grpc-gcp-enabled transactions decremented it in onReadDone() 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 call disableGrpcGcpExtension(). (Note: the review's P2-10 entry describes grpc-gcp as "default-off" — that is stale for current main here.)

Fix

  • New idempotent MultiplexedSessionTransaction.releaseSingleUseChannelHint(), called from whichever of onReadDone() / onTransactionDone() fires first (guarded by a flag under the same monitor as the existing done flag). Mutation-only ops release via onTransactionDone(); reads keep releasing via onReadDone().
  • writeAtLeastOnceWithOptions now invokes onTransactionDone() from a finally block, so failed commits release the hint (and are counted in numSessionsReleased).
  • The client-level release clears the BitSet bit under synchronized (channelUsage) — the same monitor getSingleUseChannelHint() uses (P1-3).
  • numCurrentSingleUseTransactions now strictly tracks transactions that hold a hint: transient increments are rolled back before returning NO_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 with disableGrpcGcpExtension() (the default test config never exercises the hint path):

  • testWriteAtLeastOnceReleasesSingleUseChannelHintnumChannels + 1 blind writes leave zero bits set and the counter at 0, and a subsequent single-use read still acquires (and then releases) a hint.
  • testFailedWriteAtLeastOnceReleasesSingleUseChannelHint — a FAILED_PRECONDITION commit releases its slot.
  • testBatchWriteAtLeastOnceReleasesSingleUseChannelHintbatchWriteAtLeastOnce releases its slot.

All three verified to fail against unfixed main and pass with the fix. Full google-cloud-spanner module suite: 9777 tests, 0 failures (JDK 21). fmt:check clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QkZzotf4wMQKXEhEpTLRKL

fornwall and others added 2 commits July 20, 2026 01:26
…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
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