Skip to content

feat(bigtable): add SessionClient + SessionTable + lazyPool - #20228

Merged
sushanb merged 18 commits into
googleapis:mainfrom
sushanb:bigtable-session-client
Jul 28, 2026
Merged

feat(bigtable): add SessionClient + SessionTable + lazyPool#20228
sushanb merged 18 commits into
googleapis:mainfrom
sushanb:bigtable-session-client

Conversation

@sushanb

@sushanb sushanb commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

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.goSessionClient 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.goSessionTable 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 The canonical import path should be google.golang.org/cloud #1.
  • internal/session/lazy_pool.goInvoker + SessionPool
    interfaces + open-on-first-use lazy wrapper. Failed opens are NOT
    cached; the next call retries.
  • internal/session/debug.goDebugAccess 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.gosessionPicks / classicPicks counters,
    DiverterSnapshot, Snapshot().
  • transport/connpool.goChannelSnapshot +
    ChannelPoolSnapshot type + method, WithInstanceName /
    WithAppProfile options for channelz labelling.
  • transport/debug_tracer.go — exported DebugTag +
    RecordDebugTag + TagSessionAttemptNilClusterInfo /
    TagSessionAttemptEmptyClusterID catalog constants.
  • transport/session_descriptors.goSessionType.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

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=180s
  • go test ./internal/transport/ -race -count=1 -short -skip 'AfeLbSim|TestHighQpsSession' -timeout=240s
  • gofmt -l ./internal/session/ ./internal/transport/ — clean
  • go vet ./internal/session/ ./internal/transport/ — clean
  • 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
sushanb requested review from a team as code owners July 27, 2026 21:45
@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 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.

Comment on lines +402 to +408
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))

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

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))

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.

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.

@sushanb
sushanb force-pushed the bigtable-session-client branch from 809cc0d to 22d4000 Compare July 28, 2026 01:19
@sushanb

sushanb commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Re gemini nit on session_pool.go:408 (UpdateConfig nil-check) — after the rebase off main (#20225 squash-merged as 683eda8), session_pool.go no longer belongs to this PR. The concern is real though (config.LoadBalancingOptions on the current UpdateConfig would panic on config == nil), so I will file a small follow-up against main and port to sessionz.

@sushanb

sushanb commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up in e891f351fd: wired the missing WithMetricsReporterConfig + DynamicScaleMonitor.Start + ConnectionRecycler.Start on the session-client's NewBigtableChannelPool call.

Before this commit the session path constructed BigtableChannelPool but skipped these three, so connection_pool/outstanding_rpcs and per_connection_error_count silently dropped for every session-path RPC, and neither dynamic scaling nor periodic connection replacement ran on session channels. Classic gets all three via createAndStartManagedChannelPool at bigtable/channel_pool_factory.go:54-97 — same helper can't be shared because internal/session can't import bigtable, so the wiring is duplicated with a code comment flagging the reason.

Details:

  • New fields dsm + connRecycler on sessionClient; Close() calls Stop() on both between backgroundCancel and channelPool.Close.
  • ValidateDynamicConfig hoisted above NewBigtableChannelPool for fail-fast.
  • DSM/Recycler Start(ctx) uses the caller's ctx (matching classic). Every DSM/Recycler action goes through pool methods that observe pool.poolCtx (derived from the same ctx), so a background ctx on Start would create zombie tickers after pool ops start no-op'ing.
  • Close() godoc renumbered to reflect the new 6-phase teardown.

Reviewed by session-reviewer + session-component-review + igor-reviewer — all clean.

sushanb added a commit to sushanb/google-cloud-go that referenced this pull request Jul 28, 2026
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.
@sushanb

sushanb commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Reply to gemini's inline nit on session_pool.go:408 (thread anchored at comment 3661155021):

Addressed in d92edd3c05 + 283578f903. Added the nil-guard at the 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).

Logs the contract violation via log.Printf rather than silent-return, so a broken caller surfaces on stderr the first time it fires — which is the entire point of the guard. Fires at most once per broken caller, so noise is bounded.

Comment thread bigtable/internal/session/client.go Outdated
)

// Default pool sizing — same as SessionManager's fallback (10/100).
const (

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.

remove this.

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 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.

Comment thread bigtable/internal/session/client.go Outdated

// MinSessions / MaxSessions are per-pool bounds. Zero uses
// defaults (10/100).
MinSessions int

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.

remove this.

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 67d7aad. Dropped Config.MinSessions/MaxSessions fields. Same rationale as the defaults const removal.

Comment thread bigtable/internal/session/client.go Outdated
// 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

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.

Is there newManagedChannelPool()

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 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).

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.

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:

Comment thread bigtable/internal/session/client.go Outdated
// 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 {

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.

this is also used in classic client. usea helper.

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 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.

