Skip to content

Commit 0255a44

Browse files
committed
Use Duration as the ground truth for communicating durations
Historically, the library evolved using "a Long of milliseconds" as the standard of denoting durations. Since then, `kotlin.time.Duration` appeared, encompassing a number of useful conversions. There are several consequences to this change. - Before, `delay(Long)` and `delay(Duration)` were not easily expressed via one another. For example, `delay(Long.MAX_VALUE / 2 + 1)` (up until `Long.MAX_VALUE`) used to be considered a valid delay, but it was not expressible in `delay(Duration)`. Therefore, `delay(Long)` was the more fundamental implementation. However, `delay(Duration)` could not just be expressed as `delay(inWholeMilliseconds)`, as we need to round the durations up when delaying events, and this required complex logic. With this change, `delay(Duration)` is taken as the standard implementation, and `delay(Long)` is just `delay(timeMillis.milliseconds)`, simplifying the conceptual space. - The same goes for other APIs accepting either a duration or some Long number of milliseconds. - In several platform APIs, we are actually able to pass nanoseconds as the duration to wait for. We can now accurately do that. This precision is unlikely to be important in practice, but it is still nice that we are not losing any information in transit. - On Android's main thread, it's no longer possible to wait for `Long.MAX_VALUE / 2` milliseconds: it's considered an infinite duration. `Long.MAX_VALUE / 2 - 1` is still fine. - In `kotlinx-coroutines-test`, before, it was possible to observe correct behavior for up to `Long.MAX_VALUE` milliseconds. Now, this value is drastically reduced, to be able to test the nanosecond precision. - In `kotlinx-coroutines-test`, we now fail with an `IllegalStateException` if we enter the representable ceiling of time during the test. Before, we used to continue the test execution, only using the order in which tasks arrived but not their virtual time values.
1 parent d30af7c commit 0255a44

File tree

56 files changed

+461
-450
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+461
-450
lines changed

