feat(bigtable): add SessionClient + SessionTable + lazyPool - #20228
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a proto-native, session-based API surface for the Bigtable Go client, establishing the SessionClient and SessionTableAPI interfaces along with their concrete implementations. It adds robust session pooling, lifecycle management, retry interceptors, and comprehensive debugging and snapshotting capabilities to feed diagnostic pages like sessionz and channelz. The review feedback highlights an opportunity to improve robustness by adding a nil check for the config parameter in SessionPoolImpl.UpdateConfig to prevent potential nil pointer dereferences.
| func (p *SessionPoolImpl) UpdateConfig(config *spb.SessionClientConfiguration_SessionPoolConfiguration) { | ||
| p.m.listenerFires.Add(1) | ||
| p.mu.Lock() | ||
| // Stores stay under p.mu so PoolSnapshot (also under p.mu) reads a | ||
| // consistent min/max pair. Hot-path readers still Load() without | ||
| // the lock — atomic makes both directions safe. | ||
| p.minSessions.Store(int32(config.MinSessionCount)) |
There was a problem hiding this comment.
To prevent potential nil pointer dereferences, add a nil check for config at the beginning of UpdateConfig. If config is nil, the method should return early to avoid panicking when accessing config.MinSessionCount.
func (p *SessionPoolImpl) UpdateConfig(config *spb.SessionClientConfiguration_SessionPoolConfiguration) {
if config == nil {
return
}
p.m.listenerFires.Add(1)
p.mu.Lock()
// Stores stay under p.mu so PoolSnapshot (also under p.mu) reads a
// consistent min/max pair. Hot-path readers still Load() without
// the lock — atomic makes both directions safe.
p.minSessions.Store(int32(config.MinSessionCount))There was a problem hiding this comment.
Addressed in d92edd3 + 283578f. Nil-guard at top of UpdateConfig (after the existing listenerFires counter bump — kept before the guard so debug obs still records the listener-fired signal even on empty payload). Logged via log.Printf so a contract violation shows up in prod stderr the first time it fires. Fires at most once per broken caller, so noise is bounded.
809cc0d to
22d4000
Compare
|
Re gemini nit on session_pool.go:408 (UpdateConfig nil-check) — after the rebase off main (#20225 squash-merged as 683eda8), |
|
Follow-up in Before this commit the session path constructed Details:
Reviewed by |
ClientConfigurationManager only fires listeners on a successful GetClientConfiguration, so config should never be nil in practice. Guard against a future caller that misuses the API — dereferencing config.LoadBalancingOptions on nil would panic. Log-and-bail rather than silent-return so a contract violation shows up in operator logs the same day it lands. listenerFires counter still increments before the guard so debug observability records that the listener was invoked, even on empty payload. Addresses gemini-code-assist review comment on PR googleapis#20228.
|
Reply to gemini's inline nit on Addressed in Logs the contract violation via |
| ) | ||
|
|
||
| // Default pool sizing — same as SessionManager's fallback (10/100). | ||
| const ( |
There was a problem hiding this comment.
Done in 67d7aad. Dropped defaultMinSessions/defaultMaxSessions consts. Server-driven ClientConfigurationManager sets pool bounds via SessionClientConfiguration polls; the client-side 10/100 fallback was dead code. NewSessionPoolImpl already handles min/max<=0 via defaultPoolConfig() at session_pool.go:216-221, so bootstrap behavior is preserved with a single source of truth.
|
|
||
| // MinSessions / MaxSessions are per-pool bounds. Zero uses | ||
| // defaults (10/100). | ||
| MinSessions int |
There was a problem hiding this comment.
Done in 67d7aad. Dropped Config.MinSessions/MaxSessions fields. Same rationale as the defaults const removal.
| // per-connection error histograms, dynamic scaling, and periodic | ||
| // connection replacement they get on the classic path. Nil for the | ||
| // test factory (newSessionClientFromParts with a fake pool). | ||
| dsm *btransport.DynamicScaleMonitor |
There was a problem hiding this comment.
Is there newManagedChannelPool()
There was a problem hiding this comment.
Done in d8d5ecf. Session client now calls btransport.CreateAndStartManagedChannelPool (the shared helper). ~100 LOC removed from NewSessionClient. sessionClient.dsm/connRecycler collapsed to single sessionClient.managedPool field; Close delegates to sc.managedPool.Close(). Session's no-primer behavior preserved via a new ChannelPoolConfig.SkipChannelPrimer bool (skips WithChannelPrimer on the pool AND passes nil primer to the DAC — matches pre-refactor semantic).
There was a problem hiding this comment.
Update: reversed the shared-helper approach in 84f973e1b2 per your directive this session — session client now hand-rolls NewBigtableChannelPool + NewDynamicScaleMonitor + NewConnectionRecycler construction so no config knobs (SkipChannelPrimer bool, primer override, DAC factory closure) leak into the classic ChannelPoolConfig.
The factory you asked about is now added — btransport.NewManagedChannelPool(pool, dsm, connRecycler) in channel_pool_factory.go — and both the shared CreateAndStartManagedChannelPool helper and the session client's hand-rolled path use it to bundle the three fields. Keeps the "three ship together" invariant enforced at one construction site.
Session-specific plumbing at the call site:
WithChannelPrimer(NoOpChannelPrimer{})— no eager PingAndWarm; channels warm on-demand via OpenSession streams.WithDirectAccessChecker(NewSessionClientDirectAccessChecker(...))— the export from feat(bigtable): add getClientConfigDirectAccessChecker for session pools #20209, return type widened to theDirectAccessCheckerinterface.
| // resolveConnPoolSize walks opts for a caller-supplied gRPC connection | ||
| // pool size, falling back to defaultChannelPoolSize when unavailable. | ||
| // Mirrors the same-shaped logic in bigtable/channel_pool_factory.go. | ||
| func resolveConnPoolSize(opts []option.ClientOption, fallback int) int { |
There was a problem hiding this comment.
this is also used in classic client. usea helper.
There was a problem hiding this comment.
Done in d8d5ecf. resolveConnPoolSize + defaultChannelPoolSize dropped; shared btransport.CreateAndStartManagedChannelPool handles pool sizing internally (defaultBigtableConnPoolSize=10 in the transport pkg, resolved via internaloption.NewUnsafeResolver). 2 unit tests dropped along with the function.
| return nil, fmt.Errorf("proto.Marshal session payload: %w", err) | ||
| } | ||
| handshake := &btpb.OpenSessionRequest{ | ||
| ProtocolVersion: 1, |
There was a problem hiding this comment.
create a const saying ProtocolVersion,
There was a problem hiding this comment.
Done in b984ebb. Added const sessionProtocolVersion = 1 and use it in the OpenSessionRequest envelope.
| att.SetClusterID(result.ClusterInfo.ClusterId) | ||
| att.SetZoneID(result.ClusterInfo.ZoneId) | ||
| } | ||
| if !result.SentAt.IsZero() && !att.StartTime().IsZero() { |
There was a problem hiding this comment.
mentio that it is attmeptStartTime - bidi SentAt time.
There was a problem hiding this comment.
Done in b984ebb. Added a comment: ClientBlockingLatency = SentAt - AttemptStartTime — elapsed time from attempt-start to when the request was Sent on the bidi stream. Analogous to gax's per-attempt blocking latency on the classic unary path.
| att.SetClientBlockingLatency(metrics.ConvertToMs(result.SentAt.Sub(att.StartTime()))) | ||
| } | ||
| if result.Stats != nil && result.Stats.BackendLatency != nil { | ||
| att.SetServerLatency(metrics.ConvertToMs(result.Stats.GetBackendLatency().AsDuration())) |
There was a problem hiding this comment.
just set it to zero for session.
There was a problem hiding this comment.
Held for clarification — session-reviewer flagged this as a CLIENT_SIDE_METRICS_SPEC #2 violation (spec cites this exact site as the session-path server_latencies source), and igor-reviewer noted tracer.go:693 uses serverLatency == 0 as part of connectivity_error_count classification, so a hard zero can misclassify. Grep didn't find a second populator that would double-count. Two questions: (1) is there another populator on the session path that would double-count if we keep the current stamp? (2) if we set to zero, do we also need to update CLIENT_SIDE_METRICS_SPEC.md #2 to reflect the new semantic? Happy to land whichever direction, just want the intent pinned first.
There was a problem hiding this comment.
Done in 25c75f8 per your PeerInfo guidance. Three coordinated pieces:
-
internal/metrics/tracer.go:connectivity_error_countclassifier now has a third prong —transportType != ""(from PeerInfo). Three-way OR: (PeerInfo || server-timing header || location header) → NOT a connectivity error. Java parity confirmed againstBuiltinMetricsTracer.java:400-411(Java gates seenServer on PeerInfo + gfeTiming; Go's three-prong is strictly more lenient). -
internal/session/table.go:stampAttemptnow callsSetServerLatency(0)unconditionally. BackendLatency deliberately unused — not wire-comparable to classic's server-timing value; session AFE overhead lives intransport_latenciesseparately. -
Specs updated:
CLIENT_SIDE_METRICS_SPEC.mddatastore: Support querying multiple kinds #2 (server_latencies bullet + new connectivity_error_count interlock bullet + note that classic-path also benefits from the third prong) andSESSION_COMPONENT_SPEC.mdPart C row 222 synced.
Follow-up worth flagging: the server_latencies histogram will now receive 0-valued samples for every session attempt (session-reviewer flagged this). If dashboards need to skip session data on this metric, we can add a serverLatency > 0 gate at the record site as a small follow-up.
|
|
||
| // WithInstanceName tags the pool with the fully-qualified Bigtable instance | ||
| // name (e.g. "projects/{proj}/instances/{inst}") for display in channelz. | ||
| func WithInstanceName(instanceName string) BigtableChannelPoolOption { |
There was a problem hiding this comment.
no need to show this in sesszionz ui so remove.
There was a problem hiding this comment.
Done in f4dbc5b — WithInstanceName removed along with the underlying field.
|
|
||
| // WithAppProfile tags the pool with the app profile ID for display in | ||
| // channelz. | ||
| func WithAppProfile(appProfile string) BigtableChannelPoolOption { |
There was a problem hiding this comment.
Done in f4dbc5b — WithAppProfile removed along with the underlying field.
| // instanceName / appProfile are display-only identity, surfaced by | ||
| // ChannelPoolSnapshot so channelz can label the pool. Set at | ||
| // construction via WithInstanceName / WithAppProfile. | ||
| instanceName string |
There was a problem hiding this comment.
you can remove this field.
There was a problem hiding this comment.
Done in f4dbc5b. Removed instanceName + appProfile fields on BigtableChannelPool + WithInstanceName/WithAppProfile setters (unused since the shared factory refactor stopped passing them). Also removed the corresponding ChannelPoolSnapshot.InstanceName/AppProfile fields — no read-side consumer in-repo, so they were dead labels. Instance name + app profile still flow through the PingAndWarm primer and per-vRPC request-params headers unchanged.
…go + client.go - MaxAttempts 10 → 3 in session-table retry interceptor (Java parity — RetryingVRpc.java:309). Test cap updated to match; dropped stale TODO block that flagged the divergence as a follow-up. - Dropped 2 observation-only RecordDebugTag calls in stampAttempt (TagSessionAttemptNilClusterInfo, TagSessionAttemptEmptyClusterID) — the diagnosis they were meant to confirm has landed and the tags are no longer wanted. - Comment on SetClientBlockingLatency explaining the semantic: SentAt - AttemptStartTime = elapsed time from attempt-start to when the request was Sent on the bidi stream. - New sessionProtocolVersion = 1 const used in OpenSessionRequest envelope (replaces the literal). Bump on non-backwards-compatible wire-shape changes. Addresses inline review comments on PR googleapis#20228 at table.go:191/195/267/ 269/279 and client.go:595. Fifth outstanding nit (session/table.go:283 SetServerLatency=0) held for clarification — reviewers flagged it as a CLIENT_SIDE_METRICS_SPEC #2 violation.
…nd defaults Addresses sushanb inline review nits on PR googleapis#20228: - lazy_pool.go: SessionPool interface removed. Had a single production impl (*btransport.SessionPoolImpl) and zero test fakes — pure indirection. Invoker interface stays (has 3 test fakes: fakeInvoker, stubInvoker, blockingInvoker). - client.go: Config.MinSessions/MaxSessions fields removed + defaultMinSessions/defaultMaxSessions consts removed. Server-driven ClientConfigurationManager sets these authoritatively via SessionClientConfiguration polls; the client-side 10/100 fallback was dead code duplicated with transport's defaultPoolConfig(). NewSessionPoolImpl already handles min/max<=0 via defaultPoolConfig() (see session_pool.go:216-221), so bootstrap behavior is preserved with a single source of truth. - getOrCreatePool signature simplified: dropped min/max params; return type changed from SessionPool interface to concrete *btransport.SessionPoolImpl. Behavioral shift: pre-first-UpdateConfig pool floor is now 5 (from default_client_config.go) instead of the removed 10 — no test or dashboard depends on the old value.
…hannelPool Addresses sushanb inline nits on PR googleapis#20228 at client.go:181 ("Is there newManagedChannelPool()") and client.go:404 ("this is also used in classic client. use a helper"). Session client's hand-rolled pool wiring (dial + Prime + NewBigtableChannelPool + explicit DynamicScaleMonitor.Start + ConnectionRecycler.Start + monitor Stop in Close) collapsed to a single call to btransport.CreateAndStartManagedChannelPool — the exact helper the classic path uses. Consequences: - ~100 LOC removed from NewSessionClient body. - resolveConnPoolSize helper + defaultChannelPoolSize const dropped (shared helper handles pool sizing). 2 unit tests dropped. - sessionClient.dsm/connRecycler fields collapsed to a single sessionClient.managedPool btransport.ManagedChannelPool field. Close() now delegates to sc.managedPool.Close() (stops DSM + Recycler then closes pool). Test-fake path (managedPool.Pool==nil) falls back to sc.channelPool.Close(). Session-specific behavior preserved via a new ChannelPoolConfig.SkipChannelPrimer bool: - Session channels warm on-demand via OpenSession bidi streams — no need for eager PingAndWarm on every sub-channel. - CreateBigtableChannelPool skips WithChannelPrimer AND passes nil primer to the DAC when set, so the DAC probe relies on the ALTS handshake alone (matches pre-refactor behavior). Reviewed by session-reviewer + session-component-review + igor-reviewer — all pass.
…ChannelPool Addresses sushanb inline nits on PR googleapis#20228 at connpool.go:467 ("you can remove this field") and :476/:484 ("no need to show this in sessionz ui so remove"). - BigtableChannelPool.instanceName / .appProfile fields: removed. No caller sets them (WithInstanceName / WithAppProfile were unused after the shared CreateBigtableChannelPool refactor stopped passing them), so p.instanceName == "" and p.appProfile == "" on every pool. - WithInstanceName / WithAppProfile option setters: removed. - ChannelPoolSnapshot.InstanceName / .AppProfile fields: removed. No read-side consumer in-repo; they were dead labels. Instance name + app profile still flow into the PingAndWarm primer (via ChannelPrimer) and into per-vRPC request-params headers — those paths are unchanged. Only the pool-level display copies were dead.
Addresses sushanb inline nit on PR googleapis#20228: the type is used as session.SessionClient externally, which stutters. Renamed to match the same "no package-name stutter" style as the earlier session.TableAPI rename. Renamed: - SessionClient interface → Client (session.Client) - NewSessionClient constructor → NewClient (session.NewClient) - ErrSessionClientClosed sentinel → ErrClientClosed (message text updated too — "bigtable/session: Client is closed") Unchanged: - Private sessionClient struct (unexported — no external stutter) - newSessionClientFromParts helper (unexported) - Docstring / comment prose that references "SessionClient" as a concept was left as-is; only exact type/function references got the rename. Kept SESSION_CLIENT_SPEC.md type refs synced. - Proto-generated SessionClientConfiguration and friends (wire-defined, unrelated). Also updates SESSION_CLIENT_SPEC.md so its two type-name citations (session.NewSessionClient, session.SessionClient) match the new symbols.
|
Follow-up in 5023e0c: renamed |
| // attempt's cluster_id label defaults to <unspecified> because | ||
| // stampAttempt has nothing to stamp AND session path has no per-vRPC | ||
| // gRPC headers for ExtractLocation to fall back on. | ||
| TagSessionAttemptNilClusterInfo DebugTag = "session_attempt_nil_cluster_info" |
There was a problem hiding this comment.
Done in 1a1d1b3. Right — Batch 1 removed both emission sites (in session/table.go's stampAttempt), so this const was dead. Deleted the const plus its sibling TagSessionAttemptEmptyClusterID, plus the DebugTag type and RecordDebugTag exported function (that whole exported chain existed only to let stampAttempt fire these tags across the package boundary; no other callers). Also removed the upstream tagSessionPoolCheckoutFailedCINil + its emission — its rationale (attribute downstream nil-ClusterInfo) collapsed with the downstream tags.
| // vRPC responses per CLIENT_SIDE_METRICS_SPEC #1). Companion to | ||
| // TagSessionAttemptNilClusterInfo; distinct so ops can tell | ||
| // "server didn't respond" from "server responded without cluster". | ||
| TagSessionAttemptEmptyClusterID DebugTag = "session_attempt_empty_cluster_id" |
There was a problem hiding this comment.
please clean this up
There was a problem hiding this comment.
Done in 1a1d1b3 along with the sibling nil-ClusterInfo tag — see the reply on that thread for the full cleanup scope.
…hain Addresses sushanb inline nits on PR googleapis#20228 at debug_tracer.go:259 ("why is this needed?") and :267 ("please clean this up"). Both tags were emitted only from stampAttempt call sites that Batch 1 removed; the catalog constants are dead. Removed: - TagSessionAttemptNilClusterInfo + TagSessionAttemptEmptyClusterID const declarations (dead — no emitters after Batch 1). - DebugTag type + RecordDebugTag(DebugTag) exported function (only existed to type-parameterize the RecordDebugTag call from internal/session's stampAttempt; that caller is gone, so no out-of-package consumers remain). - tagSessionPoolCheckoutFailedCINil const + its emission at SessionPoolImpl.Invoke's checkout-failure exit — this tag's whole rationale was "attribute downstream nil-ClusterInfo to pool checkout failure"; that rationale collapsed with the downstream tag removal. Preserved: - The p.recordCheckoutFailure(...) call on the checkout-failure exit, so pool-exhaustion incidents still surface in sessionz's slow-vRPC table + latency histograms. - The unexported recordDebugTag helper — still used by the other in-package tags in the catalog. Reviewed by session-reviewer + session-component-review + igor-reviewer — all pass.
Third of five PRs porting the session pool infrastructure from feat/bigtable-sessionz-debug. Stacks on googleapis#20225 (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 - internal/session/table.go — SessionTable impl with lazy read/write pools, per-attempt metrics stamping - internal/session/lazy_pool.go — Invoker + SessionPool interfaces, open-on-first-use lazy pool wrapper - internal/session/debug.go — DebugAccess impl surfacing pool snapshots Transport package additions to support the above: - transport/debug_api.go — SessionDebugProvider / ChannelDebugProvider / ConfigDebugProvider interfaces + ChannelPoolDebug / SessionRef DTOs (lives in transport, not bigtable, so both bigtable.Client and internal/session.SessionClient can implement without an import cycle) - transport/diverter.go — sessionPicks/classicPicks counters + DiverterSnapshot + Snapshot() method - transport/connpool.go — ChannelSnapshot + ChannelPoolSnapshot type + ChannelPoolSnapshot() method + WithInstanceName / WithAppProfile options for channelz labelling - transport/debug_tracer.go — exported DebugTag type + RecordDebugTag + TagSessionAttemptNilClusterInfo / TagSessionAttemptEmptyClusterID catalog constants - transport/session_descriptors.go — SessionType.ProtoName() for human-readable pool identifiers - transport/direct_access_checker.go — renames newPingAndWarmDirectAccessChecker to NewPingAndWarmDirectAccessChecker so the session package can construct one across the package boundary; nil-guards the primer.Prime call so session-based clients (which warm channels on-demand via OpenSession, not eagerly at pool-init) can pass a nil primer 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 instead of misleading errReadPoolNil / ErrWriteNotSupported.
Consumers referenced the type as session.SessionTableAPI which triggers the standard "stutters — consider TableAPI" linter warning. Renamed to session.TableAPI (interface + all implementations + all references in comments and test names).
…on client Session client's NewBigtableChannelPool call was missing WithMetricsReporterConfig, so connection_pool/outstanding_rpcs and per_connection_error_count silently dropped for every session-path RPC. Same call also skipped starting DynamicScaleMonitor and ConnectionRecycler, which the classic path gets from createAndStartManagedChannelPool. Add all three, store DSM + Recycler on sessionClient, Stop them in Close between backgroundCancel and channelPool.Close. Hoist ValidateDynamicConfig above pool construction for fail-fast. Started against caller ctx (matching classic) because every DSM/Recycler action goes through pool methods that already observe poolCtx — running the tickers against an unrelated background ctx would create zombie ticks. Duplicated (rather than shared with createAndStartManagedChannelPool) because internal/session can't import bigtable due to the import-cycle boundary. Comment explains.
ClientConfigurationManager only fires listeners on a successful GetClientConfiguration, so config should never be nil in practice. Guard against a future caller that misuses the API — dereferencing config.LoadBalancingOptions on nil would panic. Log-and-bail rather than silent-return so a contract violation shows up in operator logs the same day it lands. listenerFires counter still increments before the guard so debug observability records that the listener was invoked, even on empty payload. Addresses gemini-code-assist review comment on PR googleapis#20228.
btopt.Debugf(nil, ...) only emits when CBT_ENABLE_DEBUG=true, which would leave a "should never happen" contract violation invisible in prod. Swap for log.Printf so a broken ClientConfigurationManager caller surfaces on stderr the first time it fires — which is the whole point of adding the guard. Fires at most once per broken caller, so noise is bounded. Follow-up on d92edd3 addressing sushanb-reviewer feedback.
…go + client.go - MaxAttempts 10 → 3 in session-table retry interceptor (Java parity — RetryingVRpc.java:309). Test cap updated to match; dropped stale TODO block that flagged the divergence as a follow-up. - Dropped 2 observation-only RecordDebugTag calls in stampAttempt (TagSessionAttemptNilClusterInfo, TagSessionAttemptEmptyClusterID) — the diagnosis they were meant to confirm has landed and the tags are no longer wanted. - Comment on SetClientBlockingLatency explaining the semantic: SentAt - AttemptStartTime = elapsed time from attempt-start to when the request was Sent on the bidi stream. - New sessionProtocolVersion = 1 const used in OpenSessionRequest envelope (replaces the literal). Bump on non-backwards-compatible wire-shape changes. Addresses inline review comments on PR googleapis#20228 at table.go:191/195/267/ 269/279 and client.go:595. Fifth outstanding nit (session/table.go:283 SetServerLatency=0) held for clarification — reviewers flagged it as a CLIENT_SIDE_METRICS_SPEC #2 violation.
…nd defaults Addresses sushanb inline review nits on PR googleapis#20228: - lazy_pool.go: SessionPool interface removed. Had a single production impl (*btransport.SessionPoolImpl) and zero test fakes — pure indirection. Invoker interface stays (has 3 test fakes: fakeInvoker, stubInvoker, blockingInvoker). - client.go: Config.MinSessions/MaxSessions fields removed + defaultMinSessions/defaultMaxSessions consts removed. Server-driven ClientConfigurationManager sets these authoritatively via SessionClientConfiguration polls; the client-side 10/100 fallback was dead code duplicated with transport's defaultPoolConfig(). NewSessionPoolImpl already handles min/max<=0 via defaultPoolConfig() (see session_pool.go:216-221), so bootstrap behavior is preserved with a single source of truth. - getOrCreatePool signature simplified: dropped min/max params; return type changed from SessionPool interface to concrete *btransport.SessionPoolImpl. Behavioral shift: pre-first-UpdateConfig pool floor is now 5 (from default_client_config.go) instead of the removed 10 — no test or dashboard depends on the old value.
…n-path ServerLatency
Addresses sushanb inline review nit at session/table.go:283 ("just set
it to zero for session") + the guidance that PeerInfo presence is the
authoritative "reached server" signal.
Three coordinated changes:
1. internal/metrics/tracer.go: connectivity_error_count classifier
grew a third prong. If ANY of (PeerInfo transport_type,
server-timing header, location header) is present -> NOT a
connectivity error. Session-path serverLatency=0 by design would
otherwise misclassify every session attempt.
2. internal/session/table.go: stampAttempt now calls
SetServerLatency(0) unconditionally on session path.
result.Stats.BackendLatency is deliberately unused - not
wire-comparable to classic's server-timing-header value (session
AFE overhead lives in transport_latencies separately).
3. Specs updated:
- CLIENT_SIDE_METRICS_SPEC.md server_latencies session-source
bullet + new connectivity_error_count interlock bullet + note
that classic-path also gets the (correct) three-prong benefit.
- SESSION_COMPONENT_SPEC.md Part C row 222 synced to the new
session semantic.
Java parity: BuiltinMetricsTracer.java:400-411 gates seenServer on
PeerInfo + gfeTiming; Go's three-prong OR is strictly more lenient.
…hannelPool Addresses sushanb inline nits on PR googleapis#20228 at client.go:181 ("Is there newManagedChannelPool()") and client.go:404 ("this is also used in classic client. use a helper"). Session client's hand-rolled pool wiring (dial + Prime + NewBigtableChannelPool + explicit DynamicScaleMonitor.Start + ConnectionRecycler.Start + monitor Stop in Close) collapsed to a single call to btransport.CreateAndStartManagedChannelPool — the exact helper the classic path uses. Consequences: - ~100 LOC removed from NewSessionClient body. - resolveConnPoolSize helper + defaultChannelPoolSize const dropped (shared helper handles pool sizing). 2 unit tests dropped. - sessionClient.dsm/connRecycler fields collapsed to a single sessionClient.managedPool btransport.ManagedChannelPool field. Close() now delegates to sc.managedPool.Close() (stops DSM + Recycler then closes pool). Test-fake path (managedPool.Pool==nil) falls back to sc.channelPool.Close(). Session-specific behavior preserved via a new ChannelPoolConfig.SkipChannelPrimer bool: - Session channels warm on-demand via OpenSession bidi streams — no need for eager PingAndWarm on every sub-channel. - CreateBigtableChannelPool skips WithChannelPrimer AND passes nil primer to the DAC when set, so the DAC probe relies on the ALTS handshake alone (matches pre-refactor behavior). Reviewed by session-reviewer + session-component-review + igor-reviewer — all pass.
…ChannelPool Addresses sushanb inline nits on PR googleapis#20228 at connpool.go:467 ("you can remove this field") and :476/:484 ("no need to show this in sessionz ui so remove"). - BigtableChannelPool.instanceName / .appProfile fields: removed. No caller sets them (WithInstanceName / WithAppProfile were unused after the shared CreateBigtableChannelPool refactor stopped passing them), so p.instanceName == "" and p.appProfile == "" on every pool. - WithInstanceName / WithAppProfile option setters: removed. - ChannelPoolSnapshot.InstanceName / .AppProfile fields: removed. No read-side consumer in-repo; they were dead labels. Instance name + app profile still flow into the PingAndWarm primer (via ChannelPrimer) and into per-vRPC request-params headers — those paths are unchanged. Only the pool-level display copies were dead.
Addresses sushanb inline nit on PR googleapis#20228: the type is used as session.SessionClient externally, which stutters. Renamed to match the same "no package-name stutter" style as the earlier session.TableAPI rename. Renamed: - SessionClient interface → Client (session.Client) - NewSessionClient constructor → NewClient (session.NewClient) - ErrSessionClientClosed sentinel → ErrClientClosed (message text updated too — "bigtable/session: Client is closed") Unchanged: - Private sessionClient struct (unexported — no external stutter) - newSessionClientFromParts helper (unexported) - Docstring / comment prose that references "SessionClient" as a concept was left as-is; only exact type/function references got the rename. Kept SESSION_CLIENT_SPEC.md type refs synced. - Proto-generated SessionClientConfiguration and friends (wire-defined, unrelated). Also updates SESSION_CLIENT_SPEC.md so its two type-name citations (session.NewSessionClient, session.SessionClient) match the new symbols.
…hain Addresses sushanb inline nits on PR googleapis#20228 at debug_tracer.go:259 ("why is this needed?") and :267 ("please clean this up"). Both tags were emitted only from stampAttempt call sites that Batch 1 removed; the catalog constants are dead. Removed: - TagSessionAttemptNilClusterInfo + TagSessionAttemptEmptyClusterID const declarations (dead — no emitters after Batch 1). - DebugTag type + RecordDebugTag(DebugTag) exported function (only existed to type-parameterize the RecordDebugTag call from internal/session's stampAttempt; that caller is gone, so no out-of-package consumers remain). - tagSessionPoolCheckoutFailedCINil const + its emission at SessionPoolImpl.Invoke's checkout-failure exit — this tag's whole rationale was "attribute downstream nil-ClusterInfo to pool checkout failure"; that rationale collapsed with the downstream tag removal. Preserved: - The p.recordCheckoutFailure(...) call on the checkout-failure exit, so pool-exhaustion incidents still surface in sessionz's slow-vRPC table + latency histograms. - The unexported recordDebugTag helper — still used by the other in-package tags in the catalog. Reviewed by session-reviewer + session-component-review + igor-reviewer — all pass.
… GetClientConfig DAC
The session channel pool needs (1) no eager PingAndWarm on each sub-
channel — channels warm on-demand via OpenSession bidi streams — and
(2) a direct-access compatibility probe that speaks GetClientConfiguration
(the RPC session pools already run in production) instead of PingAndWarm.
The classic CreateAndStartManagedChannelPool helper stays untouched —
no SkipChannelPrimer flag or DirectAccessCheckerFactory closure added.
Instead session.NewClient hand-rolls the four-step assembly (pool +
DSM + ConnRecycler + ManagedChannelPool bundle) with explicit
WithChannelPrimer(NoOpChannelPrimer{}) + WithDirectAccessChecker(
NewSessionClientDirectAccessChecker(...)) options.
Wiring changes:
- Export NewSessionClientDirectAccessChecker (return type widened to
the DirectAccessChecker interface) so session/ can plug it in.
- Add NewManagedChannelPool factory in channel_pool_factory.go so the
session client's hand-rolled path bundles pool + DSM + recycler via
the same shape the shared helper uses; keeps the "three ship
together" invariant enforced at one site.
- session.NewClient pool-size resolution mirrors the classic helper
(internaloption.NewUnsafeResolver → defaultSessionChannelPoolSize
fallback).
No behavioral change to the classic path or to session lifecycle,
picker, sessionList, Diverter, hooks, retry oracle, or per-attempt
metrics stamps.
1a1d1b3 to
84f973e
Compare
| - **Session source:** `result.Stats.BackendLatency` — a typed `google.protobuf.Duration` on `SessionRequestStats` (part of the vRPC response frame). `stampAttempt` converts and stamps: `att.SetServerLatency(ConvertToMs(result.Stats.GetBackendLatency().AsDuration()))` (`internal/session/table.go:245-247`). | ||
| - **Nil-guard:** stamp is skipped when `Stats == nil` OR `Stats.BackendLatency == nil` — server MAY omit either (typically on error frames). The session path has NO t4t7-equivalent fallback; if a fallback becomes necessary, add it here rather than on the classic side. | ||
| - **Session source:** `stampAttempt` stamps `att.SetServerLatency(0)` unconditionally on session path (`internal/session/table.go`). `result.Stats.BackendLatency` (a typed `google.protobuf.Duration` on `SessionRequestStats`) captures server-side compute time, but it is NOT wire-comparable to classic's server-timing-header value — session-path AFE overhead is measured separately via `transport_latencies`. Populating `server_latencies` from BackendLatency would produce a wrong-shape sample that dashboards cross-comparing session/classic would misread. | ||
| - **connectivity_error_count interlock:** because session-path `serverLatency == 0` by design, the `connectivity_error_count` classifier at `internal/metrics/tracer.go` MUST additionally check `transportType != ""` (populated by `stampAttempt` from `result.PeerInfo`). PeerInfo presence proves the `OpenSessionResponse` arrived, i.e., we reached a server; without this prong every session attempt would misclassify. See the tracer's connectivity_error block for the three-prong OR gate (PeerInfo || server-timing header || location header). Java parity: `BuiltinMetricsTracer.java:400-411` gates its `seenServer` bit on PeerInfo + gfeTiming — Go's three-prong OR is strictly more lenient (adds the location header). |
There was a problem hiding this comment.
remove the java reference.
There was a problem hiding this comment.
Done in 9f59e0bd4a. Dropped the Java-parity sentence. The Go-side anchor on the preceding sentence (internal/metrics/tracer.go) is intact — the three-prong OR gate description itself is unchanged.
…path
primer is never nil in CreateBigtableChannelPool (built unconditionally
above), and the SkipChannelPrimer config field never landed — session
client bypasses this helper entirely with NoOpChannelPrimer{}. Comment
was a leftover fragment from the rejected config-knob approach.
| // Standard gRPC routing headers — duplicated from the bigtable package | ||
| // constants (package boundary means we can't import them). Keep the | ||
| // values in sync with bigtable/doc.go. | ||
| const ( |
There was a problem hiding this comment.
can this be shared with classic client?
There was a problem hiding this comment.
Done in 9f59e0bd4a. Moved to a shared home:
- New
bigtable/internal/transport/headers.goexportsResourcePrefixHeader+RequestParamsHeader. - Session package's local
resourcePrefixHeader/requestParamsHeaderconsts now aliasbtransport.ResourcePrefixHeader/btransport.RequestParamsHeader— call sites unchanged, single source of truth. connpool.godropped its own lowercaserequestParamsHeaderand switched its caller to the exported one.
Top-level bigtable package (doc.go) still keeps its own copies for back-compat; noted as an out-of-scope follow-up to alias those too.
| return sc | ||
| } | ||
|
|
||
| // buildFeatureFlagsMD mirrors bigtable.createFeatureFlagsMD. Duplicated |
There was a problem hiding this comment.
note that CBT_FORCE_SESSION is for internal proejcts nly.
There was a problem hiding this comment.
Done in 9f59e0bd4a. Comment now says "Google-internal only, gated server-side" so the env var isn't mistaken for a user-facing knob.
- Shared header consts: new internal/transport/headers.go exports ResourcePrefixHeader + RequestParamsHeader. Session package's local consts now alias the transport exports so session + transport share one source; connpool.go's private lowercase copy dropped. Top-level bigtable package keeps its own copies for back-compat (out of scope). - CBT_FORCE_SESSION doc: note it is Google-internal only, gated server-side. - CLIENT_SIDE_METRICS_SPEC.md: drop unnecessary Java-parity aside on the connectivity_error three-prong OR; the Go-side citation on the preceding sentence is the load-bearing anchor.
| // every session attempt would misclassify as a connectivity | ||
| // error. | ||
| // 2. Server latency (from server-timing header) — classic unary | ||
| // signal; absent in DirectPath and always 0 on session path. |
There was a problem hiding this comment.
is this right? why is it 0 on session path? session path could go through DP so server-timing would be absent in that case?
There was a problem hiding this comment.
Deferred to a follow-up. Dropped the explicit SetServerLatency(0) call on the session path in fe6cc085f9 (session/table.go), but the emission gate at tracer.go:682 is if serverLatencyErr == nil { emit(serverLatency) } — session zero-values both fields so the histogram STILL receives a 0 sample today. The connectivity_error_count classifier is unaffected (the PeerInfo prong is the load-bearing check for session attempts). Follow-up will add a serverLatencySet bool flag on AttemptTracer and gate emission on serverLatencySet && serverLatencyErr == nil so session attempts truly don't contribute to server_latencies. Kept out of this PR because the tracer is shared with classic and deserves its own review pass.
Your DP concern is worth calling out separately: on classic DP the extraction fails → serverLatencyErr != nil → emission is skipped (that path is intentional). Session's issue is that we've made the field structurally absent but the gate doesn't yet know that.
There was a problem hiding this comment.
DP fallback is fine. server_latency is supposed to be a per attempt metric so it doesn't make sense in the session protocol context.
There was a problem hiding this comment.
Thanks for confirming — will keep the DP-fallback behaviour as-is. The serverLatencySet follow-up (to actually leave server_latencies absent on session attempts instead of stamping a 0-valued sample via the tracer's zero-value default) is still on my list; that'll land in a separate tracer-scoped PR since it touches shared classic + session tracer code.
| // identified by the leaf table name (e.g. "my-table"). Full | ||
| // resource composition happens inside the implementation. | ||
| OpenSessionTable(tableName string) TableAPI | ||
| OpenSessionTable(tableID string) TableAPI |
There was a problem hiding this comment.
nit: maybe change to OpenTable since we changed the API from SessionTableAPI to TableAPI
There was a problem hiding this comment.
Done in fe6cc085f9. Renamed SessionClient.OpenSessionTable → OpenTable (session/api.go + client.go + client_test.go + SESSION_CLIENT_SPEC.md). No bigtable.Client.OpenTable collision because different package + different receiver.
| // booleans of the same name; both propagate into FeatureFlags on | ||
| // every OpenSessionRequest. | ||
| MetricsEnabled bool | ||
| DisableRetryInfo bool |
There was a problem hiding this comment.
these can probably be part of the FeatureFlagMD instead of separate configs?
There was a problem hiding this comment.
Held (with tradeoff): both MetricsEnabled and DisableRetryInfo do end up in FeatureFlags bits, but the Config struct carries the booleans and buildFeatureFlagsMD renders them because internal/session can't import the top-level bigtable package (import cycle) — the top-level createFeatureFlagsMD is the shared source of truth for the render rules and we intentionally mirror it here rather than accept a pre-built MD from callers. Happy to revisit if you'd prefer callers hand session a pre-built FeatureFlagsMD metadata.MD and drop the two bools + the mirror — that would push the render responsibility outside the session package entirely.
|
|
||
| const ( | ||
| permissionRead permission = iota | ||
| permissionWrite |
There was a problem hiding this comment.
there's also a permissinoReadWrite (for checkAndMutate and ReadModifyWrite), not supported by server yet, but we'll need in the future. We can add it now or later.
There was a problem hiding this comment.
Held for now — footgun risk. If we add a permissionReadWrite enum today, someone could route a pool through it before the server side accepts a session opened at that permission level, and the CheckAndMutate/ReadModifyWrite pool-open would fail. Preference is to add the enum value in the same PR that lands the wire support so the whole path is exercised end-to-end. Enum ordering isn't wire-encoded so appending later is free. If you'd like a placeholder + panic guard in createSessionPoolForPayload as a compromise, happy to add.
| // GetClientConfiguration (the same RPC ClientConfigurationManager | ||
| // polls) instead of PingAndWarm, matching the actual on-wire RPC | ||
| // mix a session pool serves. | ||
| poolSize := defaultSessionChannelPoolSize |
There was a problem hiding this comment.
should the poolSize be determined by the min and max AFE channels from client configuration?
There was a problem hiding this comment.
Deferred. Real design item — ClientConfigurationManager polls asynchronously after pool construction, while NewClient builds the channel pool eagerly at construction. Wiring the poll response into poolSize requires either an async reshape path (pool rebuilds on first poll response) or a bootstrap-block-until-first-poll (adds startup latency). Classic client also uses a static option.WithGRPCConnectionPool(N) for the pool size today. Worth revisiting as a follow-up so we can do it consistently for both paths.
There was a problem hiding this comment.
If the poolSize is only used for ClientConfgurationmanager until the server returns a default, can we make it 2 instead of 1? (1 channel and the other for fallback)
There was a problem hiding this comment.
Done in 4513e7cde9. Two changes:
defaultSessionChannelPoolSizelowered 10 → 4.- Dropped the
internaloption.NewUnsafeResolveroverride so session pools intentionally ignoreoption.WithGRPCConnectionPool(N). Pool shape is server-driven end-to-end viaClientConfigurationManager— honouring a caller-supplied static size would let clients over-provision channels the server would then have to shrink.
Rationale on the const doc: "the initial size only has to carry traffic long enough for the first poll to return."
Classic pool behaviour is unchanged (still honours WithGRPCConnectionPool).
| // on the internally-created background ctx. | ||
| type sessionClient struct { | ||
| cfg Config | ||
| channelPool ChannelPool |
There was a problem hiding this comment.
is this for debug and test purpose only? maybe add a comment, and move it under managedPool
There was a problem hiding this comment.
Documented in fe6cc085f9. Added a field comment clarifying it is production runtime state (not debug/test):
configManager runs the server-driven GetClientConfiguration poll loop for the lifetime of this Client. Production runtime state — its output drives Diverter.SetSessionLoad, per-pool UpdateConfig reshapes (bounds, picker, load-balancing strategy), and future eager-scale directives. Also surfaced on /debug/configz for operator visibility.
Held on moving it under managedChannelPool — configManager is client-scoped runtime, not a channel-pool lifecycle monitor; putting it there would conflate ownership. It shares Close() ordering with the pool (configManager must Close before pools tear down so no in-flight UpdateConfig races the teardown) but that's a Close-order invariant, not co-ownership.
There was a problem hiding this comment.
I was asking about channelPool instead of configManager
There was a problem hiding this comment.
Sorry, I misread which field you were pointing at last time.
For channelPool specifically — it exists in addition to managedChannelPool because:
- Production path (
NewClient): both fields reference the same underlying*btransport.BigtableChannelPool;channelPoolis the narrowClose-only surface,managedChannelPoolbundles that pool with DSM + ConnRecycler. - Test-fake path (
newSessionClientFromParts): accepts anyClose-only impl (satisfied byfakeChannelPoolinclient_test.go) without having to satisfy the fullgtransport.ConnPoolinterface (which requiresConn(),Num(), and the wholegrpc.ClientConnInterfacesurface).
Consolidating the two under managedChannelPool would require extending fakeChannelPool to satisfy gtransport.ConnPool — doable but a ~20-30-line test fake expansion. Tracked as follow-up so this PR stays scoped. Happy to bundle now if you'd prefer.
| return nil | ||
| } | ||
| return func() (Invoker, error) { | ||
| pool, err := sc.createPoolForPayload(resourceName, sessionDesc, streamFactory, payload, key) |
There was a problem hiding this comment.
| pool, err := sc.createPoolForPayload(resourceName, sessionDesc, streamFactory, payload, key) | |
| pool, err := sc.createSessionPoolForPayload(resourceName, sessionDesc, streamFactory, payload, key) |
There was a problem hiding this comment.
Done in fe6cc085f9. Renamed to createSessionPoolForPayload.
| // getOrCreatePool ports SessionManager.GetOrCreateSessionPool: | ||
| // dedups on key, mints a display name, constructs the pool, wires | ||
| // the config listener + background loops. | ||
| func (sc *sessionClient) getOrCreatePool( |
There was a problem hiding this comment.
| func (sc *sessionClient) getOrCreatePool( | |
| func (sc *sessionClient) getOrCreateSessionPool( |
There was a problem hiding this comment.
Done in fe6cc085f9. Renamed to getOrCreateSessionPool.
| // Session-path ServerLatency is 0 by design; see | ||
| // CLIENT_SIDE_METRICS_SPEC.md #2 ("server_latencies" + connectivity | ||
| // interlock) for why. | ||
| att.SetServerLatency(0) |
There was a problem hiding this comment.
I think we should leave it absent
There was a problem hiding this comment.
Done in fe6cc085f9 (partial — SPEC-accurate). Dropped the SetServerLatency(0) call on session-path stampAttempt. Because the tracer's emission gate at internal/metrics/tracer.go:682 is if serverLatencyErr == nil { emit(serverLatency) } and both fields are zero-value on session attempts, the histogram STILL receives a 0-valued sample today — observable behavior on server_latencies is unchanged from before this diff. Follow-up will add a serverLatencySet bool flag on AttemptTracer and gate emission on serverLatencySet && serverLatencyErr == nil so session attempts truly leave the field absent. Held out of this PR because tracer.go is shared with classic and deserves its own review.
CLIENT_SIDE_METRICS_SPEC.md #2 and SESSION_COMPONENT_SPEC.md Part C row both updated to reflect the current state + the deferred tracer gate.
| if setCell := mut.GetSetCell(); setCell != nil && setCell.TimestampMicros == serverTime { | ||
| return false | ||
| } | ||
| } |
There was a problem hiding this comment.
also check mutations can't be AddToCell or mergeCell. Those are not idempotent
There was a problem hiding this comment.
Done in fe6cc085f9. mutationsAreRetryable now also rejects AddToCell + MergeToCell (both inherently non-idempotent — replaying either changes the cell contents). Session becomes strictly stricter than the classic-side helper at bigtable/bigtable.go:881, which is missing this check today. Comment on the session helper acknowledges the divergence. Follow-up PR on classic to land the same reject so the two helpers don't drift.
…gate + comments) Bundled response to mutianf's PR googleapis#20228 inline review — 15 threads across naming, retry-gate, doc-only, and one behavior tweak. Renames (mechanical, per suggestion blocks): - struct managedPool → managedSessionPool - field managedPool (btransport.ManagedChannelPool) → managedChannelPool - field pools → sessionPools, mutex poolsMu → sessionPoolsMu - fn createPoolForPayload → createSessionPoolForPayload - fn getOrCreatePool → getOrCreateSessionPool - interface method Client.OpenSessionTable → OpenTable (SESSION_CLIENT_SPEC.md updated in lockstep) Retry gate (session/table.go): - mutationsAreRetryable now rejects AddToCell + MergeToCell mutations (both inherently non-idempotent — replay changes cell contents). Session becomes strictly stricter than classic-side helper at bigtable.go:881 which is missing this check today; follow-up will land the same reject on classic. Docs (session/client.go): - configManager field: clarify it is production runtime state (drives Diverter.SetSessionLoad + per-pool UpdateConfig reshapes), not debug/test. - newSessionClientFromParts: annotate the stub == nil test-only branch — no configManager, no polls, no listener; production NewClient always supplies a stub. Metrics (session/table.go + spec docs): - session-path stampAttempt no longer calls SetServerLatency(0). Because the tracer's emission gate at internal/metrics/tracer.go:682 is (serverLatencyErr == nil) and both fields are zero-value on session, the histogram STILL receives a 0-valued sample today — behavior unchanged from before this diff. Follow-up will gate emission on a serverLatencySet flag on AttemptTracer so session attempts truly don't contribute to server_latencies. CLIENT_SIDE_METRICS_SPEC.md and SESSION_COMPONENT_SPEC.md Part C row updated to reflect the current state + the deferred fix. Held per user direction / reviewer feedback: - permissionReadWrite enum (would fail at pool-open without server support — add when the wire spec lands). - Splitting MetricsEnabled into user-facing vs internal knobs — crosses top-level bigtable.ClientConfig, deferred to separate PR. - Wiring GetClientConfiguration min/max AFE channels into poolSize — bigger design item; classic also uses static pool size today.
…ctionPool Session channel pool shape is server-driven via ClientConfigurationManager (SessionClientConfiguration polls reshape at runtime). The initial dial only has to carry traffic long enough for the first poll to return, so a smaller footprint is better: 10 → 4. Also drop the internaloption.NewUnsafeResolver override that read option.WithGRPCConnectionPool(N) and applied it to the session pool. Session pools intentionally ignore that option — pool shape is server-driven end-to-end, and honouring a caller-supplied static size would let clients over-provision channels the server would then have to shrink. Classic pool behaviour is unchanged.
…rmDirectAccessChecker Golint flags exported constructors that return an unexported concrete type — annoying for callers who then have to import the unexported name. Widening the return to the DirectAccessChecker interface (same treatment we already gave NewSessionClientDirectAccessChecker) fixes CI vet. No behavior change: the concrete *pingAndWarmDirectAccessChecker already satisfies the interface end-to-end (CheckCompatibility, Dialer, LastProbeConfig — the last returns nil for this impl since PingAndWarm has no ClientConfiguration body to seed).
🤖 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>
Summary
Third of five PRs porting the session-pool infrastructure from
feat/bigtable-sessionz-debuginto upstream. Introducesinternal/session/— a proto-native SessionClient + SessionTableAPI sitting on top of PR-2's SessionPoolImpl.
internal/session/api.go— public interfaces (ChannelPool,Config,SessionClient,SessionTableAPI,DebugAccess).internal/session/client.go—SessionClientimpl: dedicatedchannel pool (no primer),
ClientConfigurationManagerwiring,OpenSessionTable/OpenAuthorizedView/OpenMaterializedViewfactories that mint lazily-opened per-resource pools keyed by
{resource, permission}.internal/session/table.go—SessionTableimpl. Two*lazyPool(read + write); MV is read-only (write pool nil,
MutateRowreturns
ErrWriteNotSupported).stampAttemptsources per-attemptcluster_id/zone_id/ peer fields from typedInvokeResult.ClusterInfoandInvokeResult.PeerInfoperCLIENT_SIDE_METRICS_SPEC The canonical import path should be google.golang.org/cloud #1.
internal/session/lazy_pool.go—Invoker+SessionPoolinterfaces + open-on-first-use lazy wrapper. Failed opens are NOT
cached; the next call retries.
internal/session/debug.go—DebugAccessimpl surfacing poolsnapshots for sessionz/loadz/channelz/configz.
Transport additions to support the above
transport/debug_api.go(new) —SessionDebugProvider/ChannelDebugProvider/ConfigDebugProviderinterfaces +ChannelPoolDebug/SessionRefDTOs. Lives in transport (notbigtable) so
bigtable.Clientandinternal/session.SessionClientcan implement without an import cycle.
transport/diverter.go—sessionPicks/classicPickscounters,DiverterSnapshot,Snapshot().transport/connpool.go—ChannelSnapshot+ChannelPoolSnapshottype + method,WithInstanceName/WithAppProfileoptions for channelz labelling.transport/debug_tracer.go— exportedDebugTag+RecordDebugTag+TagSessionAttemptNilClusterInfo/TagSessionAttemptEmptyClusterIDcatalog constants.transport/session_descriptors.go—SessionType.ProtoName()forhuman-readable pool identifiers.
transport/direct_access_checker.go— renamesnewPingAndWarmDirectAccessChecker→NewPingAndWarmDirectAccessChecker(constructor exported) andnil-guards
primer.Primeso session-based clients can pass a nilprimer. Session clients warm channels on-demand via
OpenSession,not eagerly at pool-init.
Lifecycle correctness
sessionClient.Close()snapshots owned resources underpoolsMuandreleases the lock before running
Close/Shutdown/Cancelcalls, so a snapshot method holding
poolsMunever deadlocksteardown. Post-Close Opens surface a distinct
ErrSessionClientClosedsentinel rather than misleadingerrReadPoolNil/ErrWriteNotSupported.Stack
The diff on this PR includes PR-2's commits until #20225 merges into
main.
Test plan
go build ./internal/session/ ./internal/transport/ ./...go test ./internal/session/ -race -count=1 -short -timeout=180sgo test ./internal/transport/ -race -count=1 -short -skip 'AfeLbSim|TestHighQpsSession' -timeout=240sgofmt -l ./internal/session/ ./internal/transport/— cleango vet ./internal/session/ ./internal/transport/— cleanSESSION_CLIENT_SPEC, SESSION_POOL_SPEC, CLIENT_SIDE_METRICS_SPEC)
and SESSION_COMPONENT_SPEC (boundary rules) — PASS.