Skip to content

feat(bigtable): add per-AFE sessionList for the two-tier session pool - #20224

Merged
sushanb merged 2 commits into
googleapis:mainfrom
sushanb:bigtable-session-list
Jul 27, 2026
Merged

feat(bigtable): add per-AFE sessionList for the two-tier session pool#20224
sushanb merged 2 commits into
googleapis:mainfrom
sushanb:bigtable-session-list

Conversation

@sushanb

@sushanb sushanb commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

First of five PRs porting the session-pool infrastructure from feat/bigtable-sessionz-debug (which powers the recycle-repro fleet) upstream. Stacks on #20215 (Session lifecycle, now merged).

What lands

New file: session_list.go (~600 LOC) — the per-pool sessionList data structure that groups sessions by the AFE (Application Front End) their handshake landed on. Consumed by the two-tier picker (K-choice-over-AFEs → dequeue-idle-session) in a follow-up PR.

Key types:

  • AfeID (int64) — AFE identifier from the server's PeerInfo header at session-open. 0 is the sentinel "unknown" bucket for handshakes that did not carry a peer-info header.
  • AfeSnapshot — value-typed view of an afeHandle for pickers to score without holding sl.mu (Checkout re-resolves by ID; no *afeHandle escapes).
  • AfeSnapshotRow — debug-UI row emitted by sessionList.Snapshot() (consumed by afez/sessionz in a later debug PR).
  • afeHandle (unexported) — per-AFE bucket: FIFO idle queue, refCount (idle + inFlight + closing), two PeakEwma trackers.
  • SessionHandle — pool bookkeeping wrapper around Session. Carries inExpectedCount (I5 guard against WaitServerClose retry storm) and activated / closingRecorded / closeRecorded dedup flags for the pool's per-session hook chain.
  • sessionList (unexported) — the state machine, guarded by sl.mu.

The state model documents six invariants (I1-I6) that every method preserves:

I1  inExpectedCount ⇒ handleToAfe[sh] != nil
I2  readyCount == count of inExpectedCount handles
I3  afesWithReady == {afe : len(afe.sessions) > 0}
I4  afe.refCount == count of handleToAfe entries pointing at afe
I5  sh in afe.sessions ⇒ handleToAfe[sh]==afe AND inExpectedCount
I6  refCount-- only on OnSessionClosed (Closing keeps slot warm)

Lock order: sl.mu ONLY. RecordVRpcOutcome deliberately drops sl.mu between the map lookup and the PeakEwma.Update so the hot vRPC-outcome path doesn't serialize on it.

Consolidates AFE types (AfeID + AfeSnapshot) that previously lived in afe_snapshot.go — sessionList now owns all AFE-bucket concepts. Deletes afe_snapshot.go.

New file: session_list_test.go (~770 LOC) — I1-I6 coverage plus per-method tests (OnSessionStarted / Checkout / ReleaseToPool / OnSessionClosing / OnSessionClosed / RecordVRpcOutcome / ReadyAfes / Snapshot / AllHandles / Prune) and a concurrency stress test covering the documented lock-drop path in RecordVRpcOutcome.

Edits to debug_tracer.go — three new tag constants for sessionList bookkeeping violations (all unreachable-under-invariants, kept as belt-and-suspenders):

  • tagSessionListStartedNilSession
  • tagSessionListRefcountUnderflow
  • tagSessionListReadyCountUnderflow

Edit to session.go — one-line comment retarget on AfeID() (type now lives in session_list.go, not the deleted afe_snapshot.go).

Stack

  1. feat(bigtable): add Session debug surface (observability fields + methods) #20211 — Session debug surface (merged)
  2. feat(bigtable): add Session vRPC dispatch + slot lifecycle #20213 — Session vRPC dispatch (merged)
  3. feat(bigtable): add Session lifecycle (Start, Close, ForceClose, readLoop, heartBeatLoop) #20215 — Session lifecycle (merged)
  4. This PR — sessionList (PR-1 of 5)
  5. Next — SessionPoolImpl core, pool_lifecycle, pool_scaling, pool_debug

Test plan

  • go build ./internal/transport/ passes
  • go vet ./internal/transport/ clean
  • go test ./internal/transport/ -race -count=1 -short -timeout=120s passes — 20+ new sessionList tests plus all pre-existing.

First of five PRs porting the session pool infrastructure from
`feat/bigtable-sessionz-debug` (which powers the recycle-repro fleet).
Stacks on googleapis#20215 (Session lifecycle, merged).

## What lands

**New file: `session_list.go`** (~600 LOC) — the per-pool sessionList
data structure that groups sessions by the AFE (Application Front End)
their handshake landed on. Consumed by the two-tier picker
(K-choice-over-AFEs → dequeue-idle-session) in a follow-up PR.

Key types:
- `AfeID` (int64) — AFE identifier from the server's PeerInfo header at
  session-open. 0 is the sentinel "unknown" bucket for handshakes that
  did not carry a peer-info header.
- `AfeSnapshot` — value-typed view of an afeHandle for pickers to score
  without holding sl.mu (Checkout re-resolves by ID; no *afeHandle
  escapes).
- `AfeSnapshotRow` — debug-UI row emitted by `sessionList.Snapshot()`
  (consumed by afez/sessionz in a later debug PR).
- `afeHandle` (unexported) — per-AFE bucket: FIFO idle queue, refCount
  (idle + inFlight + closing), two PeakEwma trackers.
- `SessionHandle` — pool bookkeeping wrapper around Session. Carries
  `inExpectedCount` (I5 guard against WSC retry storm) and
  `activated / closingRecorded / closeRecorded` dedup flags for the
  pool's per-session hook chain (Phase-1/Phase-2 close dedup).
- `sessionList` (unexported) — the state machine, guarded by sl.mu.

The state model documents six invariants (I1-I6) that every method
preserves:
  I1  inExpectedCount ⇒ handleToAfe[sh] != nil
  I2  readyCount == count of inExpectedCount handles
  I3  afesWithReady == {afe : len(afe.sessions) > 0}
  I4  afe.refCount == count of handleToAfe entries pointing at afe
  I5  sh in afe.sessions ⇒ handleToAfe[sh]==afe AND inExpectedCount
  I6  refCount-- only on OnSessionClosed (Closing keeps slot warm)

Lock order: sl.mu ONLY. `RecordVRpcOutcome` deliberately drops sl.mu
between the map lookup and the PeakEwma.Update so the hot vRPC-outcome
path doesn't serialize on it.

Consolidates AFE types (AfeID + AfeSnapshot) that previously lived in
`afe_snapshot.go` — sessionList owns all AFE-bucket concepts. Deletes
`afe_snapshot.go`.

**New file: `session_list_test.go`** (~770 LOC) — I1-I6 coverage plus
per-method tests (OnSessionStarted / Checkout / ReleaseToPool /
OnSessionClosing / OnSessionClosed / RecordVRpcOutcome / ReadyAfes /
Snapshot / AllHandles / Prune) and a concurrency stress test covering
the documented lock-drop path in RecordVRpcOutcome.

**Edits to `debug_tracer.go`** — three new tag constants for sessionList
bookkeeping violations (all unreachable-under-invariants, kept as
belt-and-suspenders):
- `tagSessionListStartedNilSession`
- `tagSessionListRefcountUnderflow`
- `tagSessionListReadyCountUnderflow`

**Edit to `session.go`** — one-line comment retarget on `AfeID()`
(type now lives in `session_list.go`, not the deleted
`afe_snapshot.go`).

## Test plan
- [x] `go build ./internal/transport/` passes
- [x] `go vet ./internal/transport/` clean
- [x] `go test ./internal/transport/ -race -count=1 -short -timeout=120s`
      passes — 20+ new sessionList tests + all pre-existing.
@sushanb
sushanb requested review from a team as code owners July 27, 2026 17:11
@product-auto-label product-auto-label Bot added the api: bigtable Issues related to the Bigtable API. label Jul 27, 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 introduces a new sessionList component in bigtable/internal/transport/session_list.go along with comprehensive unit tests. This component manages session grouping by Application Frontend (AFE) identifiers and tracks latency metrics using PeakEWMA. Additionally, it updates session references and adds new debug tracer tags. The review feedback suggests a modernizing improvement to use atomic.Int64 instead of raw int64 fields for tracking session metrics in SessionHandle, which aligns with modern Go practices (Go 1.19+) and enhances type safety.

Comment on lines +121 to +123
outstanding int64
lastActivity int64 // UnixNano timestamp of the last completed call
picks int64 // Number of times the picker has picked this handle.

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.

medium

Since the project uses Go 1.19+ (as evidenced by the use of atomic.Bool in this struct), we can use atomic.Int64 instead of raw int64 fields. This is more idiomatic, type-safe, and prevents accidental non-atomic access to these fields.

Suggested change
outstanding int64
lastActivity int64 // UnixNano timestamp of the last completed call
picks int64 // Number of times the picker has picked this handle.
outstanding atomic.Int64
lastActivity atomic.Int64 // UnixNano timestamp of the last completed call
picks atomic.Int64 // Number of times the picker has picked this handle.

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 we use atomic I think we can simplify incrementing these variables too?

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.

Applied in 830f494 — swapped the three fields to atomic.Int64. Nice catch — the align64 marker on typed atomics also removes the 32-bit ARM alignment concern that raw int64 had to hand-hoist for. Kept the method wrappers (IncOutstanding, IncPicks, DecOutstanding, GetLastActivity) since DecOutstanding pairs the decrement with a lastActivity stamp and GetLastActivity handles the UnixNano→time.Time + zero-case conversion that raw .Load() can't.

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.

Done in 830f494 — swapped all three fields to atomic.Int64 and simplified the accessors. Cleaner shape; also fixes the goimports vet failure (trailing blank line at EOF).

Comment on lines +155 to +181
func (h *SessionHandle) IncOutstanding() {
atomic.AddInt64(&h.outstanding, 1)
}

// IncPicks increments the cumulative pick counter. Called from
// CheckoutSession on every successful pick so pool callers don't reach
// into the handle's atomic field directly (same shape as IncOutstanding).
func (h *SessionHandle) IncPicks() {
atomic.AddInt64(&h.picks, 1)
}

// DecOutstanding decrements outstanding calls and stamps lastActivity.
// The pool wakes waiters and returns the session to its AFE queue from
// Invoke's defer, so this method no longer signals directly.
func (h *SessionHandle) DecOutstanding() {
atomic.AddInt64(&h.outstanding, -1)
atomic.StoreInt64(&h.lastActivity, time.Now().UnixNano())
}

// GetLastActivity returns the time of the last activity.
func (h *SessionHandle) GetLastActivity() time.Time {
nano := atomic.LoadInt64(&h.lastActivity)
if nano == 0 {
return time.Time{}
}
return time.Unix(0, nano)
}

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.

medium

Update the methods to use the new atomic.Int64 API, which is cleaner and more idiomatic.

func (h *SessionHandle) IncOutstanding() {
	h.outstanding.Add(1)
}

// IncPicks increments the cumulative pick counter. Called from
// CheckoutSession on every successful pick so pool callers don't reach
// into the handle's atomic field directly (same shape as IncOutstanding).
func (h *SessionHandle) IncPicks() {
	h.picks.Add(1)
}

// DecOutstanding decrements outstanding calls and stamps lastActivity.
// The pool wakes waiters and returns the session to its AFE queue from
// Invoke's defer, so this method no longer signals directly.
func (h *SessionHandle) DecOutstanding() {
	h.outstanding.Add(-1)
	h.lastActivity.Store(time.Now().UnixNano())
}

// GetLastActivity returns the time of the last activity.
func (h *SessionHandle) GetLastActivity() time.Time {
	nano := h.lastActivity.Load()
	if nano == 0 {
		return time.Time{}
	}
	return time.Unix(0, nano)
}

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.

Applied in 830f494 — method bodies now use h.field.Add(1) / .Store(v) / .Load().

sushanb added a commit to sushanb/google-cloud-go that referenced this pull request Jul 27, 2026
Three Igor nits from the PR-1 (bigtable-session-list) review landing
now that PR-1 is upstream. Keeps sessionz consistent with the
upstreamable version so the future rebase is clean.

1. File-level "sl.mu deref" note hoisted from Checkout's godoc into
   the top block near I1-I6. Reads better at file level — the lock-
   drop is a whole-file invariant, not a Checkout property.

2. dropMembershipLocked — inverted to bail BEFORE readyCount-- when
   the assert would fail. Mirrors OnSessionClosed's refCount underflow
   pattern; counter no longer corrupts to -1 on the bookkeeping-drift
   branch. Strict improvement over the prior "decrement then log if
   negative" shape.