Diff for: docs/topics/cancellation-and-timeouts.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ It produces the following output:
321321
I'm sleeping 0 ...
322322
I'm sleeping 1 ...
323323
I'm sleeping 2 ...
324-
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
324+
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1.3s
325325
```
326326

327327
<!--- TEST STARTS_WITH -->

Diff for: kotlinx-coroutines-core/api/kotlinx-coroutines-core.api

+6-5
Original file line numberDiff line numberDiff line change
@@ -297,20 +297,21 @@ public final class kotlinx/coroutines/Deferred$DefaultImpls {
297297
}
298298

299299
public abstract interface class kotlinx/coroutines/Delay {
300-
public abstract fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
301-
public abstract fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle;
302-
public abstract fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V
300+
public abstract fun invokeOnTimeout-KLykuaI (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle;
301+
public abstract fun scheduleResumeAfterDelay-VtjQ1oo (JLkotlinx/coroutines/CancellableContinuation;)V
302+
public abstract fun timeoutMessage-LRDsOJo (J)Ljava/lang/String;
303303
}
304304

305305
public final class kotlinx/coroutines/Delay$DefaultImpls {
306-
public static fun delay (Lkotlinx/coroutines/Delay;JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
307-
public static fun invokeOnTimeout (Lkotlinx/coroutines/Delay;JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle;
306+
public static fun invokeOnTimeout-KLykuaI (Lkotlinx/coroutines/Delay;JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle;
307+
public static fun timeoutMessage-LRDsOJo (Lkotlinx/coroutines/Delay;J)Ljava/lang/String;
308308
}
309309

310310
public final class kotlinx/coroutines/DelayKt {
311311
public static final fun awaitCancellation (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
312312
public static final fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
313313
public static final fun delay-VtjQ1oo (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
314+
public static final fun toDelayMillis-LRDsOJo (J)J
314315
}
315316

316317
public abstract interface annotation class kotlinx/coroutines/DelicateCoroutinesApi : java/lang/annotation/Annotation {

Diff for: kotlinx-coroutines-core/api/kotlinx-coroutines-core.klib.api

+4-3
Original file line numberDiff line numberDiff line change
@@ -289,9 +289,9 @@ abstract interface kotlinx.coroutines/CoroutineScope { // kotlinx.coroutines/Cor
289289
}
290290

291291
abstract interface kotlinx.coroutines/Delay { // kotlinx.coroutines/Delay|null[0]
292-
abstract fun scheduleResumeAfterDelay(kotlin/Long, kotlinx.coroutines/CancellableContinuation<kotlin/Unit>) // kotlinx.coroutines/Delay.scheduleResumeAfterDelay|scheduleResumeAfterDelay(kotlin.Long;kotlinx.coroutines.CancellableContinuation<kotlin.Unit>){}[0]
293-
open fun invokeOnTimeout(kotlin/Long, kotlinx.coroutines/Runnable, kotlin.coroutines/CoroutineContext): kotlinx.coroutines/DisposableHandle // kotlinx.coroutines/Delay.invokeOnTimeout|invokeOnTimeout(kotlin.Long;kotlinx.coroutines.Runnable;kotlin.coroutines.CoroutineContext){}[0]
294-
open suspend fun delay(kotlin/Long) // kotlinx.coroutines/Delay.delay|delay(kotlin.Long){}[0]
292+
abstract fun scheduleResumeAfterDelay(kotlin.time/Duration, kotlinx.coroutines/CancellableContinuation<kotlin/Unit>) // kotlinx.coroutines/Delay.scheduleResumeAfterDelay|scheduleResumeAfterDelay(kotlin.time.Duration;kotlinx.coroutines.CancellableContinuation<kotlin.Unit>){}[0]
293+
open fun invokeOnTimeout(kotlin.time/Duration, kotlinx.coroutines/Runnable, kotlin.coroutines/CoroutineContext): kotlinx.coroutines/DisposableHandle // kotlinx.coroutines/Delay.invokeOnTimeout|invokeOnTimeout(kotlin.time.Duration;kotlinx.coroutines.Runnable;kotlin.coroutines.CoroutineContext){}[0]
294+
open fun timeoutMessage(kotlin.time/Duration): kotlin/String // kotlinx.coroutines/Delay.timeoutMessage|timeoutMessage(kotlin.time.Duration){}[0]
295295
}
296296

297297
abstract interface kotlinx.coroutines/Job : kotlin.coroutines/CoroutineContext.Element { // kotlinx.coroutines/Job|null[0]
@@ -758,6 +758,7 @@ final fun (kotlin.coroutines/CoroutineContext).kotlinx.coroutines/ensureActive()
758758
final fun (kotlin.coroutines/CoroutineContext).kotlinx.coroutines/newCoroutineContext(kotlin.coroutines/CoroutineContext): kotlin.coroutines/CoroutineContext // kotlinx.coroutines/newCoroutineContext|[email protected](kotlin.coroutines.CoroutineContext){}[0]
759759
final fun (kotlin.ranges/IntRange).kotlinx.coroutines.flow/asFlow(): kotlinx.coroutines.flow/Flow<kotlin/Int> // kotlinx.coroutines.flow/asFlow|[email protected](){}[0]
760760
final fun (kotlin.ranges/LongRange).kotlinx.coroutines.flow/asFlow(): kotlinx.coroutines.flow/Flow<kotlin/Long> // kotlinx.coroutines.flow/asFlow|[email protected](){}[0]
761+
final fun (kotlin.time/Duration).kotlinx.coroutines/toDelayMillis(): kotlin/Long // kotlinx.coroutines/toDelayMillis|[email protected](){}[0]
761762
final fun (kotlin/IntArray).kotlinx.coroutines.flow/asFlow(): kotlinx.coroutines.flow/Flow<kotlin/Int> // kotlinx.coroutines.flow/asFlow|[email protected](){}[0]
762763
final fun (kotlin/LongArray).kotlinx.coroutines.flow/asFlow(): kotlinx.coroutines.flow/Flow<kotlin/Long> // kotlinx.coroutines.flow/asFlow|[email protected](){}[0]
763764
final fun (kotlinx.coroutines.channels/ReceiveChannel<*>).kotlinx.coroutines.channels/cancelConsumed(kotlin/Throwable?) // kotlinx.coroutines.channels/cancelConsumed|[email protected]<*>(kotlin.Throwable?){}[0]

Diff for: kotlinx-coroutines-core/common/src/Delay.kt

+21-36
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import kotlinx.coroutines.selects.*
44
import kotlin.coroutines.*
55
import kotlin.time.*
66
import kotlin.time.Duration.Companion.nanoseconds
7+
import kotlin.time.Duration.Companion.milliseconds
78

89
/**
910
* This dispatcher _feature_ is implemented by [CoroutineDispatcher] implementations that natively support
@@ -16,19 +17,8 @@ import kotlin.time.Duration.Companion.nanoseconds
1617
*/
1718
@InternalCoroutinesApi
1819
public interface Delay {
19-
20-
/** @suppress **/
21-
@Deprecated(
22-
message = "Deprecated without replacement as an internal method never intended for public use",
23-
level = DeprecationLevel.ERROR
24-
) // Error since 1.6.0
25-
public suspend fun delay(time: Long) {
26-
if (time <= 0) return // don't delay
27-
return suspendCancellableCoroutine { scheduleResumeAfterDelay(time, it) }
28-
}
29-
3020
/**
31-
* Schedules resume of a specified [continuation] after a specified delay [timeMillis].
21+
* Schedules resume of a specified [continuation] after a specified delay [time].
3222
*
3323
* Continuation **must be scheduled** to resume even if it is already cancelled, because a cancellation is just
3424
* an exception that the coroutine that used `delay` might wanted to catch and process. It might
@@ -42,28 +32,20 @@ public interface Delay {
4232
* with(continuation) { resumeUndispatchedWith(Unit) }
4333
* ```
4434
*/
45-
public fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>)
35+
public fun scheduleResumeAfterDelay(time: Duration, continuation: CancellableContinuation<Unit>)
4636

