Documentation
¶
Overview ¶
Package session hosts the vRPC-over-session data-plane API that the public bigtable package composes with the classic gRPC data-plane via TableShim.
The split exists because a proto-native surface — one that takes and returns *SessionReadRowRequest / *SessionReadRowResponse instead of bigtable.Row — is a materially different concern from the classic TableAPI. The two live behind TableShim, which routes between them via a Diverter and owns proto ↔ bigtable.Row conversion. Nothing in this package imports the top-level bigtable package.
Index ¶
- Constants
- Variables
- func InterceptUnimplemented[T any](i *UnimplementedErrorInterceptor, sessionOp func() (T, error), ...) (T, error)
- type ChannelPool
- type Client
- type Config
- type DebugAccess
- type Invoker
- type TableAPI
- type TableCache
- type TableHandle
- func (h *TableHandle) Close() error
- func (h *TableHandle) MutateRow(ctx context.Context, req *btpb.SessionMutateRowRequest) (*btpb.SessionMutateRowResponse, error)
- func (h *TableHandle) ReadRow(ctx context.Context, req *btpb.SessionReadRowRequest) (*btpb.SessionReadRowResponse, error)
- func (h *TableHandle) Unwrap() TableAPI
- type UnimplementedErrorInterceptor
Constants ¶
const DefaultUnimplementedThreshold int32 = 30
DefaultUnimplementedThreshold is the standard number of consecutive codes.Unimplemented responses required to trip the sticky breaker. Callers pass this to NewUnimplementedErrorInterceptor unless they have a specific reason to pick a different value (tests use small values to keep breaker-trip assertions fast).
Variables ¶
var ( DefaultTableCacheTTL = 1 * time.Hour DefaultTableCacheSweepInterval = 10 * time.Minute )
Default eviction parameters for TableCache. Vars (not consts) so tests can tighten the sweep interval and TTL to milliseconds without touching the code under test.
var ErrClientClosed = errors.New("bigtable/session: Client is closed")
ErrClientClosed is returned when a Read/MutateRow is issued against a Client whose Close() has already run. Distinct from ErrWriteNotSupported (which is a resource-permanent condition) and errReadPoolNil (which flags a bookkeeping bug) so callers can tell a closed-client operation from a mis-configured resource.
var ErrWriteNotSupported = errors.New("bigtable/session: write operations not supported on this resource")
ErrWriteNotSupported is returned by MutateRow when the resource has no write pool — e.g. materialized views, which are read-only.
Functions ¶
func InterceptUnimplemented ¶ added in v1.52.0
func InterceptUnimplemented[T any]( i *UnimplementedErrorInterceptor, sessionOp func() (T, error), classicOp func() (T, error), ) (T, error)
InterceptUnimplemented runs sessionOp, records its outcome, and — on codes.Unimplemented — returns classicOp's result instead. Never mixes results: the caller gets exactly one path's (T, error).
Generic over T so value-returning ops (Row) and void ops (Apply, with T = struct{}) share one implementation.
Free function because Go still forbids methods with additional type parameters (https://github.com/golang/go/issues/49085).
Types ¶
type ChannelPool ¶ added in v1.52.0
type ChannelPool interface {
Close() error
}
ChannelPool is the narrow surface sessionClient needs from the managed channel pool it owns. Satisfied by *btransport.BigtableChannelPool. Interfaced so tests can swap in a fake pool without wiring a real gRPC transport.
type Client ¶
type Client interface {
// OpenTable returns a TableAPI for a standard table,
// identified by the leaf table name (e.g. "my-table"). Full
// resource composition happens inside the implementation.
OpenTable(tableID string) TableAPI
// OpenAuthorizedView returns a TableAPI for a specific
// authorized view under `table`.
OpenAuthorizedView(table, view string) TableAPI
// OpenMaterializedView returns a read-only TableAPI for a
// materialized view. MutateRow on the returned handle errors.
OpenMaterializedView(view string) TableAPI
// MeterProvider exposes the OpenTelemetry meter provider the
// Client was constructed with — same instance the
// bigtable client uses for its own metrics, so callers can
// register additional instruments against the same provider.
MeterProvider() metric.MeterProvider
// SessionDebug / ChannelDebug / ConfigDebug expose the debug-page
// data surfaces. Together they satisfy the same shape
// debugview.DebugProviders needs, so a Client (or a public
// wrapper composed of one) can be handed to debugview.Handler
// without an adapter. Diverter() on the returned SessionDebugProvider
// is empty for standalone session.Client — the classic/session
// split is a mixed-mode concept that only makes sense on a
// bigtable.Client that also owns a classic pool.
SessionDebug() btransport.SessionDebugProvider
ChannelDebug() btransport.ChannelDebugProvider
ConfigDebug() btransport.ConfigDebugProvider
// AddSessionLoadListener registers a listener invoked every time
// the server-driven ClientConfigurationManager reports a new
// session-load ratio (0.0 = classic-only, 1.0 = session-only).
// Returns an unregister thunk. Used by mixed-mode bigtable.Client
// to feed its Diverter; standalone session.Client callers can
// ignore this method.
AddSessionLoadListener(func(load float64)) func()
// Close closes the underlying channel pool. TableAPI
// instances previously vended become unusable.
Close() error
}
Client owns the underlying gRPC channel pool + stub and vends per-resource TableAPI instances. Does NOT cache — callers (bigtable.Client) are responsible for caching per-resource entries so repeat Opens reuse the same underlying pools.
func NewClient ¶ added in v1.52.0
func NewClient( ctx context.Context, project, instance, appProfile, clientName string, metricsProvider metrics.MetricsProvider, featureFlagsProto *btpb.FeatureFlags, opts ...option.ClientOption, ) (Client, error)
NewClient constructs a standalone session.Client. It owns the underlying channel pool, gRPC stub, metrics factory, and background goroutines end-to-end — Close() unwinds all four.
The metricsProvider argument mirrors bigtable.ClientConfig.MetricsProvider (nil = built-in metrics enabled, NoopMetricsProvider{} = disabled). opts are the standard google.api option.ClientOption values passed to gtransport.Dial — endpoint, credentials, gRPC connection pool size, etc.
Pool sizing bootstraps from btransport.defaultPoolConfig() and is overridden at runtime by the server-driven SessionClientConfiguration polls, which reshape live pools via SessionPoolImpl.UpdateConfig.
The load-balancing hook for a mixed-mode setup lives at AddSessionLoadListener — call it after construction if you're composing this Client with a bigtable.Client Diverter.
clientName sets the client_name attribute on exported client-side metrics. An empty string keeps the default "go-bigtable/<version>" token; the accelerator daemon passes its --user-agent flag so CSM attributes the metrics to the calling client library rather than the daemon build.
type Config ¶ added in v1.52.0
type Config struct {
// Project / Instance / AppProfile identify the target resource
// and get baked into resource-name composition + request-params.
Project string
Instance string
AppProfile string
// FeatureFlagsMD is merged into per-pool routing metadata.
FeatureFlagsMD metadata.MD
// ConfigMD is the metadata attached to the ClientConfigurationManager's
// GetClientConfiguration polls — instance-scoped headers.
ConfigMD metadata.MD
// MetricsEnabled mirrors the SessionManager boolean of the same
// name; propagates into FeatureFlags on every OpenSessionRequest.
MetricsEnabled bool
// FeatureFlagsProto is the pre-built FeatureFlags proto stamped
// onto every OpenSessionRequest.Flags. Required — callers MUST
// populate this with the same proto they marshaled into
// FeatureFlagsMD so header and envelope are byte-identical (the
// server rejects OpenSession with INVALID_ARGUMENT on mismatch).
FeatureFlagsProto *btpb.FeatureFlags
// SessionLoadListener is invoked whenever the server-driven
// ClientConfigurationManager reports a new session-load ratio. The
// bigtable Client wires this to Diverter.SetSessionLoad so the
// classic/session traffic split follows the server's directive.
SessionLoadListener func(load float64)
// BackgroundCtx is the parent context for the Client's own
// process-lifetime loops (today: ClientConfigurationManager polling).
// Cancelled by Client teardown.
//
// Deliberately NOT handed to session pools. A pool's lifetime is a
// strict subset of this Client's: Close() closes every pool, and
// session.TableCache also evicts + Closes individual pools while the
// Client keeps running. So this ctx is always too coarse to scope
// pool-owned work — it can only ever fire after the pool is already
// gone (client shutdown), or never at all (TTL eviction). Either way
// the pool's loops outlive its Close, and since each loop closure
// captures the pool, the pool itself stays reachable. Pools scope
// their own goroutines; see SessionPoolImpl.Start / maintCtx.
BackgroundCtx context.Context
// EnableDebug controls whether the pools this Client mints will
// collect per-pool snapshot state (sessionz / afez / flightz /
// loadz). Default false. When false, every allocating debug
// recorder in the pool is skipped for zero hot-path overhead —
// no per-session events ring, no latency-sample buffers, no
// per-pick candidate slices, no pool-wide histogram inserts, no
// slow-vRPC log entries. SessionDebug() also returns nil so the
// debugview handler renders a "not enabled" panel.
//
// Callers that plan to serve /debug/ from bigtable/debugview or
// scrape session snapshots programmatically should set this true;
// production workloads that only care about the OTel metrics
// (attempt_latencies / operation_latencies / etc.) can leave it
// off. The debug surface is otherwise unchanged; flipping the
// flag on or off requires rebuilding the client.
EnableDebug bool
}
Config bundles the settings sessionClient needs at construction time. Kept as a struct rather than a long positional constructor.
type DebugAccess ¶ added in v1.52.0
type DebugAccess interface {
// PoolSnapshots returns one PoolSnapshot per owned pool, ordered
// by pool key. Feeds sessionz.
PoolSnapshots() []btransport.PoolSnapshot
// LoadBalancingSnapshots returns per-pool picker + pick-history
// snapshots. Feeds loadz.
LoadBalancingSnapshots() []btransport.LoadBalancingSnapshot
// ChannelPool returns the *BigtableChannelPool the Client
// was constructed with, or nil.
ChannelPool() *btransport.BigtableChannelPool
// ConfigManager returns the internal ClientConfigurationManager
// for configz. Nil when no stub was provided at construction.
ConfigManager() *btransport.ClientConfigurationManager
}
DebugAccess exposes internal snapshots for the sessionz / configz / channelz debug pages. Kept separate from Client to keep the primary interface focused on data-plane concerns; consumers type- assert (Client).(DebugAccess) when they need it.
type Invoker ¶
type Invoker interface {
Invoke(ctx context.Context, desc btransport.VRpcDescriptor, req interface{}) (btransport.InvokeResult, error)
}
Invoker is the narrow surface sessionTable needs from a session pool: dispatch a single virtual RPC and surface the full InvokeResult (response, cluster info, server-side Stats, and the local SentAt timestamp). Satisfied by *btransport.SessionPoolImpl; the interface exists so tests can substitute a fake without standing up a real pool.
type TableAPI ¶
type TableAPI interface {
ReadRow(ctx context.Context, req *btpb.SessionReadRowRequest) (*btpb.SessionReadRowResponse, error)
MutateRow(ctx context.Context, req *btpb.SessionMutateRowRequest) (*btpb.SessionMutateRowResponse, error)
// Close releases this resource's underlying read + write session
// pools from the sessionClient's per-resource keyed map. Idempotent.
// Independent from Client.Close — closing an individual resource
// does not close the shared channel pool.
//
// Per-handle pool teardown is safe because callers reach this method
// through bigtable.Client's session.TableCache, which guarantees
// at-most-one TableAPI per resource per Client. A future caller that
// bypasses that cache must add a refcount before calling Close.
Close() error
}
TableAPI is the per-resource, proto-native API exposed to TableShim. The concrete implementation routes ReadRow over a READ session pool and MutateRow over a separate WRITE session pool — callers do not see the distinction. Pools open lazily on first call (see lazyPool) so read-only resources never pay for a write pool, and construction of a TableAPI never dials.
type TableCache ¶ added in v1.53.0
type TableCache struct {
// contains filtered or unexported fields
}
TableCache holds per-resource TableHandles with TTL-on-idle eviction. Zero size cap — cardinality is naturally bounded by the caller's Open* pattern.
The cache is opener-agnostic: GetOrOpen takes an openFn per call so a single cache can back tables, authorized views, and materialized views without needing to encode the resource kind in the key or dispatch on a prefix. Keys are opaque strings; the consumer (bigtable.Client) uses the fully-qualified resource name (projects/P/instances/I/tables/T, etc.) so the cache key is the same identity Cloud Bigtable uses over the wire.
func NewTableCache ¶ added in v1.53.0
func NewTableCache(ttl, sweepInterval time.Duration, now func() time.Time) *TableCache
NewTableCache constructs a cache and starts its background sweeper. Production callers pass nil for now (→ time.Now); tests inject a controllable clock.
func (*TableCache) Close ¶ added in v1.53.0
func (c *TableCache) Close()
Close stops the sweeper, waits for it to exit, then Close()s every remaining handle. Safe to call multiple times.
func (*TableCache) GetOrOpen ¶ added in v1.53.0
func (c *TableCache) GetOrOpen(key string, openFn func() TableAPI) TableAPI
GetOrOpen returns the cached handle for key, opening a fresh one via openFn on cache miss. Returns nil when openFn returns nil or the cache is closed.
Single-flight: at most one openFn runs per key at a time. The first caller to miss becomes the loader — it installs a loadState, runs openFn OUTSIDE the cache mutex (safe for open paths that take their own locks — avoids lock inversion), then installs the handle. Any concurrent caller for the same key finds the loadState and blocks on its ready channel, then loops to re-check the map — it never dials a throwaway pool. A post-eviction RPC storm therefore mints exactly one successor pool rather than dialing N and discarding N-1.
type TableHandle ¶ added in v1.53.0
type TableHandle struct {
// contains filtered or unexported fields
}
TableHandle wraps a TableAPI with a back-reference to its cache and an atomically-updated last-access timestamp. It IS the cache entry — one type does both jobs, so ReadRow / MutateRow implicitly touch the entry without requiring the caller (TableShim) to know about the cache.
Close() runs the eviction: removes the handle from the cache map AND calls the underlying api.Close(), both guarded by closeOnce so double-close from any combination of caller + sweeper is safe. The underlying Close error is memoized in closeErr and returned by every subsequent Close() call — that keeps the observable behavior stable for callers that treat Close as a query.
Underlying Close: TableAPI.Close (sessionTable.Close in table.go) releases the per-resource read + write pool entries from sessionClient.sessionPools. Cache eviction — TTL sweep, explicit handle Close, or cache-wide shutdown — therefore actually reclaims the session pools for the resource. Safety of per-handle teardown depends on this cache's at-most-one-handle-per-key invariant, which acts as an implicit refcount of size 1; see sessionTable.Close's doc.
func (*TableHandle) Close ¶ added in v1.53.0
func (h *TableHandle) Close() error
Close evicts the handle from its cache and Close()s the underlying TableAPI. Fully idempotent: subsequent calls return the error captured on the first call without invoking api.Close again. Safe to call from multiple paths (explicit caller Close, TTL sweep, cache-wide shutdown) concurrently.
The three steps run in strict order inside closeOnce.Do:
- evicted.Store(true) — readers observing true after this point route through dispatch() → resolveSuccessor instead of taking the fast path onto the doomed api.
- cache.removeEntry — future GetOrOpen callers stop finding this handle in the map, so cache misses fall through to openFn and mint a fresh live successor.
- api.Close — actually tears down the underlying pool.
The (1) → (3) ordering is the load-bearing invariant for self-heal. (2) between them just tightens the window in which a GetOrOpen fast-path could still return this now-evicted handle.
func (*TableHandle) MutateRow ¶ added in v1.53.0
func (h *TableHandle) MutateRow(ctx context.Context, req *btpb.SessionMutateRowRequest) (*btpb.SessionMutateRowResponse, error)
MutateRow proxies to the underlying TableAPI, same shape as ReadRow. Kept minimal so any future proxied method (BulkMutate, SampleRowKeys, etc.) drops in as a 3-line pass-through without duplicating the self-heal conditional.
func (*TableHandle) ReadRow ¶ added in v1.53.0
func (h *TableHandle) ReadRow(ctx context.Context, req *btpb.SessionReadRowRequest) (*btpb.SessionReadRowResponse, error)
ReadRow proxies to the underlying TableAPI on the atomic-Load fast path, self-healing via dispatch() on eviction. Happy path adds one atomic Load vs a bare api.ReadRow; the cache-map lookup only fires on the recovery branch inside dispatch().
func (*TableHandle) Unwrap ¶ added in v1.53.0
func (h *TableHandle) Unwrap() TableAPI
Unwrap returns the underlying TableAPI. Intended for test inspection — production callers should use ReadRow / MutateRow so dispatch()'s self-heal path stays in effect.
type UnimplementedErrorInterceptor ¶ added in v1.52.0
type UnimplementedErrorInterceptor struct {
// contains filtered or unexported fields
}
UnimplementedErrorInterceptor wraps a session-path call with:
- per-call classic fallback on codes.Unimplemented (the caller's request always succeeds via classic, even before the breaker trips)
- a sticky breaker that trips after `threshold` consecutive Unimplemented responses; Bypass() then lets callers short-circuit before dialing session again
One instance per resource. Any non-Unimplemented response (success or other error) resets the consecutive count — a non-Unimplemented reply proves the RPC is understood by whichever backend served it.
func NewUnimplementedErrorInterceptor ¶ added in v1.52.0
func NewUnimplementedErrorInterceptor(threshold int32) *UnimplementedErrorInterceptor
NewUnimplementedErrorInterceptor returns an interceptor that trips its sticky breaker after `threshold` consecutive Unimplemented responses. Pass DefaultUnimplementedThreshold in production.
func (*UnimplementedErrorInterceptor) Bypass ¶ added in v1.52.0
func (i *UnimplementedErrorInterceptor) Bypass() bool
Bypass reports whether the sticky breaker has tripped. Cheap atomic Load; callers use it to skip session before dialing.
func (*UnimplementedErrorInterceptor) Count ¶ added in v1.52.0
func (i *UnimplementedErrorInterceptor) Count() int32
Count returns the current consecutive-Unimplemented count. Exposed for tests and observability; routing should call Bypass().
func (*UnimplementedErrorInterceptor) RecordOutcome ¶ added in v1.52.0
func (i *UnimplementedErrorInterceptor) RecordOutcome(err error)
RecordOutcome updates the counter (and possibly trips the breaker) based on a session-path RPC's outcome:
- nil or non-Unimplemented err → count resets to 0
- codes.Unimplemented → count increments; on reaching threshold, trip via CompareAndSwap so a follow-up debug-tag / metric hook fires exactly once