Skip to content

Commit 84823bd

Browse files
authored
Implement Timeout.cancel for waitUntilNotified (#1391)
* Implement Timeout.cancel for waitUntilNotified This triggers a 'spurious wakeup' in the case where a timeout is canceled. This is the best we can do without a discrete mechanism to distinguish between a notify() and a timeout. * API dump
1 parent 154375b commit 84823bd

File tree

3 files changed

+91
-11
lines changed

3 files changed

+91
-11
lines changed

okio/api/okio.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,7 @@ public class okio/Timeout {
772772
public static final field NONE Lokio/Timeout;
773773
public fun <init> ()V
774774
public final fun awaitSignal (Ljava/util/concurrent/locks/Condition;)V
775+
public fun cancel ()V
775776
public fun clearDeadline ()Lokio/Timeout;
776777
public fun clearTimeout ()Lokio/Timeout;
777778
public final fun deadline (JLjava/util/concurrent/TimeUnit;)Lokio/Timeout;

okio/src/jvmMain/kotlin/okio/Timeout.kt

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import java.io.IOException
1919
import java.io.InterruptedIOException
2020
import java.util.concurrent.TimeUnit
2121
import java.util.concurrent.locks.Condition
22+
import kotlin.concurrent.Volatile
2223
import kotlin.time.Duration
2324
import kotlin.time.DurationUnit
2425
import kotlin.time.toTimeUnit
@@ -32,6 +33,12 @@ actual open class Timeout {
3233
private var deadlineNanoTime = 0L
3334
private var timeoutNanos = 0L
3435

36+
/**
37+
* A sentinel that is updated to a new object on each call to [cancel]. Sample this property
38+
* before and after an operation to test if the timeout was canceled during the operation.
39+
*/
40+
@Volatile private var cancelMark: Any? = null
41+
3542
/**
3643
* Wait at most `timeout` time before aborting an operation. Using a per-operation timeout means
3744
* that as long as forward progress is being made, no sequence of operations will fail.
@@ -107,6 +114,20 @@ actual open class Timeout {
107114
}
108115
}
109116

117+
/**
118+
* Prevent all current applications of this timeout from firing. Use this when a time-limited
119+
* operation should no longer be time-limited because the nature of the operation has changed.
120+
*
121+
* This function does not mutate the [deadlineNanoTime] or [timeoutNanos] properties of this
122+
* timeout. It only applies to active operations that are limited by this timeout, and applies by
123+
* allowing those operations to run indefinitely.
124+
*
125+
* Subclasses that override this method must call `super.cancel()`.
126+
*/
127+
open fun cancel() {
128+
cancelMark = Any()
129+
}
130+
110131
/**
111132
* Waits on `monitor` until it is signaled. Throws [InterruptedIOException] if either the thread
112133
* is interrupted or if this timeout elapses before `monitor` is signaled.
@@ -239,18 +260,23 @@ actual open class Timeout {
239260
timeoutNanos
240261
}
241262

242-
// Attempt to wait that long. This will break out early if the monitor is notified.
243-
var elapsedNanos = 0L
244-
if (waitNanos > 0L) {
245-
val waitMillis = waitNanos / 1000000L
246-
(monitor as Object).wait(waitMillis, (waitNanos - waitMillis * 1000000L).toInt())
247-
elapsedNanos = System.nanoTime() - start
248-
}
263+
if (waitNanos <= 0) throw InterruptedIOException("timeout")
249264

250-
// Throw if the timeout elapsed before the monitor was notified.
251-
if (elapsedNanos >= waitNanos) {
252-
throw InterruptedIOException("timeout")
253-
}
265+
val cancelMarkBefore = cancelMark
266+
267+
// Attempt to wait that long. This will return early if the monitor is notified.
268+
val waitMillis = waitNanos / 1000000L
269+
(monitor as Object).wait(waitMillis, (waitNanos - waitMillis * 1000000L).toInt())
270+
val elapsedNanos = System.nanoTime() - start
271+
272+
// If there's time remaining, we probably got the call we were waiting for.
273+
if (elapsedNanos < waitNanos) return
274+
275+
// Return without throwing if this timeout was canceled while we were waiting. Note that this
276+
// return is a 'spurious wakeup' because Object.notify() was not called.
277+
if (cancelMark !== cancelMarkBefore) return
278+
279+
throw InterruptedIOException("timeout")
254280
} catch (e: InterruptedException) {
255281
Thread.currentThread().interrupt() // Retain interrupted status.
256282
throw InterruptedIOException("interrupted")

okio/src/jvmTest/kotlin/okio/WaitUntilNotifiedTest.kt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,49 @@ class WaitUntilNotifiedTest {
166166
}
167167
}
168168

169+
@Test
170+
@Synchronized
171+
fun cancelBeforeWaitDoesNothing() {
172+
assumeNotWindows()
173+
val timeout = Timeout()
174+
timeout.timeout(1000, TimeUnit.MILLISECONDS)
175+
timeout.cancel()
176+
val start = now()
177+
try {
178+
timeout.waitUntilNotified(this)
179+
fail()
180+
} catch (expected: InterruptedIOException) {
181+
assertEquals("timeout", expected.message)
182+
}
183+
assertElapsed(1000.0, start)
184+
}
185+
186+
@Test
187+
@Synchronized
188+
fun canceledTimeoutDoesNotThrowWhenNotNotifiedOnTime() {
189+
val timeout = Timeout()
190+
timeout.timeout(1000, TimeUnit.MILLISECONDS)
191+
timeout.cancelLater(500)
192+
193+
val start = now()
194+
timeout.waitUntilNotified(this) // Returns early but doesn't throw.
195+
assertElapsed(1000.0, start)
196+
}
197+
198+
@Test
199+
@Synchronized
200+
fun multipleCancelsAreIdempotent() {
201+
val timeout = Timeout()
202+
timeout.timeout(1000, TimeUnit.MILLISECONDS)
203+
timeout.cancelLater(250)
204+
timeout.cancelLater(500)
205+
timeout.cancelLater(750)
206+
207+
val start = now()
208+
timeout.waitUntilNotified(this) // Returns early but doesn't throw.
209+
assertElapsed(1000.0, start)
210+
}
211+
169212
/** Returns the nanotime in milliseconds as a double for measuring timeouts. */
170213
private fun now(): Double {
171214
return System.nanoTime() / 1000000.0
@@ -178,4 +221,14 @@ class WaitUntilNotifiedTest {
178221
private fun assertElapsed(duration: Double, start: Double) {
179222
assertEquals(duration, now() - start - 200.0, 250.0)
180223
}
224+
225+
private fun Timeout.cancelLater(delay: Long) {
226+
executorService.schedule(
227+
{
228+
cancel()
229+
},
230+
delay,
231+
TimeUnit.MILLISECONDS,
232+
)
233+
}
181234
}

0 commit comments

Comments
 (0)