-
Notifications
You must be signed in to change notification settings - Fork 244
fix: preserve shared resources across derived clients #851
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,9 +30,59 @@ import java.util.concurrent.Executor | |
| import java.util.concurrent.ExecutorService | ||
| import java.util.concurrent.Executors | ||
| import java.util.concurrent.ThreadFactory | ||
| import java.util.concurrent.atomic.AtomicBoolean | ||
| import java.util.concurrent.atomic.AtomicLong | ||
| import kotlin.jvm.optionals.getOrNull | ||
|
|
||
| private class ClientOptionsResource(private val close: () -> Unit) { | ||
| private var references = 1 | ||
| private var closed = false | ||
|
|
||
| @Synchronized | ||
| fun retain() { | ||
| check(!closed) { "Cannot retain a closed client resource" } | ||
| references++ | ||
| } | ||
|
|
||
| fun release() { | ||
| val shouldClose = | ||
| synchronized(this) { | ||
| check(references > 0) { "Client resource released too many times" } | ||
| references-- | ||
| if (references == 0) { | ||
| closed = true | ||
| true | ||
| } else false | ||
| } | ||
|
|
||
| if (shouldClose) close() | ||
| } | ||
| } | ||
|
|
||
| private class ClientOptionsResources( | ||
| val httpClient: ClientOptionsResource, | ||
| val httpRequestAuthenticator: ClientOptionsResource?, | ||
| val workloadIdentityAuth: ClientOptionsResource?, | ||
| val streamHandlerExecutor: ClientOptionsResource, | ||
| val sleeper: ClientOptionsResource, | ||
| ) { | ||
| fun release() { | ||
| httpRequestAuthenticator?.release() | ||
| workloadIdentityAuth?.release() | ||
| httpClient.release() | ||
| streamHandlerExecutor.release() | ||
| sleeper.release() | ||
|
Comment on lines
+70
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a provider Useful? React with 👍 / 👎. |
||
| } | ||
| } | ||
|
|
||
| private class ClientOptionsCloseAction(private val resources: ClientOptionsResources) : () -> Unit { | ||
| private val closed = AtomicBoolean(false) | ||
|
|
||
| override fun invoke() { | ||
| if (closed.compareAndSet(false, true)) resources.release() | ||
| } | ||
| } | ||
|
|
||
| /** A class representing the SDK client configuration. */ | ||
| class ClientOptions | ||
| private constructor( | ||
|
|
@@ -141,12 +191,18 @@ private constructor( | |
| private val organization: String?, | ||
| private val project: String?, | ||
| private val webhookSecret: String?, | ||
| private val resources: ClientOptionsResources, | ||
| ) { | ||
|
|
||
| private val closeAction = ClientOptionsCloseAction(resources) | ||
|
|
||
| init { | ||
| if (checkJacksonVersionCompatibility) { | ||
| checkJacksonVersionCompatibility() | ||
| } | ||
| // 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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For async uploads with Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| /** | ||
|
|
@@ -217,6 +273,11 @@ private constructor( | |
| private var project: String? = null | ||
| private var webhookSecret: String? = null | ||
| private var workloadIdentity: WorkloadIdentity? = null | ||
| private var httpClientResource: ClientOptionsResource? = null | ||
| private var httpRequestAuthenticatorResource: ClientOptionsResource? = null | ||
| private var workloadIdentityAuthResource: ClientOptionsResource? = null | ||
| private var streamHandlerExecutorResource: ClientOptionsResource? = null | ||
| private var sleeperResource: ClientOptionsResource? = null | ||
|
|
||
| @JvmSynthetic | ||
| internal fun from(clientOptions: ClientOptions) = apply { | ||
|
|
@@ -226,6 +287,11 @@ private constructor( | |
| jsonMapper = clientOptions.jsonMapper | ||
| streamHandlerExecutor = clientOptions.streamHandlerExecutor | ||
| sleeper = clientOptions.sleeper | ||
| httpClientResource = clientOptions.resources.httpClient | ||
| httpRequestAuthenticatorResource = clientOptions.resources.httpRequestAuthenticator | ||
| workloadIdentityAuthResource = clientOptions.resources.workloadIdentityAuth | ||
| streamHandlerExecutorResource = clientOptions.resources.streamHandlerExecutor | ||
| sleeperResource = clientOptions.resources.sleeper | ||
|
Comment on lines
+290
to
+294
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When code keeps only the Useful? React with 👍 / 👎. |
||
| clock = clientOptions.clock | ||
| baseUrl = clientOptions.baseUrl | ||
| headers = clientOptions.headers.toBuilder() | ||
|
|
@@ -256,6 +322,7 @@ private constructor( | |
| */ | ||
| fun httpClient(httpClient: HttpClient) = apply { | ||
| this.httpClient = PhantomReachableClosingHttpClient(httpClient) | ||
| this.httpClientResource = null | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -269,6 +336,7 @@ private constructor( | |
| this.httpRequestAuthenticator = | ||
| if (httpRequestAuthenticator == null) null | ||
| else PhantomReachableClosingHttpRequestAuthenticator(httpRequestAuthenticator) | ||
| this.httpRequestAuthenticatorResource = null | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -302,6 +370,7 @@ private constructor( | |
| if (streamHandlerExecutor is ExecutorService) | ||
| PhantomReachableExecutorService(streamHandlerExecutor) | ||
| else streamHandlerExecutor | ||
| this.streamHandlerExecutorResource = null | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -313,7 +382,10 @@ private constructor( | |
| * | ||
| * This class takes ownership of the sleeper and closes it when closed. | ||
| */ | ||
| fun sleeper(sleeper: Sleeper) = apply { this.sleeper = PhantomReachableSleeper(sleeper) } | ||
| fun sleeper(sleeper: Sleeper) = apply { | ||
| this.sleeper = PhantomReachableSleeper(sleeper) | ||
| this.sleeperResource = null | ||
| } | ||
|
|
||
| /** | ||
| * The clock to use for operations that require timing, like retries. | ||
|
|
@@ -393,6 +465,7 @@ private constructor( | |
| fun apiKey(apiKey: String?) = apply { | ||
| this.apiKey = apiKey | ||
| this.credential = apiKey?.let { BearerTokenCredential.create(it) } | ||
| this.workloadIdentityAuthResource = null | ||
| } | ||
|
|
||
| /** Alias for calling [Builder.apiKey] with `apiKey.orElse(null)`. */ | ||
|
|
@@ -406,6 +479,7 @@ private constructor( | |
| fun credential(credential: Credential) = apply { | ||
| this.apiKey = null | ||
| this.credential = credential | ||
| this.workloadIdentityAuthResource = null | ||
| } | ||
|
|
||
| fun azureServiceVersion(azureServiceVersion: AzureOpenAIServiceVersion) = apply { | ||
|
|
@@ -434,6 +508,7 @@ private constructor( | |
|
|
||
| fun workloadIdentity(workloadIdentity: WorkloadIdentity?) = apply { | ||
| this.workloadIdentity = workloadIdentity | ||
| this.workloadIdentityAuthResource = null | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When options created with Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| /** Alias for calling [Builder.workloadIdentity] with `workloadIdentity.orElse(null)`. */ | ||
|
|
@@ -701,6 +776,31 @@ private constructor( | |
| val effectiveWorkloadIdentityAuth = | ||
| (credential as? WorkloadIdentityCredential)?.getAuth() | ||
|
|
||
| val resources = | ||
| ClientOptionsResources( | ||
| httpClient = | ||
| httpClientResource?.also { it.retain() } | ||
| ?: ClientOptionsResource { httpClient.close() }, | ||
| httpRequestAuthenticator = | ||
| httpRequestAuthenticatorResource?.also { it.retain() } | ||
| ?: httpRequestAuthenticator?.let { | ||
| ClientOptionsResource { it.close() } | ||
| }, | ||
| workloadIdentityAuth = | ||
| workloadIdentityAuthResource?.also { it.retain() } | ||
| ?: effectiveWorkloadIdentityAuth?.let { | ||
| ClientOptionsResource { it.close() } | ||
| }, | ||
| streamHandlerExecutor = | ||
| streamHandlerExecutorResource?.also { it.retain() } | ||
| ?: ClientOptionsResource { | ||
| (streamHandlerExecutor as? ExecutorService)?.shutdown() | ||
| }, | ||
| sleeper = | ||
| sleeperResource?.also { it.retain() } | ||
| ?: ClientOptionsResource { sleeper.close() }, | ||
| ) | ||
|
|
||
| val loggingDelegate = | ||
| if (httpRequestAuthenticator != null) httpClient | ||
| else | ||
|
|
@@ -756,6 +856,7 @@ private constructor( | |
| organization, | ||
| project, | ||
| webhookSecret, | ||
| resources, | ||
| ) | ||
| } | ||
| } | ||
|
|
@@ -770,11 +871,7 @@ private constructor( | |
| * releases threads and connections if they remain idle, but if you are writing an application | ||
| * that needs to aggressively release unused resources, then you may call this method. | ||
| */ | ||
| fun close() { | ||
| httpClient.close() | ||
| (streamHandlerExecutor as? ExecutorService)?.shutdown() | ||
| sleeper.close() | ||
| } | ||
| fun close() = closeAction() | ||
|
|
||
| @JvmSynthetic | ||
| internal fun securityHeaders(security: SecurityOptions): Headers { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
async()has been accessed on a sync client, the lazy child constructs its ownclientOptionsWithUserAgentand retains the sameClientOptionsResources; this close path only releases the parent UA options and the original options, so the refcount stays above zero and explicitclient.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 👍 / 👎.