Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package com.braintrustdata.api.core

import com.braintrustdata.api.core.http.HttpClient
import com.braintrustdata.api.core.http.PhantomReachableClosingHttpClient
import com.braintrustdata.api.core.http.RetryingHttpClient
import com.fasterxml.jackson.databind.json.JsonMapper
import com.google.common.collect.ArrayListMultimap
Expand Down Expand Up @@ -141,11 +142,13 @@ private constructor(

return ClientOptions(
httpClient!!,
RetryingHttpClient.builder()
.httpClient(httpClient!!)
.clock(clock)
.maxRetries(maxRetries)
.build(),
PhantomReachableClosingHttpClient(
RetryingHttpClient.builder()
.httpClient(httpClient!!)
.clock(clock)
.maxRetries(maxRetries)
.build()
),
jsonMapper ?: jsonMapper(),
clock,
baseUrl,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
@file:JvmName("PhantomReachable")

package com.braintrustdata.api.core

import com.braintrustdata.api.errors.BraintrustException
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 BraintrustException("Unexpected reflective invocation failure", e)
}
}
} catch (e: ReflectiveOperationException) {
// We're running Java 8, which has no Cleaner.
null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.braintrustdata.api.core.http

import com.braintrustdata.api.core.RequestOptions
import com.braintrustdata.api.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()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.braintrustdata.api.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()
}
}