3. New TestSessionList_AllHandles_ReflectsMembership pins the h1 Idle
   / h2 Closed / h3 Closing membership contract — sessionz's per-
   session UI iterates AllHandles(), so an implementation slip that
   dropped Closing handles would silently blank rows.

Two PR-1 nits skipped as sessionz-inapplicable:
- AfeSnapshotRow.ID: int64→AfeID — sessionz uses unexported afeID; an
  exported struct field of an unexported type is un-idiomatic. Sync
  when sessionz rebases onto the upstream afeID→AfeID export rename.
- session.go AfeID() comment retarget — sessionz never had the stale
  afe_snapshot.go reference to begin with.
Comment on lines +121 to +123
outstanding int64
lastActivity int64 // UnixNano timestamp of the last completed call
picks int64 // Number of times the picker has picked this handle.

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 we use atomic I think we can simplify incrementing these variables too?

Addresses gemini-code-assist + mutianf comments on PR googleapis#20224: swap the
three raw int64 fields (outstanding, lastActivity, picks) for typed
atomic.Int64. Cleaner API surface (h.field.Add(1) / .Store(v) / .Load()
vs atomic.AddInt64(&h.field, 1)); prevents accidental non-atomic
access; carries the align64 marker so 64-bit fields are 8-byte aligned
on 32-bit ARM without hand-hoisting.

Also trim trailing blank line at EOF (goimports vet fix — the
`bigtable/internal/transport/session_list.go` entry in vet.sh's
`goimports -l .` output on the initial PR-1 push).

All 5 access sites on this branch are in session_list.go itself; the
migration is self-contained. Method wrappers (IncOutstanding /
IncPicks / DecOutstanding / GetLastActivity) preserved — DecOutstanding
pairs the decrement with the lastActivity stamp and GetLastActivity
handles the UnixNano→time.Time + zero-case conversion, so both add
value over raw .Load().
sushanb added a commit to sushanb/google-cloud-go that referenced this pull request Jul 27, 2026
Sync with PR-1 (bigtable-session-list @ 830f494) — addresses
gemini-code-assist + mutianf comments on googleapis#20224 asking for typed
atomics on the three raw int64 counter fields.

- `outstanding`, `lastActivity`, `picks`: `int64` → `atomic.Int64`
- Method bodies switched to `.Add(1)` / `.Store(v)` / `.Load()`
- Sessionz has 3 additional call sites vs PR-1 (session_pool.go
  scale-down check, session_pool_debug.go slow-vRPC log,
  session_snapshot.go SessionHandle.Snapshot); all migrated.
- session_snapshot.go's `sync/atomic` import dropped (no bare
  atomic.* funcs remain there after the migration).

Cleaner API surface; carries the align64 marker so 64-bit fields are
8-byte aligned on 32-bit ARM without hand-hoisting. Same atomicity +
memory-ordering semantics as the raw atomic.*Int64 they replace.

Method wrappers (IncOutstanding / IncPicks / DecOutstanding /
GetLastActivity) preserved on SessionHandle — DecOutstanding pairs
dec + lastActivity stamp; GetLastActivity handles the UnixNano→
time.Time + zero-case conversion. Both add value over raw .Load().
@sushanb
sushanb merged commit dbf0c3f into googleapis:main Jul 27, 2026
19 checks passed
sushanb added a commit to sushanb/google-cloud-go that referenced this pull request Jul 27, 2026
Second of five PRs porting the session pool infrastructure from
`feat/bigtable-sessionz-debug` (which powers the recycle-repro fleet).
Stacks on googleapis#20224 (per-AFE sessionList).

## What lands

**SessionPoolImpl** — the concrete two-tier read/write session pool for
one resource. ~4000 LOC across five files:

- `session_pool.go` (~550 LOC) — struct + constructor + Invoke +
  CheckoutSession (waiter queue, deadline propagation) + pluggable
  picker (Simple / LeastInFlight / LeastLatency) via the AFE picker
  from PR googleapis#20204.
- `session_pool_lifecycle.go` (~530 LOC) — SessionHooks wiring
  (onStart/onActive/onClosing/onClose), consecutive-failure breaker,
  Close (5-phase teardown), and the WaitGoroutines / spawns.Wait
  choreography that guarantees no session-owned goroutine outlives the
  pool.
- `session_pool_scaling.go` (~310 LOC) — Tick loop, createSession
  (dial + OpenSession + hook registration), pendingStarts /
  startingSessions accounting so scale-up decisions never
  double-count in-flight opens. Uses the channel-pool pick hint
  (`ChannelPickHintInto`, added to connpool.go) to attribute each
  session to its underlying channel.
- `session_pool_debug.go` (~415 LOC) — PoolSnapshot / slow-vRPC ring /
  per-close-reason counters / scaling-history buffer / pickHistory
  ring — the input to the sessionz / afez / loadz debug pages
  (landing in a later PR).
- `session_snapshot.go` (~590 LOC) — the value-typed snapshot record
  the debug surface consumes; no live locks escape.
- Five matching `_test.go` files (~2200 LOC): pool lifecycle,
  scaling, consecutive-failure breaker, AFE integration, debug
  surface, snapshot rendering, plus a K-choice bench.

**Session helpers added** (pool-facing additions to files already
touched by prior PRs, isolated to keep the diff readable):
- `Session.loops sync.WaitGroup` + `WaitGoroutines()` — pool teardown
  blocks on this so readLoop / heartbeatLoop and their
  notifyClosed → recordClose callback chains fully unwind before
  Close returns. Prevents session goroutines from racing metric-var
  writes across test boundaries.
- `Session.closeErr atomic.Pointer[error]` + `setCloseErr()` /
  `closeError()` — preserves the raw Recv error handed to
  handleClose. Pool surfaces this on consecutive-failure breaker
  trips so operators see the underlying server rejection
  (e.g. FailedPrecondition when the resource is still being created)
  instead of only the sentinel.

**Supporting additions to existing files** (minimal, isolated):
- `afe_picker.go` — const `defaultAfeRandomSubsetSize = 2`
  (power-of-two-choices K-choice default; matches Java).