4737
/**
48-
* Schedules invocation of a specified [block] after a specified delay [timeMillis].
38+
* Schedules invocation of a specified [block] after a specified delay [timeout].
4939
* The resulting [DisposableHandle] can be used to [dispose][DisposableHandle.dispose] of this invocation
5040
* request if it is not needed anymore.
5141
*/
52-
public fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =
53-
DefaultDelay.invokeOnTimeout(timeMillis, block, context)
54-
}
42+
public fun invokeOnTimeout(timeout: Duration, block: Runnable, context: CoroutineContext): DisposableHandle =
43+
DefaultDelay.invokeOnTimeout(timeout, block, context)
5544

56-
/**
57-
* Enhanced [Delay] interface that provides additional diagnostics for [withTimeout].
58-
* Is going to be removed once there is proper JVM-default support.
59-
* Then we'll be able put this function into [Delay] without breaking binary compatibility.
60-
*/
61-
@InternalCoroutinesApi
62-
internal interface DelayWithTimeoutDiagnostics : Delay {
6345
/**
6446
* Returns a string that explains that the timeout has occurred, and explains what can be done about it.
6547
*/
66-
fun timeoutMessage(timeout: Duration): String
48+
fun timeoutMessage(timeout: Duration): String = "Timed out waiting for $timeout"
6749
}
6850

