Skip to content

fix: preserve shared resources across derived clients - #851

Open
abhinavkr26104 wants to merge 3 commits into
openai:mainfrom
abhinavkr26104:codex/fix-with-options-resource-ownership
Open

fix: preserve shared resources across derived clients#851
abhinavkr26104 wants to merge 3 commits into
openai:mainfrom
abhinavkr26104:codex/fix-with-options-resource-ownership

Conversation

@abhinavkr26104

Copy link
Copy Markdown

Summary

  • preserve shared HTTP, executor, sleeper, authenticator, and workload-identity resources across ClientOptions.toBuilder() / withOptions() views
  • close shared resources only after the last owning options/client is closed, with idempotent explicit and phantom-reachability cleanup
  • close internal user-agent option views from sync and async clients
  • add regression coverage for both close orders, replacement HTTP clients, and sync/async client shutdown

Testing

  • git diff --check
  • ./gradlew :openai-java-core:test --tests com.openai.core.ClientOptionsTest --no-daemon --console=plain (timed out without diagnostics in the local environment)
  • ./gradlew :openai-java-core:compileKotlin --no-daemon --max-workers=1 --offline --console=plain (timed out without diagnostics in the local environment)

Fixes #850

@abhinavkr26104
abhinavkr26104 requested a review from a team as a code owner August 10, 2026 06:36

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 183fd87ec8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +288 to +292
httpClientResource = clientOptions.resources.httpClient
httpRequestAuthenticatorResource = clientOptions.resources.httpRequestAuthenticator
workloadIdentityAuthResource = clientOptions.resources.workloadIdentityAuth
streamHandlerExecutorResource = clientOptions.resources.streamHandlerExecutor
sleeperResource = clientOptions.resources.sleeper

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep copied resources alive while a builder is pending

When code keeps only the ClientOptions.Builder returned by toBuilder() and lets the source ClientOptions become unreachable before calling build(), these copied resource handles are not counted as references. The new ClientOptions cleaner can therefore release and mark the shared ClientOptionsResources closed while the builder still intends to reuse them, and the later build() hits retain() on a closed resource instead of producing the derived options. Retain the resources for the builder's lifetime or keep the source options strongly reachable until build() transfers ownership.

Useful? React with 👍 / 👎.

Comment on lines +70 to +74
httpRequestAuthenticator?.release()
workloadIdentityAuth?.release()
httpClient.release()
streamHandlerExecutor.release()
sleeper.release()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Release remaining resources after authenticator failures

When a provider HttpRequestAuthenticator.close() throws while closing the last owning options/client, this first release propagates immediately, so the HTTP client, stream executor, and sleeper are never released and later close() calls are no-ops through ClientOptionsCloseAction. The previous AuthenticatingHttpClient.close() path still closed the delegate after authenticator failure, so please keep releasing the remaining resources with suppressed exceptions in this failure path.

Useful? React with 👍 / 👎.

if (checkJacksonVersionCompatibility) {
checkJacksonVersionCompatibility()
}
closeWhenPhantomReachable(this, closeAction)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep resources alive for in-flight async calls

When callers keep only the CompletableFuture returned by an async request and let the client/service become unreachable, this cleaner can run as soon as the ClientOptions is phantom-reachable even though the returned future is still using the RetryingHttpClient chain. That closes the underlying HTTP client/executor/sleeper out from under an in-flight request; the prior phantom cleanup was attached to the HTTP wrapper itself, so the resources stayed reachable while the async pipeline still referenced that wrapper.

Useful? React with 👍 / 👎.

@abhinavkr26104

Copy link
Copy Markdown
Author

Addressed the P2 resource-lifetime feedback in commit 24dc9ae3.

Changes:

  • register phantom cleanup against the RetryingHttpClient chain rather than ClientOptions
  • keep HTTP transport, retry sleeper, and stream executor alive while an in-flight async future retains that chain
  • add regression coverage for a pending async request after ClientOptions becomes garbage-collectable

git diff --check passes. The focused Gradle test was attempted in offline mode but timed out during Gradle startup without diagnostics.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 24dc9ae37a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}
// Async request futures retain the HTTP client chain, not this options object. Observe the
// chain so phantom cleanup cannot close resources while an in-flight request still uses it.
closeWhenPhantomReachable(httpClient, closeAction)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep non-retryable async requests from being cleaned up

For async uploads with InputStream multipart parts, HttpRequestBody.repeatable() is false and RetryingHttpClient.executeAsync() returns the delegate responseFuture directly instead of a future that captures this RetryingHttpClient; registering the cleaner on this wrapper can therefore still release the shared HTTP client/sleeper while the only remaining reference is the in-flight delegate future. Fresh evidence for re-raising the earlier in-flight concern is this non-retryable branch, which bypasses the new reachability chain entirely.

Useful? React with 👍 / 👎.

Comment on lines +385 to +389
ClientOptions.builder()
.httpClient(httpClient)
.sleeper(sleeper)
.streamHandlerExecutor(executor)
.build()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add credentials before building the async GC test options

When this new test helper runs, ClientOptions.builder().build() still enforces that at least one credential source is configured, so the test throws IllegalStateException before it ever starts the pending async request or reaches the GC assertions. Add an API key/admin key or a test authenticator here so the new regression test can actually exercise the resource-lifetime behavior instead of failing unconditionally.

Useful? React with 👍 / 👎.

Comment on lines +217 to +218
clientOptionsWithUserAgent.close()
if (clientOptionsWithUserAgent !== clientOptions) clientOptions.close()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Close initialized async views before releasing options

When async() has been accessed on a sync client, the lazy child constructs its own clientOptionsWithUserAgent and retains the same ClientOptionsResources; this close path only releases the parent UA options and the original options, so the refcount stays above zero and explicit client.close() leaves the HTTP client/executor/sleeper open as long as the parent still holds that lazy child. Close initialized child views as well, or avoid giving them a separately retained options instance.

Useful? React with 👍 / 👎.


fun workloadIdentity(workloadIdentity: WorkloadIdentity?) = apply {
this.workloadIdentity = workloadIdentity
this.workloadIdentityAuthResource = null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain inherited workload identity auth when it is still used

When options created with workloadIdentity(...) are copied and the modifier calls workloadIdentity(null) without replacing the credential, from() has already copied the existing WorkloadIdentityCredential, so effectiveCredential() still reuses the same WorkloadIdentityAuth. Clearing workloadIdentityAuthResource here makes the derived options create an independent close resource for that same auth object, so closing the derived options can close the provider while the original options still rely on it, and closing both can close it twice.

Useful? React with 👍 / 👎.

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.

fix: closing a withOptions client closes shared resources

1 participant