-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(client): clean up resource leaks when the resource becomes phant…
…om reachable chore: unknown commit message
- Loading branch information
1 parent
685553f
commit 3ab123c
Showing
6 changed files
with
127 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
46 changes: 46 additions & 0 deletions
46
openai-java-core/src/main/kotlin/com/openai/core/PhantomReachable.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
@file:JvmName("PhantomReachable") | ||
|
||
package com.openai.core | ||
|
||
import com.openai.errors.OpenAIException | ||
import java.lang.reflect.InvocationTargetException | ||
|
||
/** | ||
* Closes [closeable] when [observed] becomes only phantom reachable. | ||
* | ||
* This is a wrapper around a Java 9+ [java.lang.ref.Cleaner], or a no-op in older Java versions. | ||
*/ | ||
@JvmSynthetic | ||
internal fun closeWhenPhantomReachable(observed: Any, closeable: AutoCloseable) { | ||
check(observed !== closeable) { | ||
"`observed` cannot be the same object as `closeable` because it would never become phantom reachable" | ||
} | ||
closeWhenPhantomReachable?.let { it(observed, closeable::close) } | ||
} | ||
|
||
private val closeWhenPhantomReachable: ((Any, AutoCloseable) -> Unit)? by lazy { | ||
try { | ||
val cleanerClass = Class.forName("java.lang.ref.Cleaner") | ||
val cleanerCreate = cleanerClass.getMethod("create") | ||
val cleanerRegister = | ||
cleanerClass.getMethod("register", Any::class.java, Runnable::class.java) | ||
val cleanerObject = cleanerCreate.invoke(null); | ||
|
||
{ observed, closeable -> | ||
try { | ||
cleanerRegister.invoke(cleanerObject, observed, Runnable { closeable.close() }) | ||
} catch (e: ReflectiveOperationException) { | ||
if (e is InvocationTargetException) { | ||
when (val cause = e.cause) { | ||
is RuntimeException, | ||
is Error -> throw cause | ||
} | ||
} | ||
throw OpenAIException("Unexpected reflective invocation failure", e) | ||
} | ||
} | ||
} catch (e: ReflectiveOperationException) { | ||
// We're running Java 8, which has no Cleaner. | ||
null | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
openai-java-core/src/main/kotlin/com/openai/core/http/PhantomReachableClosingHttpClient.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package com.openai.core.http | ||
|
||
import com.openai.core.RequestOptions | ||
import com.openai.core.closeWhenPhantomReachable | ||
import java.util.concurrent.CompletableFuture | ||
|
||
internal class PhantomReachableClosingHttpClient(private val httpClient: HttpClient) : HttpClient { | ||
init { | ||
closeWhenPhantomReachable(this, httpClient) | ||
} | ||
|
||
override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse = | ||
httpClient.execute(request, requestOptions) | ||
|
||
override fun executeAsync( | ||
request: HttpRequest, | ||
requestOptions: RequestOptions | ||
): CompletableFuture<HttpResponse> = httpClient.executeAsync(request, requestOptions) | ||
|
||
override fun close() = httpClient.close() | ||
} |
16 changes: 16 additions & 0 deletions
16
...i-java-core/src/main/kotlin/com/openai/core/http/PhantomReachableClosingStreamResponse.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package com.openai.core.http | ||
|
||
import com.openai.core.closeWhenPhantomReachable | ||
import java.util.stream.Stream | ||
|
||
internal class PhantomReachableClosingStreamResponse<T>( | ||
private val streamResponse: StreamResponse<T> | ||
) : StreamResponse<T> { | ||
init { | ||
closeWhenPhantomReachable(this, streamResponse) | ||
} | ||
|
||
override fun stream(): Stream<T> = streamResponse.stream() | ||
|
||
override fun close() = streamResponse.close() | ||
} |
27 changes: 27 additions & 0 deletions
27
openai-java-core/src/test/kotlin/com/openai/core/PhantomReachableTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package com.openai.core | ||
|
||
import org.assertj.core.api.Assertions.assertThat | ||
import org.junit.jupiter.api.Test | ||
|
||
internal class PhantomReachableTest { | ||
|
||
@Test | ||
fun closeWhenPhantomReachable_whenObservedIsGarbageCollected_closesCloseable() { | ||
var closed = false | ||
val closeable = AutoCloseable { closed = true } | ||
|
||
closeWhenPhantomReachable( | ||
// Pass an inline object for the object to observe so that it becomes immediately | ||
// unreachable. | ||
Any(), | ||
closeable | ||
) | ||
|
||
assertThat(closed).isFalse() | ||
|
||
System.gc() | ||
Thread.sleep(3000) | ||
|
||
assertThat(closed).isTrue() | ||
} | ||
} |