6951
/**
@@ -103,8 +85,8 @@ internal interface DelayWithTimeoutDiagnostics : Delay {
10385
public suspend fun awaitCancellation(): Nothing = suspendCancellableCoroutine {}
10486

10587
/**
106-
* Delays coroutine for at least the given time without blocking a thread and resumes it after a specified time.
107-
* If the given [timeMillis] is non-positive, this function returns immediately.
88+
* Delays coroutine for at least the given [duration] without blocking a thread and resumes it after the specified time.
89+
* If the given [duration] is non-positive, this function returns immediately.
10890
*
10991
* This suspending function is cancellable: if the [Job] of the current coroutine is cancelled while this
11092
* suspending function is waiting, this function immediately resumes with [CancellationException].
@@ -116,21 +98,20 @@ public suspend fun awaitCancellation(): Nothing = suspendCancellableCoroutine {}
11698
* Note that delay can be used in [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
11799
*
118100
* Implementation note: how exactly time is tracked is an implementation detail of [CoroutineDispatcher] in the context.
119-
* @param timeMillis time in milliseconds.
120101
*/
121-
public suspend fun delay(timeMillis: Long) {
122-
if (timeMillis <= 0) return // don't delay
102+
public suspend fun delay(duration: Duration) {
103+
if (duration <= Duration.ZERO) return // don't delay
123104
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
124-
// if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
125-
if (timeMillis < Long.MAX_VALUE) {
126-
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
105+
// instead of actually waiting for an infinite time, just wait forever like awaitCancellation, don't schedule.
106+
if (duration.isFinite()) {
107+
cont.context.delay.scheduleResumeAfterDelay(duration, cont)
127108
}
128109
}
129110
}
130111

131112
/**
132-
* Delays coroutine for at least the given [duration] without blocking a thread and resumes it after the specified time.
133-
* If the given [duration] is non-positive, this function returns immediately.
113+
* Delays coroutine for at least the given time without blocking a thread and resumes it after a specified time.
114+
* If the given [timeMillis] is non-positive, this function returns immediately.
134115
*
135116
* This suspending function is cancellable: if the [Job] of the current coroutine is cancelled while this
136117
* suspending function is waiting, this function immediately resumes with [CancellationException].
@@ -142,8 +123,11 @@ public suspend fun delay(timeMillis: Long) {
142123
* Note that delay can be used in [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
143124
*
144125
* Implementation note: how exactly time is tracked is an implementation detail of [CoroutineDispatcher] in the context.
126+
* @param timeMillis time in milliseconds.
145127
*/
146-
public suspend fun delay(duration: Duration): Unit = delay(duration.toDelayMillis())
128+
public suspend fun delay(timeMillis: Long) {
129+
delay(timeMillis.milliseconds)
130+
}
147131

148132
/** Returns [Delay] implementation of the given context */
149133
internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay
@@ -152,6 +136,7 @@ internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor)
152136
* Convert this duration to its millisecond value. Durations which have a nanosecond component less than
153137
* a single millisecond will be rounded up to the next largest millisecond.
154138
*/
139+
@PublishedApi
155140
internal fun Duration.toDelayMillis(): Long = when (isPositive()) {
156141
true -> plus(999_999L.nanoseconds).inWholeMilliseconds
157142
false -> 0L

Diff for: kotlinx-coroutines-core/common/src/EventLoop.common.kt

+5-16
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import kotlinx.coroutines.internal.*
55
import kotlin.concurrent.Volatile
66
import kotlin.coroutines.*
77
import kotlin.jvm.*
8+
import kotlin.time.Duration
89

910
/**
1011
* Extended by [CoroutineDispatcher] implementations that have event loop inside and can
@@ -144,24 +145,12 @@ private const val SCHEDULE_OK = 0
144145
private const val SCHEDULE_COMPLETED = 1
145146
private const val SCHEDULE_DISPOSED = 2
146147

147-
private const val MS_TO_NS = 1_000_000L
148-
private const val MAX_MS = Long.MAX_VALUE / MS_TO_NS
149-
150148
/**
151149
* First-line overflow protection -- limit maximal delay.
152150
* Delays longer than this one (~146 years) are considered to be delayed "forever".
153151
*/
154152
private const val MAX_DELAY_NS = Long.MAX_VALUE / 2
155153

156-
internal fun delayToNanos(timeMillis: Long): Long = when {
157-
timeMillis <= 0 -> 0L
158-
timeMillis >= MAX_MS -> Long.MAX_VALUE
159-
else -> timeMillis * MS_TO_NS
160-
}
161-
162-
internal fun delayNanosToMillis(timeNanos: Long): Long =
163-
timeNanos / MS_TO_NS
164-
165154
private val CLOSED_EMPTY = Symbol("CLOSED_EMPTY")
166155

167156
private typealias Queue<T> = LockFreeTaskQueueCore<T>
@@ -224,8 +213,8 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay {
224213
rescheduleAllDelayed()
225214
}
226215

227-
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
228-
val timeNanos = delayToNanos(timeMillis)
216+
override fun scheduleResumeAfterDelay(time: Duration, continuation: CancellableContinuation<Unit>) {
217+
val timeNanos = time.inWholeNanoseconds
229218
if (timeNanos < MAX_DELAY_NS) {
230219
val now = nanoTime()
231220
DelayedResumeTask(now + timeNanos, continuation).also { task ->
@@ -240,8 +229,8 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay {
240229
}
241230
}
242231

243-
protected fun scheduleInvokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle {
244-
val timeNanos = delayToNanos(timeMillis)
232+
protected fun scheduleInvokeOnTimeout(timeout: Duration, block: Runnable): DisposableHandle {
233+
val timeNanos = timeout.inWholeNanoseconds
245234
return if (timeNanos < MAX_DELAY_NS) {
246235
val now = nanoTime()
247236
DelayedRunnableTask(now + timeNanos, block).also { task ->

0 commit comments

Comments
 (0)