Skip to content

Commit cce57c9

Browse files
fvascoelizarov
authored andcommitted
kotlin.time.Duration support
Fixes #1402
1 parent 4116fbf commit cce57c9

File tree

7 files changed

+705
-0
lines changed

7 files changed

+705
-0
lines changed

kotlinx-coroutines-core/common/src/Delay.kt

+28
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ package kotlinx.coroutines
66

77
import kotlinx.coroutines.selects.*
88
import kotlin.coroutines.*
9+
import kotlin.time.Duration
10+
import kotlin.time.ExperimentalTime
911

1012
/**
1113
* This dispatcher _feature_ is implemented by [CoroutineDispatcher] implementations that natively support
@@ -75,5 +77,31 @@ public suspend fun delay(timeMillis: Long) {
7577
}
7678
}
7779

80+
/**
81+
* Delays coroutine for a given [duration] without blocking a thread and resumes it after the specified time.
82+
* This suspending function is cancellable.
83+
* If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
84+
* immediately resumes with [CancellationException].
85+
*
86+
* Note that delay can be used in [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
87+
*
88+
* Implementation note: how exactly time is tracked is an implementation detail of [CoroutineDispatcher] in the context.
89+
*/
90+
@ExperimentalTime
91+
public suspend fun delay(duration: Duration) = delay(duration.toDelayMillis())
92+
7893
/** Returns [Delay] implementation of the given context */
7994
internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay
95+
96+
/**
97+
* Convert this duration to its millisecond value.
98+
* Positive durations are coerced at least `1`.
99+
*/
100+
@ExperimentalTime
101+
internal fun Duration.toDelayMillis(): Long =
102+
when {
103+
this > Duration.ZERO -> toLongMilliseconds().coerceAtLeast(1)
104+
this < Duration.ZERO -> -1
105+
// 0, -0, NaN
106+
else -> 0
107+
}

kotlinx-coroutines-core/common/src/Timeout.kt

+34
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import kotlinx.coroutines.selects.*
1010
import kotlin.coroutines.*
1111
import kotlin.coroutines.intrinsics.*
1212
import kotlin.jvm.*
13+
import kotlin.time.Duration
14+
import kotlin.time.ExperimentalTime
1315

1416
/**
1517
* Runs a given suspending [block] of code inside a coroutine with a specified [timeout][timeMillis] and throws
@@ -32,6 +34,22 @@ public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineSco
3234
}
3335
}
3436

37+
/**
38+
* Runs a given suspending [block] of code inside a coroutine with the specified [timeout] and throws
39+
* a [TimeoutCancellationException] if the timeout was exceeded.
40+
*
41+
* The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of
42+
* the cancellable suspending function inside the block throws a [TimeoutCancellationException].
43+
*
44+
* The sibling function that does not throw an exception on timeout is [withTimeoutOrNull].
45+
* Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
46+
*
47+
* Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
48+
*/
49+
@ExperimentalTime
50+
public suspend fun <T> withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T =
51+
withTimeout(timeout.toDelayMillis(), block)
52+
3553
/**
3654
* Runs a given suspending block of code inside a coroutine with a specified [timeout][timeMillis] and returns
3755
* `null` if this timeout was exceeded.
@@ -65,6 +83,22 @@ public suspend fun <T> withTimeoutOrNull(timeMillis: Long, block: suspend Corout
6583
}
6684
}
6785

86+
/**
87+
* Runs a given suspending block of code inside a coroutine with the specified [timeout] and returns
88+
* `null` if this timeout was exceeded.
89+
*
90+
* The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of
91+
* cancellable suspending function inside the block throws a [TimeoutCancellationException].
92+
*
93+
* The sibling function that throws an exception on timeout is [withTimeout].
94+
* Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
95+
*
96+
* Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
97+
*/
98+
@ExperimentalTime
99+
public suspend fun <T> withTimeoutOrNull(timeout: Duration, block: suspend CoroutineScope.() -> T): T? =
100+
withTimeoutOrNull(timeout.toDelayMillis(), block)
101+
68102
private fun <U, T: U> setupTimeout(
69103
coroutine: TimeoutCoroutine<U, T>,
70104
block: suspend CoroutineScope.() -> T

kotlinx-coroutines-core/common/src/selects/Select.kt

+14
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import kotlin.coroutines.*
1414
import kotlin.coroutines.intrinsics.*
1515
import kotlin.jvm.*
1616
import kotlin.native.concurrent.*
17+
import kotlin.time.Duration
18+
import kotlin.time.ExperimentalTime
1719

1820
/**
1921
* Scope for [select] invocation.
@@ -52,6 +54,18 @@ public interface SelectBuilder<in R> {
5254
public fun onTimeout(timeMillis: Long, block: suspend () -> R)
5355
}
5456

57+
58+
/**
59+
* Clause that selects the given [block] after the specified [timeout] passes.
60+
* If timeout is negative or zero, [block] is selected immediately.
61+
*
62+
* **Note: This is an experimental api.** It may be replaced with light-weight timer/timeout channels in the future.
63+
*/
64+
@ExperimentalCoroutinesApi
65+
@ExperimentalTime
66+
public fun <R> SelectBuilder<R>.onTimeout(timeout: Duration, block: suspend () -> R) =
67+
onTimeout(timeout.toDelayMillis(), block)
68+
5569
/**
5670
* Clause for [select] expression without additional parameters that does not select any value.
5771
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
@file:Suppress("NAMED_ARGUMENTS_NOT_ALLOWED", "DEPRECATION")
6+
7+
// KT-21913
8+
9+
package kotlinx.coroutines
10+
11+
import kotlin.test.Test
12+
import kotlin.time.*
13+
14+
@ExperimentalTime
15+
class DelayDurationTest : TestBase() {
16+
17+
@Test
18+
fun testCancellation() = runTest(expected = { it is CancellationException }) {
19+
runAndCancel(1.seconds)
20+
}
21+
22+
@Test
23+
fun testInfinite() = runTest(expected = { it is CancellationException }) {
24+
runAndCancel(Duration.INFINITE)
25+
}
26+
27+
@Test
28+
fun testRegularDelay() = runTest {
29+
val deferred = async {
30+
expect(2)
31+
delay(1.seconds)
32+
expect(4)
33+
}
34+
35+
expect(1)
36+
yield()
37+
expect(3)
38+
deferred.await()
39+
finish(5)
40+
}
41+
42+
@Test
43+
fun testNanoDelay() = runTest {
44+
val deferred = async {
45+
expect(2)
46+
delay(1.nanoseconds)
47+
expect(4)
48+
}
49+
50+
expect(1)
51+
yield()
52+
expect(3)
53+
deferred.await()
54+
finish(5)
55+
}
56+
57+
private suspend fun runAndCancel(time: Duration) = coroutineScope {
58+
expect(1)
59+
val deferred = async {
60+
expect(2)
61+
delay(time)
62+
expectUnreached()
63+
}
64+
65+
yield()
66+
expect(3)
67+
require(deferred.isActive)
68+
deferred.cancel()
69+
finish(4)
70+
deferred.await()
71+
}
72+
}

0 commit comments

Comments
 (0)