feat(bigtable): add NewSessionClient constructor with session-dedicated channel pool - #20183
feat(bigtable): add NewSessionClient constructor with session-dedicated channel pool#20183sushanb wants to merge 1 commit into
Conversation
…ed channel pool Adds bigtable.SessionClient — a standalone session-only handle that owns its own gRPC channel pool, stub, and ClientConfigurationManager, independent of any classic *Client the caller may also have open. This is the first slice of the larger session-table-api plan: constructor + channel pool + config manager wiring. Follow-ups will add per-resource SessionTable factory methods, cross-client caching, mixed-mode integration inside *Client, and sessionz debug forwarding. Notes: - Option plumbing mirrors NewClientWithConfig verbatim so behavioural drift between the two constructors stays visible in diff. - NewSessionClient is the first production caller of NewClientConfigurationManager (all prior callers are in tests). - The config manager's polling loop is started on a background parent ctx so it outlives NewSessionClient's caller context; Close is the only teardown path. Close is idempotent via sync.Once. - Reuses the existing pingAndWarmChannelPrimer and pingAndWarmDirectAccessChecker for now; a session-specific noop primer and getClientConfigDirectAccessChecker will land as follow-ups. Tests cover accessors, the emulator NoopMetricsProvider override, and Close idempotency; all use an in-memory bttest server.
There was a problem hiding this comment.
Code Review
This pull request introduces SessionClient, a session-only handle to a Bigtable instance that manages its own gRPC channel pool, stub, and configuration manager, along with corresponding unit tests. The review feedback highlights several important improvements: ensuring proper cleanup of metricsTracerFactory if the constructor fails, making the background logger configurable instead of hardcoding log.Default(), adding a nil check for mPool in Close() to prevent potential panics, and deferring the cleanup function in the idempotency test to avoid leaking the in-memory test server.
| if err != nil { | ||
| return nil, err | ||
| } |
There was a problem hiding this comment.
When initializing resources that require cleanup (such as metrics) inside a constructor, use named return errors and a defer block to clean up the resources if the constructor fails. This ensures that if any subsequent step returns an error, the newly created metricsTracerFactory is properly cleaned up.
defer func() {
if err != nil {
metricsTracerFactory.Shutdown()
}
}()References
- When initializing resources that require cleanup (such as metrics) inside a constructor, use named return errors and a defer block to clean up the resources if the constructor fails.
| // context, so start it on a background parent. Close teardown flows | ||
| // through configManager.Close(), not ctx cancellation. | ||
| instanceName := fmt.Sprintf("projects/%s/instances/%s", project, instance) | ||
| configManager := btransport.NewClientConfigurationManager(stub, instanceName, config.AppProfile, featureFlagsMD, log.Default()) |
There was a problem hiding this comment.
Hardcoding log.Default() in production library code makes it difficult for library users to suppress, redirect, or customize background configuration polling logs (which can be noisy during transient network issues). Consider making the logger configurable via ClientConfig or using a quieter/discard logger by default if the user does not specify one.
| if c.metricsTracerFactory != nil { | ||
| c.metricsTracerFactory.Shutdown() | ||
| } | ||
| c.closeErr = c.mPool.Close() |
There was a problem hiding this comment.
To prevent a potential nil pointer dereference panic if Close() is called on an uninitialized or partially initialized SessionClient (where mPool is nil), add a nil check before calling c.mPool.Close(), similar to the guards used for configManager and metricsTracerFactory.
if c.mPool != nil {
c.closeErr = c.mPool.Close()
}| sc, _ := newTestSessionClient(t) // don't defer cleanup — we drive Close manually below | ||
|
|
||
| if err := sc.Close(); err != nil { | ||
| t.Errorf("first Close: %v", err) | ||
| } | ||
| // Second Close must not panic and must not error. | ||
| if err := sc.Close(); err != nil { | ||
| t.Errorf("second Close: %v", err) | ||
| } |
There was a problem hiding this comment.
In TestSessionClient_Close_Idempotent, discarding the cleanup function returned by newTestSessionClient leaks the in-memory bttest.Server because srv.Close() is never called. Since Close() is idempotent, you can safely defer cleanup() at the start of the test to ensure the server is properly shut down, while still manually testing multiple Close() calls on the client.
| sc, _ := newTestSessionClient(t) // don't defer cleanup — we drive Close manually below | |
| if err := sc.Close(); err != nil { | |
| t.Errorf("first Close: %v", err) | |
| } | |
| // Second Close must not panic and must not error. | |
| if err := sc.Close(); err != nil { | |
| t.Errorf("second Close: %v", err) | |
| } | |
| sc, cleanup := newTestSessionClient(t) | |
| defer cleanup() | |
| if err := sc.Close(); err != nil { | |
| t.Errorf("first Close: %v", err) | |
| } | |
| if err := sc.Close(); err != nil { | |
| t.Errorf("second Close: %v", err) | |
| } |
Summary
Adds
bigtable.SessionClient— a standalone, session-only handle that owns its own gRPC channel pool, stub, andClientConfigurationManager, independent of any classic*Clientthe caller may also have open.This is the first slice of the larger session-table-api plan: constructor + channel pool + config manager wiring only. Follow-ups will add per-resource
SessionTablefactory methods, cross-client caching on*Client, mixed-mode integration inside*Client, and sessionz debug forwarding.Usage
Notes for reviewers
NewClientWithConfigverbatim so any behavioural drift between the two constructors stays visible in diff. When retry env vars, direct-access options, or feature-flag construction change inNewClientWithConfig, they should mirror here.NewClientConfigurationManager. Every prior caller is in tests.configManager.Start(context.Background()), not the constructor's ctx. The polling loop must outliveNewSessionClient's caller context; teardown flows throughClose, not ctx cancellation.Closeis idempotent viasync.Once. Ordering: manager → metrics tracer → channel pool, so no in-flightGetClientConfigurationlands on a closed pool.pingAndWarmChannelPrimerandpingAndWarmDirectAccessCheckerfor now. A session-specific noop primer andgetClientConfigDirectAccessChecker(already anticipated indirect_access_checker.go:43-65) will land as follow-ups.Explicitly out of scope (follow-ups)
SessionClient.NewSessionTable*factory +SessionTabletypesessionTablescache on*ClientClient.sessionClientfield,OpenTablerouting)ChannelPrimergetClientConfigDirectAccessCheckerSessionDebug/ChannelDebug/ConfigDebugforwarding (waits ondebugviewpackage landing upstream)Test plan
go build ./...cleango vet ./...cleango test -run '^(TestNewSessionClient|TestSessionClient)' -count=1passesgo test -short ./internal/transport/passes (no regression in the underlying pool machinery)/cc @mutianf