Skip to content

fix(relayenv): discard SDK client build that finishes after Close() - #800

Draft
aaron-zeisler wants to merge 1 commit into
v8from
aaronz/fix-sdk-client-leak-on-close-race
Draft

fix(relayenv): discard SDK client build that finishes after Close()#800
aaron-zeisler wants to merge 1 commit into
v8from
aaronz/fix-sdk-client-leak-on-close-race

Conversation

@aaron-zeisler

Copy link
Copy Markdown
Contributor

Summary

Fixes a resource leak in envContextImpl.startSDKClient where an SDK client build that finishes after the environment has already been Close()'d gets installed into c.clients instead of being closed, leaking its streaming connection and goroutines forever.

Background

startSDKClient builds the SDK client via c.sdkClientFactory(...) without holding c.mu (the build can block for up to sdkInitTimeout), then re-acquires the lock and unconditionally does c.clients[sdkKey] = client, with no check of c.closed. Close() only closes and clears the clients present in c.clients at the moment it runs, sets c.closed = true, and never revisits the map again — a later Close() call short-circuits on the closed guard before touching c.clients. If a build that was started before Close() (from addCredential's go c.startSDKClient(...) on SDK key rotation, or from the initial build kicked off in NewEnvContext) finishes after Close() has already run to completion, it silently installs a client that will never be closed again. This is reachable in production whenever an environment/credential is torn down (e.g. removed via auto-config/RAC) while an SDK client build for it is still in flight, not just in tests.

This was spotted as a side-finding while investigating an unrelated feat/concurrent-keys test flake — that branch already carries an equivalent guard in its version of startSDKClient for a related (but more elaborate, generation/anchor-based) scenario. v8 predates that guard and needed its own, scoped to what v8 actually has.

Changes

  • startSDKClient now checks c.closed after re-acquiring the lock; if the environment was closed while the build was in flight, the newly built client is closed instead of installed, and the evaluator/data-store wiring and initErr are left untouched.
  • Added a regression test that blocks an SDK client build mid-flight, calls Close() while it's blocked, then lets the build complete — asserting the client is closed and never appears in c.clients. Verified the test fails against the pre-fix code and passes after the fix, under -race.

startSDKClient builds the SDK client without holding c.mu, since the
build can block up to sdkInitTimeout. It then unconditionally installs
the result into c.clients. If Close() runs to completion while a build
is still in flight, Close() has already closed and cleared c.clients
and will never revisit it - a later Close() call short-circuits on
c.closed without touching the map. A build that finishes afterward
would silently install its client into the map, leaking its streaming
connection and goroutines forever.

Check c.closed after re-acquiring the lock and close the client instead
of installing it when the environment has already been torn down.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant