Skip to content

docs(bigtable): document implicit session.Client sharing behavior - #20337

Open
sushanb wants to merge 3 commits into
googleapis:mainfrom
sushanb:docs/bigtable-implicit-session-sharing
Open

docs(bigtable): document implicit session.Client sharing behavior#20337
sushanb wants to merge 3 commits into
googleapis:mainfrom
sushanb:docs/bigtable-implicit-session-sharing

Conversation

@sushanb

@sushanb sushanb commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Documents the process-wide session.Client sharing behavior introduced in #20335 and benchmarked in #20336.

Changes

  • `bigtable/CHANGES.md` — Unreleased-section entry noting the new sharing behavior and the incompatible-options guardrail.
  • `bigtable/client.go` — `NewClient` godoc gains one paragraph on implicit sharing + opt-outs (distinct app profile, `DisableSession`).
  • `bigtable/example_shared_session_test.go` — two Godoc examples:
    • `ExampleNewClient_multipleClientsShare` — happy path with two matching-identity `NewClient` calls.
    • `ExampleNewClientWithConfig_incompatibleOptions` — guardrail: mismatched options against the same identity return an error at construction.

Compile-only examples (no `// Output:` line) — matches the existing `ExampleCheckDirectAccessSupported` pattern.

Stacked on

This PR's diff currently includes both of the above; once they merge this PR will be rebased onto `main` and only the docs changes will remain.

sushanb added 3 commits August 6, 2026 18:10
…s with matching identity

Two bigtable.NewClient calls with the same (project, instance,
appProfile, endpoint) tuple now automatically share ONE underlying
session.Client, one gRPC channel pool, and one
ClientConfigurationManager poll goroutine. Each bigtable.Client still
has its own classic gRPC ConnPool, metrics tracer factory, Diverter,
and per-Client session.TableCache; sharing is scoped to the session
data plane only.

Wire diagram: NewClientWithConfig now calls acquireSharedSession
(new shared_session.go intern cache) in place of session.NewClient.
The cache dedups on sharedKey{project, instance, appProfile,
endpoint}; endpoint is resolved via
internaloption.UnsafeResolver.ResolvedGRPCEndpoint so
option.WithEndpoint and (indirectly) option.WithUniverseDomain both
feed into the identity. On cache hit the session's refcount is
incremented; the underlying session.Client only tears down when the
LAST bigtable.Client using it releases via Close.

Fan-out for the Diverter: each bigtable.Client registers its own
SessionLoad listener on the shared session.Client's config manager
(session.Client.AddSessionLoadListener already supports multiple
listeners) and stores the unregister thunk on the Client. Close
unregisters BEFORE releasing so a late config-poll firing during
teardown cannot fire against a Diverter whose owning Client is on the
way out.

Guardrail: two NewClient calls with matching sharedKey but different
MetricsProvider / FeatureFlags settings return an error at NewClient
time (sessionFingerprint compared on cache hit). Callers get a
diff-style message naming the diverging fields so they can normalize.

DisableSession=true and preDialed callers skip the shared cache
entirely — unchanged behavior. The accelerator package is untouched;
it dials session.NewClient directly with its own lifecycle.

Java-parity note: matches the intent of google-cloud-java PR googleapis#13829
(BigtableDataClientFactory session support). Go does it implicitly
via a package-level intern cache rather than introducing a new public
factory type so callers get sharing for free.

Trade-off: two bigtable.Clients sharing one session.Client merge
their session-level dispatch metrics into a single per-session histogram
(the dispatch-metrics counters live on the shared *sessionClient).
This matches Java parity and is accepted as an implicit-sharing
consequence.

Tests:
- Cache basics: same key → same instance, different keys →
  distinct, refcount release semantics.
- ForceCloseSharedSessions all-at-once teardown (exposed for tests
  and drastic shutdown).
- Incompatible-options error path (no refcount increment, no rebuild).
- Close-and-reopen freshness.
- Concurrent 10-way acquire dedupes to a single build, single Close.
- Build error not cached (retry works).

All existing bigtable/internal/session/, bigtable/internal/transport/,
and bigtable/ tests pass under -race.
Adds a benchmark file covering the shared-session intern cache
introduced in PR googleapis#20335. Exercises acquireSharedSession directly with a
fake build closure so nothing dials real gRPC — the same test
pattern shared_session_test.go uses. Between iterations
ForceCloseSharedSessions() resets the cache so iteration N doesn't
inherit state from iteration N-1.

Five benchmarks:
- BenchmarkAcquireSharedSession_x100_Shared — 100 acquires against
  ONE sharedKey; asserts build fires exactly once.
- BenchmarkAcquireSharedSession_x100_Distinct — 100 acquires against
  100 sharedKeys; asserts build fires 100 times (linear-scale
  baseline).
- BenchmarkRelease_SharedRefcount — pure refcount-decrement path
  (Close on a non-last holder).
- BenchmarkRelease_LastHolderTeardown — last-holder branch that
  deletes the entry and calls sc.Close.
- BenchmarkAcquireSharedSession_Concurrent — 32-way parallel acquire
  storm on one key; asserts single build under contention.

Sample numbers (INTEL(R) XEON(R) PLATINUM 8581C, -benchtime=1s):

  Shared_x100      11.68 us/op  13,632 B/op   304 allocs/op
  Distinct_x100    36.67 us/op  51,112 B/op   710 allocs/op
  RefcountRelease  73.6 ns/op        0 B/op     0 allocs/op
  LastHolder       328  ns/op        0 B/op     0 allocs/op
  Concurrent       236  ns/op      204 B/op     3 allocs/op

Sharing cuts per-100-client acquire cost by ~3x on both wall time
and bytes allocated, and the refcount fast path is allocation-free.
This is a lower bound on the real savings — these numbers exclude
the classic gRPC dial + channel pool + config-poll goroutine that
sharing eliminates entirely for cache hits.
Adds user-facing documentation for the process-wide session.Client
sharing introduced in PR googleapis#20335.

- CHANGES.md: Unreleased-section entry noting the new sharing
  behavior and the incompatible-options guardrail.
- NewClient godoc: one paragraph explaining that two NewClient calls
  with matching identity share the underlying session.Client, and
  pointing at the opt-outs (distinct app profile, DisableSession).
- New example_shared_session_test.go with two Godoc examples:
  * ExampleNewClient_multipleClientsShare — the happy path.
  * ExampleNewClientWithConfig_incompatibleOptions — the guardrail
    that rejects mismatched options against a shared session.

Compile-only examples (no `// Output:` line) matching the existing
ExampleCheckDirectAccessSupported pattern in the package — these are
documentation, not integration tests.
@sushanb
sushanb requested review from a team as code owners August 6, 2026 18:19
@product-auto-label product-auto-label Bot added the api: bigtable Issues related to the Bigtable API. label Aug 6, 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 process-wide implicit sharing of the underlying session.Client across NewClient calls targeting the same project, instance, app profile, and endpoint, reducing connection costs to O(1). The review feedback highlights several critical areas for improvement: first, holding the global mutex while executing the build callback (which performs a gRPC dial) creates a significant performance bottleneck, which could be resolved using a coordination mechanism like singleflight; second, EnableDebug and other feature flags are missing from the sessionFingerprint, allowing mismatched configurations to be silently ignored; and third, the metricsProviderKind type switch should be updated to handle pointer types (e.g., *NoopMetricsProvider) to avoid preventing sharing when pointer and value types are mixed.

Comment on lines +185 to +204
func acquireSharedSession(key sharedKey, fp sessionFingerprint, build func() (session.Client, error)) (session.Client, func() error, error) {
sharedSessionsMu.Lock()
defer sharedSessionsMu.Unlock()
if entry, ok := sharedSessions[key]; ok {
if entry.fingerprint != fp {
return nil, nil, fmt.Errorf(
"bigtable: NewClient called with same (%s) but incompatible options (%s); "+
"to use different options, use different resource identifiers",
key, entry.fingerprint.diff(fp))
}
entry.refs++
return entry.sc, releaseFor(key), nil
}
sc, err := build()
if err != nil {
return nil, nil, err
}
sharedSessions[key] = &refcountedSession{sc: sc, fingerprint: fp, refs: 1}
return sc, releaseFor(key), nil
}

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.

high

Holding the global sharedSessionsMu mutex while calling the build() callback (which performs a gRPC dial) is a significant performance bottleneck and violates the general rule against calling external callbacks while holding internal locks. If one dial hangs or takes several seconds, all other concurrent NewClient calls across the entire process (even for completely different projects or instances) will be blocked.

Consider using a coordination mechanism like golang.org/x/sync/singleflight or a map of pending builds/channels to ensure that:

  1. Callers targeting different keys can dial concurrently without blocking each other.
  2. Concurrent callers targeting the same key still deduplicate to a single dial.
References
  1. Avoid calling external callbacks while holding internal mutex locks to prevent potential deadlocks or lock inversion.
  2. Avoid performing slow operations, such as gRPC channel creation, while holding an exclusive lock, as this can block other operations and slow down the entire client.

Comment on lines +102 to +106
type sessionFingerprint struct {
metricsProviderKind string
clientSideMetricsEnabled bool
enableDirectAccess 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.

high

The changelog and documentation state that mismatched EnableDebug or FeatureFlags will return an error at NewClient time. However, sessionFingerprint only contains metricsProviderKind, clientSideMetricsEnabled, and enableDirectAccess.

If EnableDebug or other feature flags are configured differently between two clients targeting the same identity, they will currently share the same session, and the second client's configuration will be silently ignored. Please add EnableDebug and any other relevant feature flags to sessionFingerprint to ensure they are compared and mismatched configurations are correctly rejected.

Comment on lines +133 to +142
func metricsProviderKind(mp MetricsProvider) string {
switch mp.(type) {
case nil, DefaultMetricsProvider:
return "default"
case NoopMetricsProvider:
return "noop"
default:
return fmt.Sprintf("%T", mp)
}
}

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

The type switch in metricsProviderKind currently only checks for the value types DefaultMetricsProvider and NoopMetricsProvider. If a caller passes a pointer (e.g., &NoopMetricsProvider{}), it will fall into the default case and return "*bigtable.NoopMetricsProvider" instead of "noop". This prevents two otherwise identical clients from sharing a session if one passes a value and the other passes a pointer.

We should update the type switch to handle both value and pointer types.

Suggested change
func metricsProviderKind(mp MetricsProvider) string {
switch mp.(type) {
case nil, DefaultMetricsProvider:
return "default"
case NoopMetricsProvider:
return "noop"
default:
return fmt.Sprintf("%T", mp)
}
}
func metricsProviderKind(mp MetricsProvider) string {
switch mp.(type) {
case nil, DefaultMetricsProvider, *DefaultMetricsProvider:
return "default"
case NoopMetricsProvider, *NoopMetricsProvider:
return "noop"
default:
return fmt.Sprintf("%T", mp)
}
}

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.

1 participant