feat(bigtable): add Session state enum - #19981
Conversation
Introduce the State type used by the upcoming Session primitive: a small five-value enum (New / Starting / Active / Closing / Closed) with a String() method for logs and a test that pins the numeric ordinals so log scrapers and metric labels stay stable across releases. Sessions move strictly forward through these values; once StateClosed is reached the session is terminal. First in a series of small PRs that together replace the prior all-at-once session-core PR (googleapis#19980).
There was a problem hiding this comment.
Code Review
This pull request introduces a new State type to represent the lifecycle of a Session in the Bigtable transport layer, along with corresponding unit tests to verify string representation and ensure constant ordinals remain stable. The review feedback suggests renaming the package from internal to transport in both files to align with Go package naming conventions and match the directory structure.
| // StateNew indicates the session is newly created and not yet active. | ||
| StateNew State = iota | ||
| // StateStarting indicates the session is dialing and handshaking. | ||
| StateStarting |
There was a problem hiding this comment.
nit: maybe
| StateStarting | |
| StateReady |
?
Also do we need "StateStarting" instead of just "Starting"?
There was a problem hiding this comment.
Renamed to StateReady in 755b90d.
On the State prefix — keeping it on every constant. Package-level Starting / Ready / Closed would collide with common identifiers and lose context at call sites (internal.StateReady reads better than internal.Ready). It's also the convention stringer would generate.
| // StateClosing indicates the session is draining and shutting down. It | ||
| // covers both the pre-CloseSession drain and the post-CloseSession wait | ||
| // for the server's EOF. | ||
| StateClosing |
There was a problem hiding this comment.
I think we need another state WAIT_SERVER_CLOSE? (mirrors the java implementation).
- when client receives GoAway, we transition to Closing.
- if there's still active vRPC, wait for those work to finish before notifying server that it can close this session
- Otherwise notify the server and transition to WAIT_SERVER_CLOSE state.
And we can have a monitoring thread watching for sessions that's stuck in WAIT_SERVER_CLOSE and force close it.
There was a problem hiding this comment.
(go/cbt-jetstream-retry-client-impl for reference)
There was a problem hiding this comment.
Added in 755b90d as StateWaitServerClose. Enum now mirrors Java's six states with matching ordinals (NEW / STARTING / READY / CLOSING / WAIT_SERVER_CLOSE / CLOSED = 0..5).
The transition behavior you described (drain outstanding vRPCs in StateClosing → send CloseSession → transition to StateWaitServerClose → wait for server EOF → terminal) is exactly what splitting the state into two enables. The actual transition code + the WAIT_SERVER_CLOSE watchdog land in the stacked Session lifecycle PR; this PR is just the enum so reviewers can see the vocabulary first.
There was a problem hiding this comment.
Thanks for the link — using go/cbt-jetstream-retry-client-impl as the reference for the lifecycle PR.
Rename StateActive to StateReady and split StateClosing into a client-initiated drain (StateClosing) and a post-CloseSession wait for the server's EOF (StateWaitServerClose). Six states now, matching the Java SessionState enum in google-cloud-java (NEW, STARTING, READY, CLOSING, WAIT_SERVER_CLOSE, CLOSED) so the lifecycle vocabulary and ordinals line up across language clients — telemetry and logs read the same in both. Tests pin the new ordinals (0..5) and string mappings.
Replaces the previous one-shot 30s ForceClose goroutine from handleGoAway with the WAIT_SERVER_CLOSE state machine the Java client uses (and that mutianf asked for in googleapis#19981's SessionState review). Lifecycle now matches the Java reference: New → Starting → Active → Closing → WaitServerClose → Closed - Closing = we've decided to close, draining outstanding vRPCs before sending CloseSession. - WaitServerClose = CloseSession sent; waiting for the server's EOF / trailers to confirm teardown. - Closed = terminal. Session.Close now drives the full sequence (drain → send CloseSession → transition to WaitServerClose) and accepts being called from Closing too, so handleGoAway can hand off to it instead of open-coding a teardown. handleClose accepts the Closed transition from any non-terminal state (including the new WaitServerClose), unchanged behavior in that regard. handleGoAway spawns a goroutine that runs s.Close with a 30s ctx so the drain + CloseSession dance happens off the readLoop. Reason "GoAway" is stamped up-front via the existing CompareAndSwap-once setCloseReason so the eventual handleClose's "StreamEnd" fallback can't overwrite it. New SessionPoolImpl.sweepStuckSessions runs each PerformScaling tick (1Hz) and force-closes any session parked in WaitServerClose for >30s. This bounds the worst case when the server accepts CloseSession but forgets to EOF the stream — without it the session would sit forever, the pool would never retire it, and sessionsClosed / CloseReasons would never bump (which is exactly what the prior sandbox-test snapshot was showing). UI: sessionz colors WaitServerClose the same as Closing (warning amber).
…on sweep Server-sent GoAway transitions the session to Closing and cancels in-flight RPCs beyond lastAdmitted, but the protocol leaves the actual stream close up to the server's eventual EOF. In practice we've observed GoAway'd sessions sit in Closing forever — the pool keeps them in p.sessions, OnClose never fires, and the pool can't replace them. Adds the WAIT_SERVER_CLOSE state the Java client uses (and that mutianf asked for in googleapis#19981's SessionState review) and drives the lifecycle to completion deterministically: New → Starting → Active → Closing → WaitServerClose → Closed - Closing = we've decided to close, draining outstanding vRPCs before sending CloseSession. - WaitServerClose = CloseSession sent; waiting for the server's EOF / trailers to confirm teardown. - Closed = terminal. Session.Close now drives the full sequence (drain → send CloseSession → transition to WaitServerClose) and accepts being called from Closing too so handleGoAway can hand off without open-coding the teardown. handleClose's Closed transition is unchanged (already permitted from any non-terminal state). handleGoAway spawns a goroutine that runs s.Close with a 30s ctx so the drain + CloseSession dance happens off the readLoop, then exits. New SessionPoolImpl.sweepStuckSessions runs on every PerformScaling tick (1Hz) and force-closes any session parked in WaitServerClose past waitServerCloseGrace (30s). This bounds the worst case when the server accepts CloseSession but forgets to EOF the stream. TestState_String updated to cover the new state; TestClose_Graceful… updated to assert WaitServerClose after Send instead of the old Closing.
…am parity Aligns the Session lifecycle enum value name and its String() output with Java's SessionState.READY and with upstream PR googleapis#19981 (0748972). Our fork used "Active" internally since before the naming settled; every other client (Java, upstream Go) uses "Ready" so cross-language telemetry and log-scraping tools can rely on a consistent vocabulary. Mechanical rename across 10 files, 29 references: - Constant StateActive → StateReady in session.go + call sites. - String() output "Active" → "Ready" in session.go and updated matching test-table assertion in session_test.go. - sessionz UI switch cases + state-chip display order. - flightz + sessionz + session_snapshot test fixtures. - Comments referring to "reached StateActive" / "to StateActive". No behavioural change — this is naming only. sessionz JSON responses now emit "state":"Ready" instead of "Active"; downstream scrapers need to update if any.
Adds the Session struct itself along with the two "surface" halves that have no lifecycle behavior of their own — the OTel tracer and the in-process debug counters / event ring. Also adds SessionHandle, the per-session slot the pool checkout / picker code holds. Lifecycle (Start / Close / readLoop / heartBeatLoop) and vRPC (Invoke) come next in the reshaped googleapis#20112. - session.go — Session struct + hooks + NewSession + State transitions (transitionTo, is/notState, signalQuiescent), vrpcResult, sessionErr, unavailable, afeID. Uses the existing State enum from session_state.go (PR googleapis#19981) — no re-declaration. - session_debug.go — embedded sessionDebug: counters (retries, okRpcs, errorRpcs, msgsSent, msgsRecv), per-session event ring, latency histogram, cluster-id map, WithSessionLogger / WithSessionPoolName options, RemoteAddr / SampleUptime / RecordTransportOverhead accessors. Consumes metrics.TransportTypeName (googleapis#20115) and recordDebugTag (googleapis#20114). - session_tracer.go — OTel sessionTracer for per-session + per-attempt metrics (session duration, open latency, uptime, transport-overhead histogram). Consumes metrics.TransportTypeName. - picker.go — SessionHandle wrapping *Session with the per-pick counters (Picks, Outstanding, LastActivity) the pool needs. Its concrete users (SessionPool, AFE picker) come in later PRs; the handle type is here because Session embeds an atomic.Pointer to it. Part 3b/3c of the Session core sub-split. Stacks on bigtable-session-primitives (Part 3a), googleapis#20115 (metrics.TransportTypeName export), and googleapis#20114 (debug tag counter).
Adds the Session struct itself along with the two "surface" halves that have no lifecycle behavior of their own — the OTel tracer and the in-process debug counters / event ring. Also adds SessionHandle, the per-session slot the pool checkout / picker code holds. Lifecycle (Start / Close / readLoop / heartBeatLoop) and vRPC (Invoke) come next in the reshaped googleapis#20112. - session.go — Session struct + hooks + NewSession + State transitions (transitionTo, is/notState, signalQuiescent), vrpcResult, sessionErr, unavailable, afeID. Uses the existing State enum from session_state.go (PR googleapis#19981) — no re-declaration. - session_debug.go — embedded sessionDebug: counters (retries, okRpcs, errorRpcs, msgsSent, msgsRecv), per-session event ring, latency histogram, cluster-id map, WithSessionLogger / WithSessionPoolName options, RemoteAddr / SampleUptime / RecordTransportOverhead accessors. Consumes metrics.TransportTypeName (googleapis#20115) and recordDebugTag (googleapis#20114). - session_tracer.go — OTel sessionTracer for per-session + per-attempt metrics (session duration, open latency, uptime, transport-overhead histogram). Consumes metrics.TransportTypeName. - picker.go — SessionHandle wrapping *Session with the per-pick counters (Picks, Outstanding, LastActivity) the pool needs. Its concrete users (SessionPool, AFE picker) come in later PRs; the handle type is here because Session embeds an atomic.Pointer to it. Part 3b/3c of the Session core sub-split. Stacks on bigtable-session-primitives (Part 3a), googleapis#20115 (metrics.TransportTypeName export), and googleapis#20114 (debug tag counter).
Adds the Session struct itself along with the two "surface" halves that have no lifecycle behavior of their own — the OTel tracer and the in-process debug counters / event ring. Also adds SessionHandle, the per-session slot the pool checkout / picker code holds. Lifecycle (Start / Close / readLoop / heartBeatLoop) and vRPC (Invoke) come next in the reshaped googleapis#20112. - session.go — Session struct + hooks + NewSession + State transitions (transitionTo, is/notState, signalQuiescent), vrpcResult, sessionErr, unavailable, afeID. Uses the existing State enum from session_state.go (PR googleapis#19981) — no re-declaration. - session_debug.go — embedded sessionDebug: counters (retries, okRpcs, errorRpcs, msgsSent, msgsRecv), per-session event ring, latency histogram, cluster-id map, WithSessionLogger / WithSessionPoolName options, RemoteAddr / SampleUptime / RecordTransportOverhead accessors. Consumes metrics.TransportTypeName (googleapis#20115) and recordDebugTag (googleapis#20114). - session_tracer.go — OTel sessionTracer for per-session + per-attempt metrics (session duration, open latency, uptime, transport-overhead histogram). Consumes metrics.TransportTypeName. - picker.go — SessionHandle wrapping *Session with the per-pick counters (Picks, Outstanding, LastActivity) the pool needs. Its concrete users (SessionPool, AFE picker) come in later PRs; the handle type is here because Session embeds an atomic.Pointer to it. Part 3b/3c of the Session core sub-split. Stacks on bigtable-session-primitives (Part 3a), googleapis#20115 (metrics.TransportTypeName export), and googleapis#20114 (debug tag counter).
Adds the Session struct itself along with the two "surface" halves that have no lifecycle behavior of their own — the OTel tracer and the in-process debug counters / event ring. Also adds SessionHandle, the per-session slot the pool checkout / picker code holds. Lifecycle (Start / Close / readLoop / heartBeatLoop) and vRPC (Invoke) come next in the reshaped googleapis#20112. - session.go — Session struct + hooks + NewSession + State transitions (transitionTo, is/notState, signalQuiescent), vrpcResult, sessionErr, unavailable, afeID. Uses the existing State enum from session_state.go (PR googleapis#19981) — no re-declaration. - session_debug.go — embedded sessionDebug: counters (retries, okRpcs, errorRpcs, msgsSent, msgsRecv), per-session event ring, latency histogram, cluster-id map, WithSessionLogger / WithSessionPoolName options, RemoteAddr / SampleUptime / RecordTransportOverhead accessors. Consumes metrics.TransportTypeName (googleapis#20115) and recordDebugTag (googleapis#20114). - session_tracer.go — OTel sessionTracer for per-session + per-attempt metrics (session duration, open latency, uptime, transport-overhead histogram). Consumes metrics.TransportTypeName. - picker.go — SessionHandle wrapping *Session with the per-pick counters (Picks, Outstanding, LastActivity) the pool needs. Its concrete users (SessionPool, AFE picker) come in later PRs; the handle type is here because Session embeds an atomic.Pointer to it. Part 3b/3c of the Session core sub-split. Stacks on bigtable-session-primitives (Part 3a), googleapis#20115 (metrics.TransportTypeName export), and googleapis#20114 (debug tag counter).
Adds the Session struct itself along with the two "surface" halves that have no lifecycle behavior of their own — the OTel tracer and the in-process debug counters / event ring. Also adds SessionHandle, the per-session slot the pool checkout / picker code holds. Lifecycle (Start / Close / readLoop / heartBeatLoop) and vRPC (Invoke) come next in the reshaped googleapis#20112. - session.go — Session struct + hooks + NewSession + State transitions (transitionTo, is/notState, signalQuiescent), vrpcResult, sessionErr, unavailable, afeID. Uses the existing State enum from session_state.go (PR googleapis#19981) — no re-declaration. - session_debug.go — embedded sessionDebug: counters (retries, okRpcs, errorRpcs, msgsSent, msgsRecv), per-session event ring, latency histogram, cluster-id map, WithSessionLogger / WithSessionPoolName options, RemoteAddr / SampleUptime / RecordTransportOverhead accessors. Consumes metrics.TransportTypeName (googleapis#20115) and recordDebugTag (googleapis#20114). - session_tracer.go — OTel sessionTracer for per-session + per-attempt metrics (session duration, open latency, uptime, transport-overhead histogram). Consumes metrics.TransportTypeName. - picker.go — SessionHandle wrapping *Session with the per-pick counters (Picks, Outstanding, LastActivity) the pool needs. Its concrete users (SessionPool, AFE picker) come in later PRs; the handle type is here because Session embeds an atomic.Pointer to it. Part 3b/3c of the Session core sub-split. Stacks on bigtable-session-primitives (Part 3a), googleapis#20115 (metrics.TransportTypeName export), and googleapis#20114 (debug tag counter).
Adds the Session struct itself along with the two "surface" halves that have no lifecycle behavior of their own — the OTel tracer and the in-process debug counters / event ring. Also adds SessionHandle, the per-session slot the pool checkout / picker code holds. Lifecycle (Start / Close / readLoop / heartBeatLoop) and vRPC (Invoke) come next in the reshaped googleapis#20112. - session.go — Session struct + hooks + NewSession + State transitions (transitionTo, is/notState, signalQuiescent), vrpcResult, sessionErr, unavailable, afeID. Uses the existing State enum from session_state.go (PR googleapis#19981) — no re-declaration. - session_debug.go — embedded sessionDebug: counters (retries, okRpcs, errorRpcs, msgsSent, msgsRecv), per-session event ring, latency histogram, cluster-id map, WithSessionLogger / WithSessionPoolName options, RemoteAddr / SampleUptime / RecordTransportOverhead accessors. Consumes metrics.TransportTypeName (googleapis#20115) and recordDebugTag (googleapis#20114). - session_tracer.go — OTel sessionTracer for per-session + per-attempt metrics (session duration, open latency, uptime, transport-overhead histogram). Consumes metrics.TransportTypeName. - picker.go — SessionHandle wrapping *Session with the per-pick counters (Picks, Outstanding, LastActivity) the pool needs. Its concrete users (SessionPool, AFE picker) come in later PRs; the handle type is here because Session embeds an atomic.Pointer to it. Part 3b/3c of the Session core sub-split. Stacks on bigtable-session-primitives (Part 3a), googleapis#20115 (metrics.TransportTypeName export), and googleapis#20114 (debug tag counter).
🤖 I have created a release *beep* *boop* --- ## [1.51.0](bigtable/v1.50.0...bigtable/v1.51.0) (2026-07-23) ### Features * **bigtable:** Add ChainInterceptors and RetryingVRpc for vRPC pipeline ([#20185](#20185)) ([c7a832a](c7a832a)) * **bigtable:** Add ClientConfigurationManager ([#19986](#19986)) ([3a8f927](3a8f927)) * **bigtable:** Add debug tag counter (recordDebugTag / assertDebugTag) ([#20114](#20114)) ([3c97590](3c97590)) * **bigtable:** Add lazyPool helper for on-demand session pool opening ([#20182](#20182)) ([f6ae3fb](f6ae3fb)) * **bigtable:** Add PeakEwma continuous time-decay latency tracker ([#20187](#20187)) ([9d124ef](9d124ef)) * **bigtable:** Add PoolSizer for server-driven session pool capacity ([#20189](#20189)) ([57ebbeb](57ebbeb)) * **bigtable:** Add session package with SessionClient + SessionTableAPI interfaces ([#20180](#20180)) ([4b82fd2](4b82fd2)) * **bigtable:** Add Session primitives (AttemptOutcome, vRPC ctx, msgtype) ([#20116](#20116)) ([e1011e2](e1011e2)) * **bigtable:** Add Session state enum ([#19981](#19981)) ([0748972](0748972)) * **bigtable:** Add SessionThrottler / AdaptiveSessionThrottler for OpenSession pacing ([#20184](#20184)) ([02e3c6d](02e3c6d)) * **bigtable:** Add SessionThrottler / AdaptiveSessionThrottler for OpenSession pacing ([#20184](#20184)) ([29be83e](29be83e)) * **bigtable:** Add sessionTracer for per-Session lifecycle + vRPC metrics ([#20190](#20190)) ([a466345](a466345)) * **bigtable:** Enable new auth library and JWT for instance admin client ([#20013](#20013)) ([21c4a44](21c4a44)) * **bigtable:** Modularize channel priming behind a ChannelPrimer interface ([#20027](#20027)) ([5214ab7](5214ab7)) * **bigtable:** Modularize Direct Access compatibility check ([#19987](#19987)) ([a25e93d](a25e93d)) * **o11y:** Regenerate clients for LRO tracing ([#20107](#20107)) ([779074e](779074e)) ### Bug Fixes * **bigtable:** Default cluster/zone in toOtelMetricAttrs to avoid Monitoring reject ([#20178](#20178)) ([14493f4](14493f4)) * **bigtable:** Eliminate stats-handler MD race in internal/metrics tracer ([#20158](#20158)) ([c387066](c387066)) --- 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>
Summary
First in a series of small PRs that together replace the all-at-once #19980. Introduces just the State enum used by the upcoming Session primitive, modeled on the
SessionStateenum in the Java Bigtable client so vocabulary and ordinals line up across language clients:StateNew,StateStarting,StateReady,StateClosing,StateWaitServerClose,StateClosedStateClosingis the client-initiated drain (beforeCloseSessionis sent);StateWaitServerCloseis the post-CloseSessionwait for the server's EOFString()method for logsphasevaluesSessions move strictly forward through these values; once
StateClosedis reached the session is terminal.What's next (stacked on top)
Start/Send/Close, read loop, heartbeat, frame handlers)Invokepath (vRPC encode → send → await response →InvokeResult)SessionTracer(OTel session-scoped metrics) lands lastTest plan
gofmt,goimports,go vet,staticcheck,revivecleango test ./internal/transport/ -run TestState -count=1passes (bothTestState_StringandTestState_OrdinalsPinned)