- `debug_tracer.go` — three new tag constants:
  `tagSessionPoolCreatePanic` (distinguishes recovered panic from
  plain error return), `tagSessionPoolConsecutiveFailuresTripped`
  (breaker drain fired), `tagSessionPoolCheckoutFailedCINil`
  (Invoke returned InvokeResult{} with nil ClusterInfo — dominates
  the nil-ClusterInfo population during cold-start / pool-close
  bursts).
- `connpool.go` — `ChannelPickHintInto(ctx, *atomic.Int32)` context
  helper used by createSession to link each session to its channel
  (surfaced in sessionz / channelz in a later PR). No-op when the
  channel pool doesn't consume the hint.

## What does NOT land yet
- The `SessionPool` / `Invoker` interfaces (follow-up PR alongside
  sessionClient / sessionTable — those consumers land in PR-3+).
- The `SessionPoolImpl` factory / `NewSessionPool` wiring against
  `bigtable.Client` (PR-3).
- Debug pages (sessionz / afez / flightz / loadz — landing under
  `bigtable/debugview/` in a later PR).

## Test plan
- [x] `go build ./...` passes
- [x] `go vet ./internal/transport/` clean
- [x] `go test ./internal/transport/ -race -count=1 -short
      -skip 'AfeLbSim' -timeout=180s` passes (32s wall)
sushanb added a commit to sushanb/google-cloud-go that referenced this pull request Jul 27, 2026
Second of five PRs porting the session pool infrastructure from
`feat/bigtable-sessionz-debug` (which powers the recycle-repro fleet).
Stacks on googleapis#20224 (per-AFE sessionList).

## What lands

**SessionPoolImpl** — the concrete two-tier read/write session pool for
one resource. ~4000 LOC across five files:

- `session_pool.go` (~550 LOC) — struct + constructor + Invoke +
  CheckoutSession (waiter queue, deadline propagation) + pluggable
  picker (Simple / LeastInFlight / LeastLatency) via the AFE picker
  from PR googleapis#20204.
- `session_pool_lifecycle.go` (~530 LOC) — SessionHooks wiring
  (onStart/onActive/onClosing/onClose), consecutive-failure breaker,
  Close (5-phase teardown), and the WaitGoroutines / spawns.Wait
  choreography that guarantees no session-owned goroutine outlives the
  pool.
- `session_pool_scaling.go` (~310 LOC) — Tick loop, createSession
  (dial + OpenSession + hook registration), pendingStarts /
  startingSessions accounting so scale-up decisions never
  double-count in-flight opens. Uses the channel-pool pick hint
  (`ChannelPickHintInto`, added to connpool.go) to attribute each
  session to its underlying channel.
- `session_pool_debug.go` (~415 LOC) — PoolSnapshot / slow-vRPC ring /
  per-close-reason counters / scaling-history buffer / pickHistory
  ring — the input to the sessionz / afez / loadz debug pages
  (landing in a later PR).
- `session_snapshot.go` (~590 LOC) — the value-typed snapshot record
  the debug surface consumes; no live locks escape.
- Five matching `_test.go` files (~2200 LOC): pool lifecycle,
  scaling, consecutive-failure breaker, AFE integration, debug
  surface, snapshot rendering, plus a K-choice bench.

**Session helpers added** (pool-facing additions to files already
touched by prior PRs, isolated to keep the diff readable):
- `Session.loops sync.WaitGroup` + `WaitGoroutines()` — pool teardown
  blocks on this so readLoop / heartbeatLoop and their
  notifyClosed → recordClose callback chains fully unwind before
  Close returns. Prevents session goroutines from racing metric-var
  writes across test boundaries.
- `Session.closeErr atomic.Pointer[error]` + `setCloseErr()` /
  `closeError()` — preserves the raw Recv error handed to
  handleClose. Pool surfaces this on consecutive-failure breaker
  trips so operators see the underlying server rejection
  (e.g. FailedPrecondition when the resource is still being created)
  instead of only the sentinel.

**Supporting additions to existing files** (minimal, isolated):
- `afe_picker.go` — const `defaultAfeRandomSubsetSize = 2`
  (power-of-two-choices K-choice default; matches Java).
- `debug_tracer.go` — three new tag constants:
  `tagSessionPoolCreatePanic` (distinguishes recovered panic from
  plain error return), `tagSessionPoolConsecutiveFailuresTripped`
  (breaker drain fired), `tagSessionPoolCheckoutFailedCINil`
  (Invoke returned InvokeResult{} with nil ClusterInfo — dominates
  the nil-ClusterInfo population during cold-start / pool-close
  bursts).
- `connpool.go` — `ChannelPickHintInto(ctx, *atomic.Int32)` context
  helper used by createSession to link each session to its channel
  (surfaced in sessionz / channelz in a later PR). No-op when the
  channel pool doesn't consume the hint.

## What does NOT land yet
- The `SessionPool` / `Invoker` interfaces (follow-up PR alongside
  sessionClient / sessionTable — those consumers land in PR-3+).
- The `SessionPoolImpl` factory / `NewSessionPool` wiring against
  `bigtable.Client` (PR-3).
- Debug pages (sessionz / afez / flightz / loadz — landing under
  `bigtable/debugview/` in a later PR).

## Test plan
- [x] `go build ./...` passes
- [x] `go vet ./internal/transport/` clean
- [x] `go test ./internal/transport/ -race -count=1 -short
      -skip 'AfeLbSim' -timeout=180s` passes (32s wall)
sushanb added a commit that referenced this pull request Jul 28, 2026
…#20225)

## Summary

Second of five PRs porting the session pool infrastructure from
`feat/bigtable-sessionz-debug` (which powers the recycle-repro fleet).

Adds `SessionPoolImpl`: the concrete two-tier read/write session pool
for one resource. ~4000 LOC across five files plus matching tests.

## Stack

