session

package
v1.56.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

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

View Source
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

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

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

View Source
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:

  1. evicted.Store(true) — readers observing true after this point route through dispatch() → resolveSuccessor instead of taking the fast path onto the doomed api.
  2. 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.
  3. 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

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

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

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL