Skip to content
Open
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 @@ -8,21 +8,57 @@ import java.util.concurrent.CompletableFuture
class DefaultSleeper : Sleeper {

private val timer = Timer("DefaultSleeper", true)
private val lock = Any()
private val pending = mutableSetOf<CompletableFuture<Void>>()
private var closed = false

override fun sleep(duration: Duration) = Thread.sleep(duration.toMillis())

override fun sleepAsync(duration: Duration): CompletableFuture<Void> {
val future = CompletableFuture<Void>()
timer.schedule(
val task =
object : TimerTask() {
override fun run() {
future.complete(null)
if (synchronized(lock) { pending.remove(future) }) {
future.complete(null)
Comment on lines +22 to +23

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 the future tracked until it is completed

When the timer fires concurrently with close(), this removes the future from pending and releases the lock before calling future.complete(null). During that gap, close() can acquire the lock, find no pending future, and return while the future is still incomplete, so shutdown does not deterministically settle every outstanding sleep as intended. Complete the future within the same critical section, or otherwise keep it tracked until completion has occurred.

Useful? React with 👍 / 👎.

}
}
},
duration.toMillis(),
)
}

synchronized(lock) {
if (closed) {
future.cancel(false)
return future
}

pending.add(future)
future.whenComplete { _, _ ->
synchronized(lock) { pending.remove(future) }
task.cancel()
}

try {
timer.schedule(task, duration.toMillis())
} catch (throwable: Throwable) {
pending.remove(future)
throw throwable
}
}

return future
}

override fun close() = timer.cancel()
override fun close() {
val pendingFutures =
synchronized(lock) {
if (closed) {
return
}
closed = true
timer.cancel()
pending.toList().also { pending.clear() }
}

pendingFutures.forEach { it.cancel(false) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.openai.core

import java.time.Duration
import java.util.concurrent.TimeUnit
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

internal class DefaultSleeperTest {

@Test
fun closeCancelsPendingAsyncSleep() {
val sleeper = DefaultSleeper()
val future = sleeper.sleepAsync(Duration.ofHours(1))

sleeper.close()

assertThat(future.isDone).isTrue()
assertThat(future.isCancelled).isTrue()
}

@Test
fun completedAsyncSleepRemainsCompletedAfterClose() {
val sleeper = DefaultSleeper()
val future = sleeper.sleepAsync(Duration.ZERO)

future.get(1, TimeUnit.SECONDS)
sleeper.close()

assertThat(future.isDone).isTrue()
assertThat(future.isCancelled).isFalse()
assertThat(future.isCompletedExceptionally).isFalse()
}

@Test
fun closeIsIdempotent() {
val sleeper = DefaultSleeper()
val future = sleeper.sleepAsync(Duration.ofHours(1))

sleeper.close()
sleeper.close()

assertThat(future.isCancelled).isTrue()
}

@Test
fun sleepAsyncAfterCloseReturnsCancelledFuture() {
val sleeper = DefaultSleeper()
sleeper.close()

val call = runCatching { sleeper.sleepAsync(Duration.ofSeconds(1)) }

assertThat(call.isSuccess).isTrue()
assertThat(call.getOrThrow().isCancelled).isTrue()
}
}