8000
Skip to content

feat(bigtable): route Client.Open()-returned *Table through the Diverter - #20273

Merged
sushanb merged 7 commits into
googleapis:mainfrom
sushanb:feat/bigtable-open-divertible
Jul 30, 2026
Merged

feat(bigtable): route Client.Open()-returned *Table through the Diverter#20273
sushanb merged 7 commits into
googleapis:mainfrom
sushanb:feat/bigtable-open-divertible

Conversation

@sushanb
@sushanb sushanb commented Jul 30, 2026
Copy link
Copy Markdown
Contributor

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

  • `Table.divertible TableAPI` field. Nil for classic-only clients
    (no diverter) → gate short-circuits and the classic fast path runs
    unchanged. Populated by `Open()` when the client has a diverter.
  • `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 (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; without the bypass 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 (three cardinalities in play, only
one shifts).

Files

file change
`bigtable/open.go` `Open()` wires `t.divertible`; new `buildDivertible` helper
`bigtable/table.go` `Table` gains `divertible TableAPI` field; `tableImpl` overrides `Apply` / `ReadRow` to bypass the gate
`bigtable/bigtable.go` `Table.Apply` / `Table.ReadRow` gain a divertible gate; classic bodies extracted into `applyClassic` / `readRowClassic`

Test plan

  • `go build ./...` clean
  • `go vet ./...` clean
  • `goimports -l` clean
  • full `go test -race -count=1 -short -timeout=180s ./bigtable/ ./bigtable/internal/session/ ./bigtable/internal/transport/` green modulo the two pre-existing flakes on `upstream/main` (`TestIntegration_NewClientWithEmulatorHost` — emulator-host resolver; `TestSessionTableCache_TTLSweepEvictsIdle` — passes in isolation)

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)
@sushanb
sushanb requested review from a team as code owners July 30, 2026 21:33
@product-auto-label product-auto-label Bot added the api: bigtable Issues related to the Bigtable API. label Jul 30, 2026
@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 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.

sushanb added 4 commits July 30, 2026 21:37
…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.
sushanb added 2 commits July 30, 2026 23:22
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
sushanb merged commit 2b81c7d into googleapis:main Jul 30, 2026
19 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: bigtable Issues related to the Bigtable API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0