Comment thread bigtable/internal/session/client.go Outdated
return nil, fmt.Errorf("proto.Marshal session payload: %w", err)
}
handshake := &btpb.OpenSessionRequest{
ProtocolVersion: 1,

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.

create a const saying ProtocolVersion,

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 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() {

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.

mentio that it is attmeptStartTime - bidi SentAt time.

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 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.

Comment thread bigtable/internal/session/table.go Outdated
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()))

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.

just set it to zero for session.

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.

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.

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 25c75f8 per your PeerInfo guidance. Three coordinated pieces:

  1. internal/metrics/tracer.go: connectivity_error_count classifier now has a third prong — transportType != "" (from PeerInfo). Three-way OR: (PeerInfo || server-timing header || location header) → NOT a connectivity error. Java parity confirmed against BuiltinMetricsTracer.java:400-411 (Java gates seenServer on PeerInfo + gfeTiming; Go's three-prong is strictly more lenient).

  2. internal/session/table.go: stampAttempt now calls SetServerLatency(0) unconditionally. BackendLatency deliberately unused — not wire-comparable to classic's server-timing value; session AFE overhead lives in transport_latencies separately.

  3. Specs updated: CLIENT_SIDE_METRICS_SPEC.md datastore: Support querying multiple kinds #2 (server_latencies bullet + new connectivity_error_count interlock bullet + note that classic-path also benefits from the third prong) and SESSION_COMPONENT_SPEC.md Part 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.

Comment thread bigtable/internal/transport/connpool.go Outdated

// 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 {

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.

no need to show this in sesszionz ui so remove.

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 f4dbc5b — WithInstanceName removed along with the underlying field.

Comment thread bigtable/internal/transport/connpool.go Outdated

// WithAppProfile tags the pool with the app profile ID for display in
// channelz.
func WithAppProfile(appProfile string) BigtableChannelPoolOption {

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.

same remove.

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 f4dbc5b — WithAppProfile removed along with the underlying field.

Comment thread bigtable/internal/transport/connpool.go Outdated
// instanceName / appProfile are display-only identity, surfaced by
// ChannelPoolSnapshot so channelz can label the pool. Set at
// construction via WithInstanceName / WithAppProfile.
instanceName string

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.

you can remove this field.

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 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.

sushanb added a commit to sushanb/google-cloud-go that referenced this pull request Jul 28, 2026
…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.
sushanb added a commit to sushanb/google-cloud-go that referenced this pull request Jul 28, 2026
…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.
sushanb added a commit to sushanb/google-cloud-go that referenced this pull request Jul 28, 2026
…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.
sushanb added a commit to sushanb/google-cloud-go that referenced this pull request Jul 28, 2026
…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.
sushanb added a commit to sushanb/google-cloud-go that referenced this pull request Jul 28, 2026
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.
@sushanb

sushanb commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up in 5023e0c: renamed session.SessionClientsession.Client (and NewSessionClientNewClient, ErrSessionClientClosedErrClientClosed) to unstutter the package-qualified name. Same treatment as the earlier session.TableAPI rename. Private sessionClient struct + newSessionClientFromParts helper unchanged (unexported, no external stutter). SESSION_CLIENT_SPEC.md type-name citations updated to match.

// 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"

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.

why is this neede?

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 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"

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.

please clean this up

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 1a1d1b3 along with the sibling nil-ClusterInfo tag — see the reply on that thread for the full cleanup scope.

sushanb added a commit to sushanb/google-cloud-go that referenced this pull request Jul 28, 2026
…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.
sushanb added 11 commits July 28, 2026 17:52
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.
sushanb added 2 commits July 28, 2026 17:52
…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.
@sushanb
sushanb force-pushed the bigtable-session-client branch from 1a1d1b3 to 84f973e Compare July 28, 2026 18:04
- **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).

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.

remove the java reference.

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 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 (

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.

can this be shared with classic client?

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 9f59e0bd4a. Moved to a shared home:

  • New bigtable/internal/transport/headers.go exports ResourcePrefixHeader + RequestParamsHeader.
  • Session package's local resourcePrefixHeader / requestParamsHeader consts now alias btransport.ResourcePrefixHeader / btransport.RequestParamsHeader — call sites unchanged, single source of truth.
  • connpool.go dropped its own lowercase requestParamsHeader and 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

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.

note that CBT_FORCE_SESSION is for internal proejcts nly.

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 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.

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.

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?

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.

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.

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.

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.

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.

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.

Comment thread bigtable/internal/session/api.go Outdated
// 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

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.

nit: maybe change to OpenTable since we changed the API from SessionTableAPI to TableAPI

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 fe6cc085f9. Renamed SessionClient.OpenSessionTableOpenTable (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

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.

these can probably be part of the FeatureFlagMD instead of separate configs?

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.

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

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.

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.

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.

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

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.

should the poolSize be determined by the min and max AFE channels from client configuration?

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.

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.

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 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)

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 4513e7cde9. Two changes:

  1. defaultSessionChannelPoolSize lowered 10 → 4.
  2. Dropped the internaloption.NewUnsafeResolver override so session pools intentionally ignore option.WithGRPCConnectionPool(N). Pool shape is server-driven end-to-end via ClientConfigurationManager — 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).

Comment thread bigtable/internal/session/client.go Outdated
// on the internally-created background ctx.
type sessionClient struct {
cfg Config
channelPool ChannelPool

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.

is this for debug and test purpose only? maybe add a comment, and move it under managedPool

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.

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 managedChannelPoolconfigManager 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.

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.

I was asking about channelPool instead of configManager

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.

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; channelPool is the narrow Close-only surface, managedChannelPool bundles that pool with DSM + ConnRecycler.
  • Test-fake path (newSessionClientFromParts): accepts any Close-only impl (satisfied by fakeChannelPool in client_test.go) without having to satisfy the full gtransport.ConnPool interface (which requires Conn(), Num(), and the whole grpc.ClientConnInterface surface).

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.

Comment thread bigtable/internal/session/client.go Outdated
return nil
}
return func() (Invoker, error) {
pool, err := sc.createPoolForPayload(resourceName, sessionDesc, streamFactory, payload, key)

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.

Suggested change
pool, err := sc.createPoolForPayload(resourceName, sessionDesc, streamFactory, payload, key)
pool, err := sc.createSessionPoolForPayload(resourceName, sessionDesc, streamFactory, payload, key)

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 fe6cc085f9. Renamed to createSessionPoolForPayload.

Comment thread bigtable/internal/session/client.go Outdated
// getOrCreatePool ports SessionManager.GetOrCreateSessionPool:
// dedups on key, mints a display name, constructs the pool, wires
// the config listener + background loops.
func (sc *sessionClient) getOrCreatePool(

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.

Suggested change
func (sc *sessionClient) getOrCreatePool(
func (sc *sessionClient) getOrCreateSessionPool(

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 fe6cc085f9. Renamed to getOrCreateSessionPool.

Comment thread bigtable/internal/session/table.go Outdated
// Session-path ServerLatency is 0 by design; see
// CLIENT_SIDE_METRICS_SPEC.md #2 ("server_latencies" + connectivity
// interlock) for why.
att.SetServerLatency(0)

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.

I think we should leave it absent

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 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
}
}

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.

also check mutations can't be AddToCell or mergeCell. Those are not idempotent

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 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.
sushanb added 2 commits July 28, 2026 20:47
…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).
@sushanb
sushanb merged commit ab2c96c into googleapis:main Jul 28, 2026
19 checks passed
sushanb pushed a commit that referenced this pull request Aug 3, 2026
🤖 I have created a release *beep* *boop*
---


##
[1.52.0](bigtable/v1.51.0...bigtable/v1.52.0)
(2026-08-03)


### Features

* **bigtable:** Add AFE picker (Simple / LeastInFlight / LeastLatency)
([#20204](#20204))
([bcbf714](bcbf714))
* **bigtable:** Add ClientConfig.DisableSession to opt out of session
backend
([#20297](#20297))
([7ee5e44](7ee5e44))
* **bigtable:** Add getClientConfigDirectAccessChecker for session pools
([#20209](#20209))
([3b8d30a](3b8d30a))
* **bigtable:** Add NoOpChannelPrimer for session channel pools
([#20208](#20208))
([d055a8a](d055a8a))
* **bigtable:** Add per-AFE sessionList for the two-tier session pool
([#20224](#20224))
([dbf0c3f](dbf0c3f))
* **bigtable:** Add protoRowToRow conversion helper for TableShim
([#20257](#20257))
([1297143](1297143))
* **bigtable:** Add Session debug surface (observability fields +
methods)
([#20211](#20211))
([d8d3e16](d8d3e16))
* **bigtable:** Add Session lifecycle (Start, Close, ForceClose,
readLoop, heartBeatLoop)
([#20215](#20215))
([b9e53c6](b9e53c6))
* **bigtable:** Add Session struct + state machine
([#20117](#20117))
([09acbb3](09acbb3))
* **bigtable:** Add session.Config.EnableDebug to gate sessionz debug
state
([#20247](#20247))
([ce74c31](ce74c31))
* **bigtable:** Add SessionClient + SessionTable + lazyPool
([#20228](#20228))
([ab2c96c](ab2c96c))
* **bigtable:** Add SessionPoolImpl (two-tier pool + scaling + debug)
([#20225](#20225))
([683eda8](683eda8))
* **bigtable:** Rename session pool display to
&lt;resource-id&gt;-&lt;PERM&gt;
([#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