You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 599aea9
Browse filesBrowse the repository at this point in the historyBrowse files
fix(bigtable): real per-resource pool teardown on sessionTable.Close + cache close-race gate (#20264)
Fixes two related latent bugs in the session data plane:
1. **sessionTable.Close was a no-op** — the interface godoc at
`bigtable/internal/session/api.go:45-48` promises to release the
resource's read + write session pools; the implementation returned
nil. Pools were reclaimed only by `sessionClient.Close`, so
`bigtable.Client`'s TTL cache could evict a handle without freeing
the underlying pool + streams + goroutines.
2. **sessionTableCache close-race (audit finding #5)** — a slow-path
openFn straddling `sessionTableCache.close()` would install a
fresh handle into a cache the sweeper had already stopped clearing.
Zero-impact while sessionTable.Close was a no-op; a per-race pool
leak once teardown becomes real.
## Design
Real teardown routed through symmetric release closures:
- `sessionClient.releaseSessionPool(key)` — under `sessionPoolsMu`,
delete the entry then `unregister()` + `pool.Close()` outside the
lock (matches the snapshot-under-lock pattern in `Close`).
- `buildLazyReleaser(key)` — sibling to `buildLazyOpener`; returns a
`func() error` closure for a specific poolKey.
- `sessionTable.Close` — invokes closeRead + closeWrite, joining errors
via `errors.Join`. Nil-safe for materialized views' missing write side.
No refcount inside sessionClient. Rationale: `bigtable.Client`'s
sessionTableCache dedupes handles per fully-qualified resource name,
so at-most-one sessionTable per resource per Client at any moment —
the cache is the 'refcount of at-most-1'. Doc on `sessionTable.Close`
names the invariant; a future caller bypassing the cache would need
to add a refcount then.
Paired cache guard: `sessionTableCache` gains a `closed bool` under
`c.mu`, flipped by `close()`. `getOrOpen`'s slow path re-checks it
before insert and, if set, releases the freshly-opened api and returns
nil so `TableShim` falls back to classic. Prevents the finding-#5 leak.
## Tests
New unit tests (all pass under `-race`):
- `TestSessionTable_Close_CallsBothReleasers`
- `TestSessionTable_Close_NilWriteReleaserOK` (materialized view)
- `TestSessionTable_Close_JoinsErrors`
- `TestSessionTable_Close_ReleasersIdempotent`
- `TestReleaseSessionPool_AfterClientClose_NoOp`
- `TestReleaseSessionPool_MissingKeyNoOp`
- `TestReleaseSessionPool_RemovesEntryAndInvokesUnregister`
- `TestSessionTableCache_ClosedGate_SlowPathInsertNoLeak`
## Drive-by
The pre-existing `closeCountingTable` test helper races between the
sweeper goroutine's `*counter++` and the test-goroutine's read. Switched
`*int` to `*atomic.Int32` so the full-package `-race` sweep is green
(fires on the base branch too — this was blocking my race-stress
verification).
## Test plan
- [x] `go build ./...` and `go vet ./...` clean
- [x] `go test -race -count=1 -short -timeout=120s ./
./internal/session/ ./internal/transport/` — all green
- [x] `TestSessionTable_Close*` and `TestReleaseSessionPool*` — all pass
- [x] `TestSessionTableCache_ClosedGate_SlowPathInsertNoLeak` —
reproduces the race deterministically, passes with the fix
- [x] Sandbox smoke against sushanb-uc1 via `CBT_RUN_SANDBOX=1
CBT_FORCE_SESSION=true`: Table + AV round-trip on both classic and
session paths
## Stack
Stacked on #20263 (session_table_cache) which is stacked on #20262
(session.Client wiring into bigtable.Client Open*). Both must land
first, or this PR must be rebased onto main after they merge.
0 commit comments