feat(bigtable): route Client.Open()-returned *Table through the Diverter - #20273
Merged
Conversation
Backward-compatible session-routing wiring on the bare *Table surface.
When c.diverter is set, Apply and ReadRow on a *Table returned by
Client.Open() now route through an internal TableShim so calls can be
diverted to the session data path under the diverter's SessionLoad
ratio. Return type stays *Table; every existing method keeps its
signature and behavior.
Motivation: callers that use Open() (BulkMutation, existing app code
that holds *Table across many ops) previously never saw session
routing — only OpenTable did. This closes the gap so the same
*Table works for both paths.
Design:
* Table gains a divertible TableAPI field. Nil for classic-only
clients (no diverter) → gate short-circuits and the classic
fast path runs unchanged.
* Table.Apply and Table.ReadRow gain a one-line gate at the top:
if t.divertible != nil, dispatch there; else fall through to
the new applyClassic / readRowClassic helpers (the pre-existing
bodies, extracted verbatim).
* tableImpl.Apply and tableImpl.ReadRow bypass the gate — they
call applyClassic / readRowClassic directly. Necessary because
tableImpl is what NewTableShim wraps as its classic side; if
it went back through the gate, the shim would recurse into
itself.
* Open() calls c.buildDivertible(t, ...) which returns a
*TableShim wrapping a snapshot of t (with divertible nil-ed).
buildDivertible returns nil when c.diverter is nil, so the
zero-cost path stays intact.
Cardinality: the sessionTables cache map grows one entry per unique
Open() call now (previously only per OpenTable). Cost per entry is
~110 B — a fully-qualified table name string + a *SessionTable. No
new sessions or streams open — sessions still materialize lazily on
first RPC per lazyPool.get(). See buildDivertible doc for the full
cardinality analysis.
Verified:
* go build ./... clean
* go vet ./... clean
* goimports -l clean
* full test sweep green (modulo the two pre-existing failures on
upstream/main: TestIntegration_NewClientWithEmulatorHost and
TestSessionTableCache_TTLSweepEvictsIdle, both unrelated to
this change)
Contributor
There was a problem hiding this comment.
Code Review
This pull request updates the bigtable package to support transparent session routing for callers holding a bare *Table. It introduces a divertible field to the Table struct, which is populated during Open when a diverter is configured. The ReadRow and Apply methods on Table are updated to route calls through this divertible shim when present, falling back to classic implementations otherwise. To prevent infinite recursion, tableImpl overrides ReadRow and Apply to bypass the divertible gate and call the classic methods directly. I have no feedback to provide.
…invariants Six deterministic tests, all -race clean, that guard the wiring added in the previous commit (8502175): * TestOpen_WiresDivertibleWhenDiverterPresent — bare *Table returned by Open now carries a *TableShim on t.divertible when c.diverter is set. Field asserts also cover the old 'bare' contract for project/instance/table/authorizedView/MV. * TestOpen_NoDivertibleWhenDiverterAbsent — pins the zero-cost classic path: no diverter → divertible stays nil → the gate in Apply/ReadRow short-circuits without allocating a shim. * TestOpen_DivertibleShimBreaksRecursionLoop — pins the value- copy + nil-field anti-recursion invariant on the shim's classic-side *tableImpl. * TestOpen_ApplyRoutesThroughDivertible — end-to-end proof: with SessionLoad=1.0 and a fakeSessionClient, tbl.Apply on a bare *Table from Open reaches the fake's OpenTable stub via the shim's session dispatch (would panic on the classic path since there's no gRPC conn). * TestOpen_ReadRowRoutesThroughDivertible — mirror of the Apply test for the read side. * TestTableImpl_BypassesDivertibleGate — pins the second half of the anti-recursion safety net: tableImpl.Apply and .ReadRow call the *Classic helpers directly, never re-entering the Table.Apply/ReadRow gate. Uses a spyPanic sentinel so a regression that dispatches to divertible is distinguishable from an unrelated classic-body panic on missing gRPC conn. Also renamed the pre-existing TestOpen_ReturnsBareTable → TestOpen_WiresDivertibleWhenDiverterPresent to reflect the new contract; the old name misled once Open started wiring divertible.
Shrink the ~30-line cardinality discussion to 5 lines that keep the
load-bearing facts:
* cost per unique Open (with diverter+sessionImpl wired) = one
sessionTables entry
* no I/O — pools materialize lazily on first RPC
* SessionPoolImpl count and server-side OpenSession stream count
unchanged
Drops the ~110 B estimate (was understated — real cost is
~250-400 B once sessionTableHandle + sessionTable + lazyPool
allocations and the fully-qualified name string are counted; the
qualitative claim 'one bounded entry' is what matters). Also drops
the 'if this matters, hoist to lazy' paragraph — that's a design
speculation, not a load-bearing invariant.
…pen call
Prior wording ('one cache entry per unique Open call') read as if
each Open() invocation allocates. Actually the sessionTableCache
dedups on fully-qualified table name — repeat Open("t") hits the
cache and returns the same handle. Reword to 'one entry per unique
fully-qualified table name' with an explicit 'repeated Open of the
same table hits the cache' clause.
The cost paragraph kept needing corrections (~110 B was wrong, 'per Open call' was ambiguous vs. per-unique-table). The essentials that survive are the anti-recursion invariant and the nil-diverter short-circuit — both already in the remaining doc. Drop the cost prose entirely; the sessionTableCache doc covers the cache semantics if a reader wants that detail.
mutianf
approved these changes
Jul 30, 2026
Adds TestOpen_ClassicOnlyMethodsSkipDivertibleGate with 4 subtests (ReadRows, ApplyBulk, SampleRowKeys, ApplyReadModifyWrite). Each builds a *Table with divertible set to a panickingTableAPI spy; if the method dispatches through divertible, the spy panics with a distinctive sentinel and the test fails. Reaching the classic body (which panics on the nil gRPC conn) is the expected outcome and gets recover'd as such. These four methods have no session equivalent — TableShim itself delegates them straight to classic (see TableShim.ReadRows / ApplyBulk / SampleRowKeys / ApplyReadModifyWrite). Gating them at Table.* would just add pure indirection; the invariant is 'they never touch divertible'. Regression guard for a plausible future mistake: someone porting another RPC into the divertible surface (e.g., if session ever grows a bulk-mutate op) adds a gate uniformly to all *Table methods and accidentally routes a classic-only op through the shim.
…xture Previous test used newBareClientForOpenTests(t, 0.0) — SessionLoad=0 reads as 'nothing would divert anyway,' making the test look weaker than it is. Move to newSessionWiredClient + SessionLoad=1.0 so the scenario is 'session backend fully enabled, would-divert-if-gated' — which is the natural configuration where a stray gate on ApplyBulk / ReadRows / SampleRowKeys / ApplyReadModifyWrite would actually route to session and cause harm. Test mechanics unchanged: divertible is overwritten with the panickingTableAPI spy, so any dispatch through the gate panics with spyPanic and the subtest fails. Nil-conn panic from the classic body is expected and gets recover'd.
sushanb
pushed a commit
that referenced
this pull request
Aug 3, 2026
🤖 I have created a release *beep* *boop* --- ## [1.52.0](bigtable/v1.51.0...bigtable/v1.52.0) (2026-08-03) ### Features * **bigtable:** Add AFE picker (Simple / LeastInFlight / LeastLatency) ([#20204](#20204)) ([bcbf714](bcbf714)) * **bigtable:** Add ClientConfig.DisableSession to opt out of session backend ([#20297](#20297)) ([7ee5e44](7ee5e44)) * **bigtable:** Add getClientConfigDirectAccessChecker for session pools ([#20209](#20209)) ([3b8d30a](3b8d30a)) * **bigtable:** Add NoOpChannelPrimer for session channel pools ([#20208](#20208)) ([d055a8a](d055a8a)) * **bigtable:** Add per-AFE sessionList for the two-tier session pool ([#20224](#20224)) ([dbf0c3f](dbf0c3f)) * **bigtable:** Add protoRowToRow conversion helper for TableShim ([#20257](#20257)) ([1297143](1297143)) * **bigtable:** Add Session debug surface (observability fields + methods) ([#20211](#20211)) ([d8d3e16](d8d3e16)) * **bigtable:** Add Session lifecycle (Start, Close, ForceClose, readLoop, heartBeatLoop) ([#20215](#20215)) ([b9e53c6](b9e53c6)) * **bigtable:** Add Session struct + state machine ([#20117](#20117)) ([09acbb3](09acbb3)) * **bigtable:** Add session.Config.EnableDebug to gate sessionz debug state ([#20247](#20247)) ([ce74c31](ce74c31)) * **bigtable:** Add SessionClient + SessionTable + lazyPool ([#20228](#20228)) ([ab2c96c](ab2c96c)) * **bigtable:** Add SessionPoolImpl (two-tier pool + scaling + debug) ([#20225](#20225)) ([683eda8](683eda8)) * **bigtable:** Rename session pool display to <resource-id>-<PERM> ([#20248](#20248)) ([35e146e](35e146e)) * **bigtable:** Route Client.Open()-returned *Table through the Diverter ([#20273](#20273)) ([2b81c7d](2b81c7d)) * **bigtable:** State-based classification for abnormal session close ([#20243](#20243)) ([f2905b7](f2905b7)) * **bigtable:** TableShim fallback to classic on session UNIMPLEMENTED ([#20269](#20269)) ([36540af](36540af)) * **bigtable:** TTL-on-idle cache for per-resource session.TableAPI ([#20263](#20263)) ([00b2a49](00b2a49)) * **bigtable:** Wire Diverter on Client and route Open* via TableShim ([#20256](#20256)) ([b32fbd7](b32fbd7)) ### Bug Fixes * **bigtable:** AFE picker latency signal — subtract poolWait and compute TransportLatency = wire − backend at source ([#20281](#20281)) ([bb8c4d5](bb8c4d5)) * **bigtable:** Guard NewStream OnFinish against grpc-go double-fire ([#20295](#20295)) ([b51da29](b51da29)) * **bigtable:** Real per-resource pool teardown on sessionTable.Close + cache close-race gate ([#20264](#20264)) ([599aea9](599aea9)) * **bigtable:** Session.durations / session.uptime — set explicit histogram bucket boundaries ([#20276](#20276) A567 ) ([97eee22](97eee22)) * **bigtable:** SessionTableHandle self-heals across cache eviction ([#20296](#20296)) ([0dd98cd](0dd98cd)) * **bigtable:** Translate ctx errors to gRPC status on session vRPC ([#20299](#20299)) ([0f3b2a5](0f3b2a5)) * **bigtable:** Treat PingAndWarm NotFound as a successful prime ([#20219](#20219)) ([a1557ad](a1557ad)) ### Performance Improvements * **bigtable:** Delete periodic Tick loop; sizing is event-driven ([#20285](#20285)) ([2c096bd](2c096bd)) * **bigtable:** Drop pick_lost_race debug tag from CheckoutSession hot path ([#20280](#20280)) ([bd0e400](bd0e400)) --- 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>
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.
Summary
Backward-compatible session-routing wiring on the bare `*Table`
surface. When `c.diverter` is set, `Apply` and `ReadRow` on a
`*Table` returned by `Client.Open()` now route through an internal
`TableShim` so calls can be diverted to the session data path under
the diverter's SessionLoad ratio. Return type stays `*Table`; every
existing method keeps its signature and behavior.
Why
Callers that use `Open()` (BulkMutation, existing app code that holds
`*Table` across many ops) previously never saw session routing —
only `OpenTable` did. This closes the gap so the same `*Table` works
for both paths.
Design
(no diverter) → gate short-circuits and the classic fast path runs
unchanged. Populated by `Open()` when the client has a diverter.
top: if `t.divertible != nil`, dispatch there; else fall through
to the new `applyClassic` / `readRowClassic` helpers (pre-existing
bodies extracted verbatim).
they call `applyClassic` / `readRowClassic` directly. Necessary
because `tableImpl` is what `NewTableShim` wraps as its classic
side; without the bypass the shim would recurse into itself.
`*TableShim` wrapping a snapshot of `t` (with `divertible`
nil-ed). `buildDivertible` returns nil when `c.diverter` is nil,
so the zero-cost path stays intact.
Cardinality
The `sessionTables` cache map grows one entry per unique `Open()`
call now (previously only per `OpenTable`). Cost per entry is
~110 B — a fully-qualified table name string + a `*SessionTable`.
No new sessions or streams open — sessions still materialize
lazily on first RPC per `lazyPool.get()`. See `buildDivertible` doc
for the full cardinality analysis (three cardinalities in play, only
one shifts).
Files
Test plan