- [ ] PR-1: sessionList (#20224) — per-AFE bucketing data structure.
**Not yet merged.**
- [x] **PR-2 (this)** — SessionPoolImpl (pool + scaling + debug +
snapshot).
- [ ] PR-3 — `SessionPool` / `Invoker` interfaces + `sessionClient` /
`sessionTable` factory wiring.
- [ ] PR-4 — Debug pages (`sessionz` / `afez` / `flightz` / `loadz`
under `bigtable/debugview/`).
- [ ] PR-5 — `bigtable.Client` integration + release notes.

**Because PR-1 has not landed, this PR is opened against `main` and the
diff includes PR-1's commits.** Once #20224 merges the base can be
re-targeted (or this branch rebased) so only PR-2's own delta shows.

## What lands

**SessionPoolImpl** (5 files, ~2400 LOC prod + ~2200 LOC tests):

- `session_pool.go` — struct + constructor + `Invoke` +
`CheckoutSession` (waiter queue, deadline propagation) + pluggable
picker via the AFE picker from #20204.
- `session_pool_lifecycle.go` — `SessionHooks` wiring,
consecutive-failure breaker, `Close` (5-phase teardown),
`WaitGoroutines` / `spawns.Wait` choreography so no session-owned
goroutine outlives the pool.
- `session_pool_scaling.go` — `Tick` loop, `createSession` (dial +
`OpenSession` + hook registration), `pendingStarts` / `startingSessions`
accounting so scale-up decisions never double-count in-flight opens.
Uses the channel-pool pick hint (`ChannelPickHintInto`, added to
`connpool.go`) to attribute each session to its underlying channel.
- `session_pool_debug.go` — `PoolSnapshot` / slow-vRPC ring /
per-close-reason counters / scaling-history buffer / `pickHistory` ring
— the input to the sessionz / afez / loadz debug pages (landing in a
later PR).
- `session_snapshot.go` — the value-typed snapshot record the debug
surface consumes; no live locks escape.

**Session helpers added** (pool-facing additions to files already
touched by prior PRs, kept minimal):

- `Session.loops sync.WaitGroup` + `WaitGoroutines()` — pool teardown
blocks on this so `readLoop` / `heartbeatLoop` and their `notifyClosed →
recordClose` callback chains fully unwind before `Close` returns.
Prevents session goroutines from racing metric-var writes across test
boundaries.
- `Session.closeErr atomic.Pointer[error]` + `setCloseErr` /
`closeError` — preserves the raw `Recv` error handed to `handleClose`.
Pool surfaces this on consecutive-failure breaker trips so operators see
the underlying server rejection (e.g. `FailedPrecondition` when the
resource is still being created) instead of only the sentinel.

**Supporting additions to existing files:**

- `afe_picker.go` — const `defaultAfeRandomSubsetSize = 2`
(power-of-two-choices K-choice default; matches Java).
- `debug_tracer.go` — three new tag constants:
`tagSessionPoolCreatePanic`, `tagSessionPoolConsecutiveFailuresTripped`,
`tagSessionPoolCheckoutFailedCINil`.
- `connpool.go` — `ChannelPickHintInto(ctx, *atomic.Int32)` context
helper. No-op when the channel pool doesn't consume the hint.

## What does NOT land yet

- `SessionPool` / `Invoker` interfaces (follow-up PR alongside
sessionClient / sessionTable).
- `bigtable.Client` integration (PR-3+).
- Debug pages under `bigtable/debugview/` (later PR).

## Test plan

- [x] `go build ./...` passes.
- [x] `go vet ./internal/transport/` clean.
- [x] `go test ./internal/transport/ -race -count=1 -short -skip
'AfeLbSim' -timeout=180s` — passes (32s wall). ~2200 LOC of new tests
across pool lifecycle, scaling, consecutive-failure breaker, AFE
integration, debug surface, snapshot rendering, plus a K-choice bench.

---

# Reviewer guide

## Guide 1 — mutianf (human)

### What this PR does
Adds `SessionPoolImpl`, the layer that sits above the per-AFE
`sessionList` shipped in #20224 and consumes it via a two-tier picker
(AFE first, then a ready session in that AFE). It owns the session
lifecycle (open / active / closing / close hooks), server-driven scaling
via `PoolSizer`, a consecutive-failure circuit breaker, and the
debug/observability surface (histograms + ring buffers) that feeds
sessionz/loadz. New files: 5 source, 6 test, ~4.9k LOC. Nothing outside
`session_pool*.go` / `session_snapshot*.go` is new logic — the small
edits elsewhere are hook-plumbing scaffolding already vetted by the
session/AFE subagent reviewers.

### Recommended read order
1. **`session_pool.go`** — start here. Struct field layout with
per-field ownership comments (`:104-179`), the `waiter` FIFO shape
(`:94-101`), `CheckoutSession` two-tier pick + parking (`:235-310`),
`Invoke` (`:465-559`), `Stats` (`:361-397`), `UpdateConfig`
(`:402-431`), `pickerFromLoadBalancing` (`:439-461`). Skim
`session_pool_test.go` (28 tests) — the FIFO waiter, Stats, and
UpdateConfig behaviors are all covered there.
2. **`session_pool_lifecycle.go`** — hooks (`onActive:255`,
`onClosing:308`, `onClose:336`), `recordSessionClose` once-CAS on
`Session.poolCloseRecorded` (`:117-130`), `Close`'s 6-phase teardown
(`:154-247`), `noteAbnormalCloseIfAny` breaker (`:363-392`), the three
ticker loops (`:426-538`). Skim `session_pool_lifecycle_test.go` — every
hook + `Close`.
3. **`session_pool_scaling.go`** — `Tick` (`:81-162`), `createSession`
worker (`:164-274`), `scalingReason` (`:278-299`),
`noDeadlineButCancellableContext` (`:301-311`). Skim
`session_pool_scaling_test.go` — the `scalingInProgress` gate and
panic-safety are the only non-obvious contracts.
4. **`session_pool_debug.go`** — `poolMetrics` (`:36-72`), `latencyHist`
log2 histogram (`:160-228`), the four ring buffers (slow-vRPC,
time-series, lifetimes, pick-history), `recordPickDecision`
(`:366-387`). Skim `session_pool_debug_test.go` — mostly ring-cap and
rate-computation coverage.
5. **`session_snapshot.go`** — mostly type defs. Focus on `PoolSnapshot`
(`:452-594`) and `LoadBalancingSnapshot` (`:414-436`) as the debug-view
contract.
6. **`session_pool_consecutive_failures_test.go`** and
**`session_pool_afe_test.go`** — end-to-end behavior verification;
useful for confirming intent.

### Flow of events
- **CheckoutSession → Invoke → release.** `CheckoutSession`
(`session_pool.go:235`) opportunistically kicks Tick if
`sl.ReadyCount()==0`, snapshots the picker under `p.mu`, then two-tier
picks outside the lock: `ReadyAfes()` → `PickAfe` → `Checkout(afeID)`
(`:259-268`). Miss → park in the FIFO waiter queue (`:286-289`), bracket
`waitersCount` for the sizer (`:291,300`). `Invoke` (`:465`) checks out,
runs `sh.session.Invoke`, records latencies (`:508-523`), logs a
slow-vRPC row if over threshold (`:524-557`); the deferred
`sh.DecOutstanding()` + `noteVRpcOutcome` (`:493-496`) hands the
OK-gated latency to the per-AFE PeakEwma tracker. Session release itself
is driven by `OnSlotDrained` (installed at
`session_pool_scaling.go:228-231`), which returns the handle to
`sessionList` and calls `signalFree` — separate from the `defer` in
`Invoke`.
- **Background Tick.** `startTickLoop` (`session_pool_lifecycle.go:426`)
fires every 1 s → `tickOnce` debounces via `tickPending` CAS
(`:447-458`) → `Tick` (`session_pool_scaling.go:81`) samples uptimes,
gates on `scalingInProgress`, calls `sizer.Decide()`, and on a positive
delta reserves `pendingStarts += delta` + `spawns.Add(delta)` under
`p.mu` (`:131-138`) then fans out one goroutine per session. Each
`createSession` acquires the budget outside `p.mu`, dials via
`streamFactory`, transfers `pendingStarts → startingSessions` in one
lock (`:246-249`), starts the session, and blocks on `WaitGoroutines` so
it stays on `p.spawns` until the session dies.
- **Abnormal close → breaker trip.** `onClose`
(`session_pool_lifecycle.go:336`) CAS's `closeRecorded`, calls
`noteAbnormalCloseIfAny` (`:363`), which bumps `consecutiveFailures` and
stores the raw error into `lastAbnormalCloseErr`. Crossing the threshold
snapshots the poison, CAS-resets the counter, and calls
`drainWaitersWithErr` — waiters get `*consecutiveFailureError` wrapping
the last cause (so `errors.Is(err, ErrConsecutiveFailures)` and
`status.Code(err)` both still work, `:60-82`). Counter only resets in
`onActive` (`:292-293`) — a successful open, not a healthy vRPC.

### Key invariants
1. **Two-tier pick, no re-entrant `p.mu`.** `CheckoutSession` reads
`p.picker` under `p.mu` (`session_pool.go:249-255`) then unlocks before
calling picker/sessionList. `recordPickDecision` takes `pickerName` as a
**parameter** (`session_pool_debug.go:366`, `session_pool.go:260-262`)
precisely because the caller already holds no lock — but any new pool
method that reads `p.picker.Name()` from a hot path must not re-take
`p.mu`.
2. **Waiter FIFO with `waitersCount` bracketed.** Every `PushBack` bumps
`waitersCount` (`session_pool.go:291`); every wake path (`ctx.Done`,
`w.ready`) decrements it (`:294,300`). `removeWaiter` (`:316`) is
idempotent via `w.elem != nil`; `signalFree` and `drainWaitersWithErr`
nil out `elem` under `waitersMu` (`:329-358`). `Stats().PendingCount`
reads `waitersCount.Load()` — this is the sizer's queue-depth input.
3. **Close-exactly-once accounting.** `sessionsClosed` and
`closesByReason` bumps are gated by
`Session.poolCloseRecorded.CompareAndSwap(false, true)` inside
`recordSessionClose` (`session_pool_lifecycle.go:117-130`).
`sh.closingRecorded` and `sh.closeRecorded` are per-handle CAS's
protecting the lifetime histogram + the `OnClose` branch. `Close`'s
Phase 1 pre-flips both CAS's on every handle (`:187-193`) so a
concurrent mid-flight onClosing can't double-count.
4. **Breaker resets only on `onActive`.** `consecutiveFailures.Store(0)`
and `lastAbnormalCloseErr.Store(nil)` live at
`session_pool_lifecycle.go:292-293`. Not on per-vRPC OK — otherwise one
long-lived healthy session would mask a run of failed opens.
5. **Hot path is atomics/RLocks; debug views take snapshots.** `Stats`
is the only per-request path that briefly takes `p.mu`
(`session_pool.go:362`); everything else on the vRPC path is atomic.
Debug snapshotters copy under lock and format after release
(`session_snapshot.go:452-594`).

### What NOT to worry about
- **Session / vRPC layer itself** — shipped in #20213 / #20215 (state
machine, one-in-flight, PeerInfo timing, retry oracle, heartbeat).
- **Per-AFE `sessionList` I1-I6** — shipped in #20224, has its own
tests.
- **`PoolSizer` scaling formula** — already upstream (`pool_sizer.go`);
this PR only wires it and consumes `ScaleDecision`.
- **AFE pickers (`SimpleAfePicker` / `LeastInFlight` / `LeastLatency`)**
— already upstream (`afe_picker.go`); this PR only builds them via
`pickerFromLoadBalancing`.
- **`SessionThrottler` / `AdaptiveSessionThrottler`** — already
upstream; this PR consumes `Acquire` / `Release` / `UpdateConfig`.
- **`ClientConfigurationManager` polling** — this pool receives
`UpdateConfig` calls; the polling itself is elsewhere.

### Danger zones
- **Re-entrant `p.mu` on picker access.** `recordPickDecision`
intentionally takes `pickerName` as a param
(`session_pool_debug.go:366`). Adding a new pool method that reads
`p.picker.Name()` from within a `CheckoutSession` code path is a
re-entrant deadlock; pass the name in or snapshot up-front.
- **`startingSessions` / `pendingStarts` accounting.** Tick reserves
`pendingStarts` under `p.mu` (`session_pool_scaling.go:131-138`),
`createSession`'s `reserved` defer releases it on any early return
(`:172-179`), and the transfer at `:246-249` is atomic under `p.mu`.
`onActive` deletes from `startingSessions`
(`session_pool_lifecycle.go:265`). Any new failure branch in
`createSession` must preserve the invariant `pendingStarts +
len(startingSessions) + Ready = "in-flight scale-up capacity"`.
- **`budget.Acquire` blocks; must run OUTSIDE `p.mu`.** Currently at
`session_pool_scaling.go:181`, deliberately after the `defer reserved`
block and before any `p.mu.Lock()`. Moving it under the lock deadlocks
scale-up under budget exhaustion.
- **Slow-vRPC is fire-and-forget.** `recordSlowVRpc`
(`session_pool_debug.go:301`) appends into a mutex-guarded ring; it's on
the vRPC return path but bounded by threshold gating and one small
mutex. Do not add I/O, logging fanout, or channel sends here.
- **`newTestPool` / bootstrap defaults.** Test helpers construct the
pool without a `ClientConfigurationManager`, so the bootstrap defaults
from `defaultPoolConfig()` (`session_pool.go:218-226`) are the only
config those tests see. Real callers always get `UpdateConfig`
synchronously on registration — verify by reading `NewSessionPoolImpl`
end-to-end, not by trusting the bootstrap values.
- **`Close` phase ordering.** Phase 4 (`poolCancel`) runs AFTER Phase 3
(`wg.Wait` on graceful closes) because Phase 2's `closeCtx` derives from
`p.poolCtx`; swapping order strands in-flight graceful closes on a
cancelled ctx. Phase 5 (`p.spawns.Wait`) blocks on every createSession
goroutine's `WaitGoroutines` — that's why createSession blocks on
`s.WaitGoroutines()` at `session_pool_scaling.go:272`.

---

## Guide 2 — mutianf-bot (automated reviewer)

### REAL HAZARDS to flag

- **Re-entrant `p.mu` in pool methods called from `CheckoutSession`.**
Anchor: `session_pool.go:235-310`. `p.mu` is dropped at `:255` before
`PickAfe` / `Checkout` / `recordPickDecision` fire. Flag any newly-added
helper called from that block that re-acquires `p.mu`, or any new method
that reads `p.picker.Name()` without taking the name as a parameter (see
the intentional parameter pattern at `session_pool_debug.go:366`).
- **`budget.Acquire` under `p.mu`.** Currently correctly outside the
lock at `session_pool_scaling.go:181`. `SessionThrottler.Acquire` blocks
on the budget semaphore; calling it while holding `p.mu` would deadlock
scale-up. Flag any code path that acquires `p.mu` before line `:181` or
moves `Acquire` inside a `Lock`/`Unlock` bracket.
- **`sync.Map` allocations on hit paths.** `bumpCloseReason` uses `Load`
first, `LoadOrStore(k, new(atomic.Int64))` only on miss
(`session_pool_lifecycle.go:102-111`) — this is the correct pattern.
Flag any new `sync.Map.LoadOrStore(key, new(...))` call on a hot path
that isn't gated by a preceding `Load` — that allocates on every hit.
- **Waiter counter drift.** `waitersCount.Add(+1)` at
`session_pool.go:291`, `Add(-1)` on both the `ctx.Done` branch (`:294`)
and the `w.ready` branch (`:300`). Flag any new wake path, timeout
branch, or early-return between `:291` and `:308` that doesn't
decrement, and any new enqueue site that doesn't increment. Drift here
corrupts the sizer's `PendingCount` input.
- **Unbalanced `pendingStarts` / `startingSessions`.** Tick increments
`pendingStarts` under `p.mu` at `session_pool_scaling.go:131-138`;
`createSession`'s `reserved` defer at `:172-179` releases on early
return; the transfer to `startingSessions` at `:246-249` is atomic;
`onActive` deletes at `session_pool_lifecycle.go:265`; failed-start
deletes at `session_pool_scaling.go:253-255`. Flag any new failure
branch in `createSession` that returns without either the `reserved`
defer or an explicit transfer/cleanup.
- **Missing CAS on close-once flags.** `sessionsClosed.Add(1)` sits
downstream of `s.poolCloseRecorded.CompareAndSwap(false, true)` at
`session_pool_lifecycle.go:121`. Lifetime histogram sits downstream of
`sh.closingRecorded.CompareAndSwap(false, true)` at `:187` and `:317`.
`recordSessionClose` re-entry is dedup'd at `:190` and `:347`. Flag any
new counter bump / histogram record that isn't gated by the
corresponding CAS.
- **Uncontrolled goroutine spawns.** Every pool-spawned goroutine must
be tracked so `Close` phases 3/5/6 can drain: createSession workers use
`p.spawns.Add`/`Done` under `p.mu`
(`session_pool_scaling.go:137-138,149`); `spawnTickOnce` uses the same
pattern (`session_pool_lifecycle.go:464-476`); the mid-Close onActive
dispatch adds at `:274`. Session-owned goroutines
(`readLoop`/`heartbeatLoop`) are tracked on `Session.loops` and drained
via `WaitGoroutines`. Flag any bare `go func(){…}()` in these files with
no WaitGroup entry.
- **Hot-path lock churn.** `p.mu` is briefly held on the fast path only
at `session_pool.go:239-241` and `:249-255`. `Stats` takes `p.mu`
(`:362`). Flag any new `p.mu.Lock()` inserted into `Invoke` (`:465-559`)
or the pre-park stretch of `CheckoutSession` outside the two existing
points.

### FALSE-POSITIVE SUPPRESSION

- **"Package name should be `transport`, not `internal`."** The
directory is `bigtable/internal/transport/` but the `package`
declaration is `package internal` (confirmed at `session_pool.go:20`).
Package name is correct as-is; do NOT flag.
- **Local variable shadowing an exported type** where the local name is
idiomatic (e.g. `afeID` local vs `AfeID` type in `CheckoutSession`).
Already resolved intentionally; do NOT re-raise variants.
- **`goimports` / `gofmt` / column-alignment / trailing-newline nits.**
CI (`goimports -l`, `gofmt -l`, `go vet`) already gates these. Bot echo
is noise.
- **Comments referencing PR #20213 / #20215 / #20224.** Stacked-PR
context, not stale references; do NOT suggest removal.
- **Test coverage complaints for `pool_sizer.go`, `afe_picker.go`,
`session_list.go`, `session.go`, `session_vrpc.go`,
`session_throttler.go`, `client_configuration_manager.go`,
`default_client_config.go`.** All shipped in earlier PRs (#20213,
#20215, #20224) with their own tests; out of scope here.
- **"Missing error wrapping"** on internal-only calls where the caller
already annotates via `fmt.Errorf("POOL %s ...: %w", ...)` or via
`btopt.Debugf`. Do NOT suggest adding a second wrap.
- **Retry loop / context propagation questions on `Session.Invoke`.**
That's the Session layer (`session_vrpc.go`), out of scope for this PR.
- **"Consider using `sync.RWMutex` instead of `sync.Mutex` on `p.mu`."**
The pool holds `p.mu` for tens of nanoseconds at a time and never for
read-heavy loops; the added atomic on `RLock`/`RUnlock` would cost more
than it saves. Do NOT suggest.
- **"Consider extracting anonymous goroutine into named function."**
Style-only; do NOT suggest for the three ticker loops or the
createSession worker.

### SCOPE BOUNDARY

Comment ONLY on:
- `bigtable/internal/transport/session_pool.go`
- `bigtable/internal/transport/session_pool_lifecycle.go`
- `bigtable/internal/transport/session_pool_scaling.go`
- `bigtable/internal/transport/session_pool_debug.go`
- `bigtable/internal/transport/session_snapshot.go`
- `bigtable/internal/transport/session_pool_*_test.go`
- `bigtable/internal/transport/session_snapshot_test.go`

Do NOT comment on additions to:
- `session.go` / `session_vrpc.go` (WaitGoroutines / closeError
additions — vetted)
- `connpool.go` (`ChannelPickHintInto` helper — vetted)
- `afe_picker.go` (`defaultAfeRandomSubsetSize` constant — vetted)
- `debug_tracer.go` (3 new tags — vetted)

These are supporting scaffolding, already reviewed by the 3 subagent
reviewers in this stack. Only re-raise if something looks actively
unsafe.

### EFFORT SCALING

- ~4.9k LOC across 12 files. Do NOT paginate uniformly.
- **First pass — the 4 hot source files, in this order:**
  1. `session_pool.go` (559 LOC)
  2. `session_pool_lifecycle.go` (538 LOC)
  3. `session_pool_scaling.go` (311 LOC)
  4. `session_pool_debug.go` (416 LOC)
- **Second pass ONLY if a first-pass finding needs corroboration:**
`session_snapshot.go` (594 LOC, mostly type defs), and the tests. Tests
use `newTestPool`, which skips config wiring — do NOT flag bootstrap
defaults on tests as if they were production paths.
- If a first-pass finding is a real hazard from the list above, cite the
file:line and the exact anchor pattern it violates. Do not file
speculative "consider" comments.
sushanb added a commit that referenced this pull request Jul 28, 2026
## Summary

Third of five PRs porting the session-pool infrastructure from
`feat/bigtable-sessionz-debug` into upstream. Introduces
`internal/session/` — a proto-native SessionClient + SessionTable
API sitting on top of PR-2's SessionPoolImpl.

- `internal/session/api.go` — public interfaces (`ChannelPool`,
  `Config`, `SessionClient`, `SessionTableAPI`, `DebugAccess`).
- `internal/session/client.go` — `SessionClient` impl: dedicated
  channel pool (no primer), `ClientConfigurationManager` wiring,
  `OpenSessionTable` / `OpenAuthorizedView` / `OpenMaterializedView`
  factories that mint lazily-opened per-resource pools keyed by
  `{resource, permission}`.
- `internal/session/table.go` — `SessionTable` impl. Two `*lazyPool`
  (read + write); MV is read-only (write pool nil, `MutateRow`
  returns `ErrWriteNotSupported`). `stampAttempt` sources per-attempt
  `cluster_id` / `zone_id` / peer fields from typed
  `InvokeResult.ClusterInfo` and `InvokeResult.PeerInfo` per
  CLIENT_SIDE_METRICS_SPEC #1.
- `internal/session/lazy_pool.go` — `Invoker` + `SessionPool`
  interfaces + open-on-first-use lazy wrapper. Failed opens are NOT
  cached; the next call retries.
- `internal/session/debug.go` — `DebugAccess` impl surfacing pool
  snapshots for sessionz/loadz/channelz/configz.

### Transport additions to support the above

- `transport/debug_api.go` (new) — `SessionDebugProvider` /
  `ChannelDebugProvider` / `ConfigDebugProvider` interfaces +
  `ChannelPoolDebug` / `SessionRef` DTOs. Lives in transport (not
  bigtable) so `bigtable.Client` and `internal/session.SessionClient`
  can implement without an import cycle.
- `transport/diverter.go` — `sessionPicks` / `classicPicks` counters,
  `DiverterSnapshot`, `Snapshot()`.
- `transport/connpool.go` — `ChannelSnapshot` +
  `ChannelPoolSnapshot` type + method, `WithInstanceName` /
  `WithAppProfile` options for channelz labelling.
- `transport/debug_tracer.go` — exported `DebugTag` +
  `RecordDebugTag` + `TagSessionAttemptNilClusterInfo` /
  `TagSessionAttemptEmptyClusterID` catalog constants.
- `transport/session_descriptors.go` — `SessionType.ProtoName()` for
  human-readable pool identifiers.
- `transport/direct_access_checker.go` — renames
  `newPingAndWarmDirectAccessChecker` →
  `NewPingAndWarmDirectAccessChecker` (constructor exported) and
  nil-guards `primer.Prime` so session-based clients can pass a nil
  primer. Session clients warm channels on-demand via `OpenSession`,
  not eagerly at pool-init.

### Lifecycle correctness

`sessionClient.Close()` snapshots owned resources under `poolsMu` and
releases the lock before running `Close` / `Shutdown` / `Cancel`
calls, so a snapshot method holding `poolsMu` never deadlocks
teardown. Post-Close Opens surface a distinct
`ErrSessionClientClosed` sentinel rather than misleading
`errReadPoolNil` / `ErrWriteNotSupported`.

## Stack

- **PR-1** #20224 (sessionList) — merged
- **PR-2** #20225 (SessionPoolImpl) — open; this PR stacks on it
- **PR-4** (this PR)
- PR-5 will follow with the bigtable-package integration + debugview.

The diff on this PR includes PR-2's commits until #20225 merges into
main.

## Test plan

- [x] `go build ./internal/session/ ./internal/transport/ ./...`
- [x] `go test ./internal/session/ -race -count=1 -short -timeout=180s`
- [x] `go test ./internal/transport/ -race -count=1 -short -skip
'AfeLbSim|TestHighQpsSession' -timeout=240s`
- [x] `gofmt -l ./internal/session/ ./internal/transport/` — clean
- [x] `go vet ./internal/session/ ./internal/transport/` — clean
- [x] Reviewed against the 4 behavioral specs (SESSION_SPEC,
  SESSION_CLIENT_SPEC, SESSION_POOL_SPEC, CLIENT_SIDE_METRICS_SPEC)
  and SESSION_COMPONENT_SPEC (boundary rules) — PASS.
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))
([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