Skip to content

fix(spanner): keep DCP transaction on its channel while it drains - #20618

Merged
gcf-merge-on-green[bot] merged 3 commits into
mainfrom
fm/spanner-dcp-drain-sticky
Oct 1, 2026
Merged

gcf-merge-on-green[bot] merged 3 commits into
mainfrom
fm/spanner-dcp-drain-sticky

Conversation

@rahul2393

Copy link
Copy Markdown
Contributor

With the dynamic channel pool, a session handle re-resolved its bound channel by id among active entries on every RPC, so a read-write transaction moved to another channel as soon as scale-down marked its channel draining. Keep a pointer to the bound entry instead and use it while it is active or draining; re-pick only once it is closed.

Add TestDCPReadWriteTransactionChannelAffinityAcrossDrain. A read-write
transaction bound to a DCP channel should keep using that channel when it
is drained mid-transaction, and move to another active channel only after
the channel is closed. The draining case fails on current code because
dcpResolvingSpannerClient.resolve re-picks as soon as the entry is no
longer active.
@rahul2393
rahul2393 requested review from a team as code owners October 1, 2026 11:52
@product-auto-label product-auto-label Bot added the api: spanner Issues related to the Spanner API. label Oct 1, 2026
@rahul2393
rahul2393 requested a review from olavloite October 1, 2026 11:54

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request modifies the dynamic channel pool to maintain channel affinity for read-write transactions during scale-down, ensuring operations stay on the same channel until it is closed. It replaces entry ID tracking with an atomic pointer to the dcpEntry and introduces a safe idle-closing mechanism. The review feedback identifies a critical race condition between claimClose and a permanent close() that could lead to resource leaks during pool shutdown. The reviewer suggests introducing a new dcpStateClosedPermanently state and provides code suggestions to ensure permanent closes are respected.

Comment thread spanner/dynamic_channel_pool.go Resolved
Comment thread spanner/dynamic_channel_pool.go Resolved
Comment thread spanner/dynamic_channel_pool.go Resolved
With the dynamic channel pool, a session handle re-resolved its bound
channel by id among active entries on every RPC, so a read-write
transaction moved to another channel as soon as scale-down marked its
channel draining. Keep a pointer to the bound entry instead and use it
while it is active or draining; re-pick only once it is closed.

Each entry now has a reference count that is negative once the entry is
closed. The session handle resolver takes a reference before each RPC
and releases it when the RPC returns, and a stream holds its own
reference until it finishes. Scale-down drain closes an idle entry only
by swapping zero references for closed, so an RPC that acquired the
entry keeps its channel open and a later one fails to acquire and
re-picks. Pool shutdown closes regardless of references and always
wins. Concurrent RPCs of one handle agree on a replacement entry via
compare-and-swap.
@rahul2393
rahul2393 force-pushed the fm/spanner-dcp-drain-sticky branch from fa5976a to e32b6ff Compare October 1, 2026 14:15
@rahul2393

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a reference-counting mechanism to the Spanner dynamic channel pool (dcpEntry) to prevent draining channels from being closed while active RPCs or streams are still using them. It refactors dcpResolvingSpannerClient to track the dcpEntry pointer directly and acquire/release references atomically during RPC execution, ensuring that read-write transactions maintain channel affinity through scale-down events. Extensive unit tests have been added to verify these concurrency behaviors. There are no review comments to address, and the implementation looks solid.

// closeIfIdle closes a draining entry that has been idle for grace, unless an
// RPC holds a reference to it. It reports whether the entry is closed.
func (e *dcpEntry) closeIfIdle(grace time.Duration) bool {
return e.idleFor(grace) && e.closeIfUnreferenced()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand the flow correctly here, then:

  1. The reference counter for a channel is only increased as long as there is an active RPC on it.
  2. That means that a draining channel that is in use by a read/write transaction is only prevented from being closed by the grace period when the read/write transaction is not actively executing any RPCs.
  3. The default grace period is 1 minute, so more than enough for an inactive read/write transaction, as it will be aborted by Spanner after 10s.
  4. However, the grace period is configurable, meaning that an application could set the grace period to 1s, and thereby (unintentionally) cause a problem for itself.

Should we therefore add a fixed duration of 15s here to ensure that a channel is only closed after 15s + the grace period (and maybe make the fixed 15s period an unexported configuration option, so we can set it to 0 in tests)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated channel close logic to wait for minimum 15 seconds irrespective of custom grace time.

@rahul2393 rahul2393 added automerge Merge the pull request once unit tests and other checks pass. kokoro:force-run Add this label to force Kokoro to re-run the tests. labels Oct 1, 2026
@kokoro-team kokoro-team removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Oct 1, 2026
@gcf-merge-on-green
gcf-merge-on-green Bot merged commit 93dfc20 into main Oct 1, 2026
17 checks passed
@gcf-merge-on-green
gcf-merge-on-green Bot deleted the fm/spanner-dcp-drain-sticky branch October 1, 2026 15:48
rahul2393 pushed a commit that referenced this pull request Oct 7, 2026
🤖 I have created a release *beep* *boop*
---


##
[1.96.0](spanner/v1.95.1...spanner/v1.96.0)
(2026-10-07)


### Features

* **all:** Update supported go versions
([#20520](#20520))
([90413d3](90413d3))
* **spanner:** Add experimental vtprotobuf decoding with
ExperimentalBorrowRows under the spanner_vtproto build tag
([#20642](#20642))
([fdbd4f4](fdbd4f4))


### Bug Fixes

* **spanner:** Keep DCP transaction on its channel while it drains
([#20618](#20618))
([93dfc20](93dfc20))
* **spanner:** Prioritize leader replica for read-write transactions in
location-aware routing
([#20620](#20620))
([a524d66](a524d66))
* **spanner:** Record streaming built-in metrics per result stream
([#20596](#20596))
([be75f7e](be75f7e))
* **spanner:** Retry and resume BatchWrite streams on UNAVAILABLE
([#20623](#20623))
([a302277](a302277))


### Performance Improvements

* **spanner:** Skip builtin metrics tracer allocation when metrics are
disabled
([#20582](#20582))
([9971a0c](9971a0c))
* **spanner:** Skip reading response headers when no latency sink is
enabled
([#20583](#20583))
([3dcc2c3](3dcc2c3))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: spanner Issues related to the Spanner API. automerge Merge the pull request once unit tests and other checks pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants