feat(bigtable): modularize Direct Access compatibility check - #19987
Conversation
Extract the Direct Access (DirectPath / DirectPathXds) compatibility decision out of BigtableChannelPool behind a new DirectAccessChecker interface so each channel pool factory can plug in its own strategy. The classic channel pool factory wires up a PingAndWarm-based checker (today's only behavior); the upcoming session pool factory will swap in a GetClientConfiguration-based checker driven by ClientConfigurationManager without touching the pool. A disabledDirectAccessChecker preserves the direct_access/compatible metric for the env/config-disabled path. This removes the in-pool probe (checkIfDirectAccessCompatible), the failure investigation chain (investigateDirectAccessFailure, probeSingleEndpoint, checkIPPlumbing, checkKernelRoutes), the metric reporting helpers, the duplicate CBT_ENABLE_DIRECTPATH env check, and the WithDirectAccessDialer / WithDirectAccessFeatureFlagsMetadata options from BigtableChannelPool. WithDirectAccessChecker replaces them as the single seam.
There was a problem hiding this comment.
Code Review
This pull request refactors the Direct Access compatibility check logic by modularizing it into a pluggable DirectAccessChecker interface, moving the classic PingAndWarm probe and the disabled state logic out of the main connection pool into separate implementations in a new file. The review feedback highlights a critical compilation issue where the new file is declared under the wrong package name (internal instead of transport), and suggests improving context propagation by passing the active context to metric recording calls instead of using context.Background().
| // Add the cached gauge instrument | ||
| daEligibleGauge metric.Int64Gauge | ||
| // directAccessChecker is the pluggable Direct Access compatibility | ||
| // strategy. nil means Direct Access is not configured for this pool; |
There was a problem hiding this comment.
is this still relevant? checker should always be configured on the pool now right?
| } else { | ||
| btopt.Debugf(pool.logger, "bigtable_connpool: Direct Access manually disabled via config or environment.") | ||
| pool.reportDirectAccessFailure("manually_disabled") | ||
| btopt.Debugf(pool.logger, "bigtable_connpool: Direct Access not configured.") |
There was a problem hiding this comment.
Do we still need the null check and should we update this log?
There was a problem hiding this comment.
if directAccessChecker is null, we skipp it so.
There was a problem hiding this comment.
"Direct Access not configured." -> this PR seems to suggest that even when Direct Access is not configured, we still provide a DisableDirectAccessChecker. So I wonder if we should update the log to just "DirectAccessChecker not configured" ?
There was a problem hiding this comment.
The null check is gone — NewBigtableChannelPool now refuses construction without a checker (commit a554712). The "Direct Access not configured." log line went away with the else branch in the same commit, so no log update needed.
| // FeatureFlagsMetadata returns the feature-flag metadata to attach when | ||
| // priming direct-access connections post-check. Only consulted when | ||
| // CheckCompatibility returned true. | ||
| FeatureFlagsMetadata() metadata.MD |
There was a problem hiding this comment.
why do we need to return feature flags?
The factory was threading directAccessMD into the pool twice: once via WithFeatureFlagsMetadata (read by the standard-path connection factory) and once into the Direct Access checker (read by the DA factory after a successful probe). Both held the same value. Make the checker the single source of truth. NewBigtableChannelPool now seeds factoryFeatureFlagsMD from checker.FeatureFlagsMetadata() before the probe runs, so both the DA and standard-path connection factories read it from the same place. The disabled checker stub carries the metadata too, so the manually-disabled path keeps the same priming behavior. WithFeatureFlagsMetadata remains as the fallback for callers that build a pool without a checker.
disabledDirectAccessChecker recorded its single manually_disabled reading inside the constructor using context.Background(). Move the recording into CheckCompatibility so it runs with the pool ctx the factory already threads in — matching reportSuccess/reportFailure on the ping-and-warm checker and keeping construction side-effect-free. The pool calls CheckCompatibility exactly once at startup, so the single-emission guarantee is preserved without an explicit `reported` flag. Thread the same ctx through pingAndWarmDirectAccessChecker's reportSuccess for consistency.
Reverse the prior indirection: the checker no longer owns or surfaces the feature-flag metadata, and the pool no longer sources it from the checker. The factory passes WithFeatureFlagsMetadata(directAccessMD) directly, and NewBigtableChannelPool wires pool.featureFlagsMD into the connectionFactory for both the direct-access and standard-path priming flows. Net effect: one option for feature-flag metadata everywhere, smaller DirectAccessChecker interface (just CheckCompatibility + Dialer), and the disabled stub stops carrying state it never used itself — pingAndWarmDirectAccessChecker still holds featureFlagsMD privately for its own probe Prime() calls.
The factory always wires either pingAndWarmDirectAccessChecker or the disabled stub, so the nil branch in NewBigtableChannelPool was only load-bearing for tests. Tighten the contract: refuse construction when no checker is set, drop the conditional around the compatibility probe, and document that the disabled stub is the way to opt out. Tests pass the disabled stub via the shared poolOpts() helper; the handful of call sites that bypass that helper now pass it explicitly, and TestNewBigtableChannelPoolEdgeCases gains a NilDirectAccessChecker case to lock the new error in place.
The field is no longer optional: NewBigtableChannelPool refuses construction without one. Reflect that in the godoc and point opt-out callers at the disabled stub.
…tpath-checker # Conflicts: # bigtable/internal/transport/connpool.go
…erface (#20027) ## Summary Mirrors the pluggable shape that PR #19987 established for the Direct Access compatibility check. Channel priming had been hard-wired into the pool via three loose options (`WithInstanceName` / `WithAppProfile` / `WithFeatureFlagsMetadata`) that the `connectionFactory` had to stitch back together on every dial. - New `ChannelPrimer` interface (`channel_primer.go`): `Prime(ctx, *BigtableConn) error`. The pluggable extension point that future pool factories (session-based, custom) can swap in. - `pingAndWarmChannelPrimer` is today's only implementation; it owns the `(instance, appProfile, featureFlagsMD)` tuple and delegates to `BigtableConn.Prime` — the existing `PingAndWarm` RPC stays untouched. - New `WithChannelPrimer` pool option replaces `WithInstanceName` / `WithAppProfile` / `WithFeatureFlagsMetadata`. - `connectionFactory` now carries a `ChannelPrimer` instead of three individual fields. **When the primer is nil, `primeWithRetry` returns immediately** — the pool dials the channel and puts it straight into rotation, no `PingAndWarm` sent. The classic channel pool factory still wires the `pingAndWarmChannelPrimer`, so user-facing default behavior is unchanged. - `pingAndWarmDirectAccessChecker` reuses the same primer rather than duplicating `conn.Prime(ctx, instance, profile, flags)` in two places. The factory now constructs **one** primer and shares it with both the pool (via `WithChannelPrimer`) and the checker, eliminating the three-arg drift that existed across the two consumers. ## Test plan - [x] `go build ./...` clean - [x] `go vet ./...` clean - [x] `golint` clean on touched files - [x] `go test ./internal/transport/...` passes (existing tests migrated; new `channel_primer_test.go` covers (a) `pingAndWarmChannelPrimer.Prime` issues `PingAndWarm` carrying the configured feature-flag metadata, and (b) `connectionFactory` skips priming entirely when the primer is nil)
…oogleapis#19987) Hand-port of upstream a25e93d into the sessionz branch. Cherry-pick failed on layout drift (bigtable/channel_pool_factory.go sits at the bigtable/ top level here, not in internal/transport/), so the diff was reproduced by hand. - New internal/transport/direct_access_checker.go: DirectAccessChecker interface, pingAndWarmDirectAccessChecker, disabledDirectAccessChecker, and the moved helpers (xdsCdsURITemplate, checkIPPlumbing, checkKernelRoutes, probeSingleEndpoint). Constructors exported as NewPingAndWarmDirectAccessChecker / NewDisabledDirectAccessChecker so the top-level bigtable package can wire them. - internal/transport/connpool.go: remove directAccessDialer, directAccessFeatureFlagsMD, daEligibleGauge fields, the WithDirectAccessDialer / WithDirectAccessFeatureFlagsMetadata options, the in-pool probe + investigation chain, the duplicate CBT_ENABLE_DIRECTPATH check, and the metric reporting helpers. Add WithDirectAccessChecker. NewBigtableChannelPool now delegates to the checker (and refuses construction without one). - bigtable/channel_pool_factory.go: gate on isDirectAccessEnabled(config) and wire a pingAndWarm checker when Direct Access is on, otherwise a disabled checker. The env var check moves out of the pool. - Test updates: poolOpts() adds the disabled checker; TestNewBigtableChannelPoolEdgeCases gains a NilDirectAccessChecker case; the six TestDirectAccessLogic subtests construct explicit checkers; DirectAccess_DisabledByEnv becomes DirectAccess_DisabledChecker since the env-var gate has moved to the factory layer. Verified: go build ./... , go vet ./... , and go test ./internal/transport/... ./debugview/ -count=1 -short all pass.
…erface (port of googleapis#20027) Hand-port of upstream 5214ab7 into the sessionz branch. Cherry-pick failed on layout drift for the same reason as PR googleapis#19987 — the top-level bigtable/channel_pool_factory.go doesn't exist upstream — so the diff was reproduced by hand. Mirrors the pluggable shape that the DAC extract established: - New internal/transport/channel_primer.go: ChannelPrimer interface with a single Prime(ctx, *BigtableConn) error method. Today's default implementation, pingAndWarmChannelPrimer, owns the (instance name, app profile, feature-flag metadata) tuple and delegates to BigtableConn.Prime. Constructor exported as NewPingAndWarmChannelPrimer so the top-level bigtable package can wire it. - New internal/transport/channel_primer_test.go: covers (a) primer delegates PingAndWarm with configured feature-flag metadata, (b) connectionFactory skips priming entirely when primer is nil. - connpool.go: add WithChannelPrimer option and channelPrimer field on BigtableChannelPool. Drop WithFeatureFlagsMetadata and the featureFlagsMD field. Keep WithInstanceName / WithAppProfile — the sessionz ChannelPoolSnapshot still reads them for debugview channelz/configz identity. connectionFactory now carries a ChannelPrimer instead of three individual fields; primeWithRetry returns immediately when the primer is nil so the pool can be used without priming. - direct_access_checker.go: pingAndWarmDirectAccessChecker now takes a ChannelPrimer instead of (instance, appProfile, featureFlagsMD) — both CheckCompatibility and probeSingleEndpoint route their Prime through it. NewPingAndWarmDirectAccessChecker signature updated accordingly. - bigtable/channel_pool_factory.go: construct one ChannelPrimer and share it with both the pool (via WithChannelPrimer) and the checker, eliminating the three-arg drift between them. Test updates in connpool_test.go: poolOpts() adds the primer, NilDirectAccessChecker case takes a primer to isolate the checker gate, TestConnectionFactory factory struct uses the primer field, and the five TestDirectAccessLogic subtests construct a primer for their DA checker. Verified: go build ./... , go vet ./... , and go test ./internal/transport/... ./debugview/ -count=1 -short all pass.
Take upstream/main's versions of both files. Fork's ports of googleapis#20027 and googleapis#19987 exported the constructors (NewPingAndWarm...) and made them return interfaces; upstream landed them unexported (newPingAndWarm...) returning concrete types. Upstream's channel_pool_factory.go calls the unexported forms, so restoring these files re-aligns the trio. Downstream break to fix next: fork's internal/session and internal/transport/session_* code that called the uppercase exported constructors will now need to be updated to the unexported forms or to route through the interface accessors upstream provides.
Restructures the spec-driven review deck from a 5-slide feature tour into a 4-slide 'why we moved from one-shot prompting to specs' story: - Slide 1 — one-shot prompting definition + the three PRs that shipped this way (googleapis#19987 DirectAccessChecker, googleapis#20027 ChannelPrimer, googleapis#20099 client-side-metrics decouple). Common shape: extract one implicitly- unary abstraction into an interface. - Slide 2 — Jetstream is 30k+ LOC; one-shot breaks. Introduces the five spec files with verified invariant counts (10 / 4 / 5 / 3 / 12+PartC) and one illustrative rule per spec. - Slide 3 — SESSION_COMPONENT_SPEC.md tour: Part A (7-layer descriptive map), Part B (12 boundary MUST-rules with grep patterns), Part C (ownership matrix excerpts). - Slide 4 — three prompt sizes with real examples from this branch: (1) simple refactor — activeVRPC/casActiveVRPC accessor extraction; (2) logic addition — adaptive session-creation throttler + how the spec invariants (POOL #5, CLIENT #3, B6) chain together; (3) big feature — unified debugview/ (7 z-pages behind one Handler) and how B3, POOL #4, B10 crystallized during that refactor. Plus reviewer-agent flow and PASS/VIOLATION/AMBIGUOUS semantics. CSS and navigation unchanged. Counter reflects 4 slides. Companion specs-deck.md not updated in this commit — will follow.
🤖 I have created a release *beep* *boop* --- ## [1.51.0](bigtable/v1.50.0...bigtable/v1.51.0) (2026-07-23) ### Features * **bigtable:** Add ChainInterceptors and RetryingVRpc for vRPC pipeline ([#20185](#20185)) ([c7a832a](c7a832a)) * **bigtable:** Add ClientConfigurationManager ([#19986](#19986)) ([3a8f927](3a8f927)) * **bigtable:** Add debug tag counter (recordDebugTag / assertDebugTag) ([#20114](#20114)) ([3c97590](3c97590)) * **bigtable:** Add lazyPool helper for on-demand session pool opening ([#20182](#20182)) ([f6ae3fb](f6ae3fb)) * **bigtable:** Add PeakEwma continuous time-decay latency tracker ([#20187](#20187)) ([9d124ef](9d124ef)) * **bigtable:** Add PoolSizer for server-driven session pool capacity ([#20189](#20189)) ([57ebbeb](57ebbeb)) * **bigtable:** Add session package with SessionClient + SessionTableAPI interfaces ([#20180](#20180)) ([4b82fd2](4b82fd2)) * **bigtable:** Add Session primitives (AttemptOutcome, vRPC ctx, msgtype) ([#20116](#20116)) ([e1011e2](e1011e2)) * **bigtable:** Add Session state enum ([#19981](#19981)) ([0748972](0748972)) * **bigtable:** Add SessionThrottler / AdaptiveSessionThrottler for OpenSession pacing ([#20184](#20184)) ([02e3c6d](02e3c6d)) * **bigtable:** Add SessionThrottler / AdaptiveSessionThrottler for OpenSession pacing ([#20184](#20184)) ([29be83e](29be83e)) * **bigtable:** Add sessionTracer for per-Session lifecycle + vRPC metrics ([#20190](#20190)) ([a466345](a466345)) * **bigtable:** Enable new auth library and JWT for instance admin client ([#20013](#20013)) ([21c4a44](21c4a44)) * **bigtable:** Modularize channel priming behind a ChannelPrimer interface ([#20027](#20027)) ([5214ab7](5214ab7)) * **bigtable:** Modularize Direct Access compatibility check ([#19987](#19987)) ([a25e93d](a25e93d)) * **o11y:** Regenerate clients for LRO tracing ([#20107](#20107)) ([779074e](779074e)) ### Bug Fixes * **bigtable:** Default cluster/zone in toOtelMetricAttrs to avoid Monitoring reject ([#20178](#20178)) ([14493f4](14493f4)) * **bigtable:** Eliminate stats-handler MD race in internal/metrics tracer ([#20158](#20158)) ([c387066](c387066)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>
Summary
BigtableChannelPoolbehind a newDirectAccessCheckerinterface so each channel pool factory can plug in its own strategy.pingAndWarmDirectAccessChecker, preserves current behavior (PingAndWarm probe + ALTS check + async failure investigation +direct_access/compatiblemetric). The upcoming session pool factory will plug in aGetClientConfiguration-based checker (driven byClientConfigurationManager) without touching the pool.disabledDirectAccessCheckerkeeps thedirect_access/compatible{reason=manually_disabled}reading when the env/config disables Direct Access — the env var check moves up to the factory so the pool no longer re-readsCBT_ENABLE_DIRECTPATH.Changes
internal/transport/direct_access_checker.go:DirectAccessCheckerinterface,pingAndWarmDirectAccessChecker,disabledDirectAccessChecker, and the moved helpers (xdsCdsURITemplate,checkIPPlumbing,checkKernelRoutes,probeSingleEndpoint).internal/transport/connpool.go: removedirectAccessDialer,directAccessFeatureFlagsMD,daEligibleGaugefields, theWithDirectAccessDialer/WithDirectAccessFeatureFlagsMetadataoptions, the in-pool probe + investigation chain, the duplicateCBT_ENABLE_DIRECTPATHcheck, and the metric reporting helpers. AddWithDirectAccessChecker.NewBigtableChannelPoolnow just delegates to the checker.internal/transport/channel_pool_factory.go: whenisDirectAccessEnabled(config)is true, wire apingAndWarmDirectAccessChecker; otherwise wire adisabledDirectAccessChecker. Both passed viaWithDirectAccessChecker.internal/transport/connpool_test.go: sixTestDirectAccessLogicsubtests updated to construct the appropriate checker. The env-disabled subtest becomesDirectAccess_DisabledCheckersince the env-var gate has moved to the factory layer.Test plan
cd bigtable && go build ./...cd bigtable && go vet ./...cd bigtable && go test ./internal/transport/... -count=1(full transport suite, including all sixTestDirectAccessLogicsubtests,TestCreateAndStartManagedChannelPool*, andTestManagedChannelPool_Close)