Skip to content

Commit 90e3285

Browse files
committed
kotlin.time.Duration support
Fixes #1402
1 parent bf9509d commit 90e3285

File tree

7 files changed

+677
-0
lines changed

7 files changed

+677
-0
lines changed

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

+15
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,18 @@ 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.toLongMilliseconds())
92+
7893
/** Returns [Delay] implementation of the given context */
7994
internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay

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.toLongMilliseconds(), 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.toLongMilliseconds(), 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.toLongMilliseconds(), 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,57 @@
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") // KT-21913
6+
7+
package kotlinx.coroutines
8+
9+
import kotlin.test.Test
10+
import kotlin.time.Duration
11+
import kotlin.time.ExperimentalTime
12+
import kotlin.time.milliseconds
13+
import kotlin.time.seconds
14+
15+
@ExperimentalTime
16+
class DelayDurationTest : TestBase() {
17+
18+
@Test
19+
fun testCancellation() = runTest(expected = { it is CancellationException }) {
20+
runAndCancel(1.seconds)
21+
}
22+
23+
@Test
24+
fun testInfinite() = runTest(expected = { it is CancellationException }) {
25+
runAndCancel(Duration.INFINITE)
26+
}
27+
28+
@Test
29+
fun testRegularDelay() = runTest {
30+
val deferred = async {
31+
expect(2)
32+
delay(1.milliseconds)
33+
expect(3)
34+
}
35+
36+
expect(1)
37+
yield()
38+
deferred.await()
39+
finish(4)
40+
}
41+
42+
private suspend fun runAndCancel(time: Duration) = coroutineScope {
43+
expect(1)
44+
val deferred = async {
45+
expect(2)
46+
delay(time)
47+
expectUnreached()
48+
}
49+
50+
yield()
51+
expect(3)
52+
require(deferred.isActive)
53+
deferred.cancel()
54+
finish(4)
55+
deferred.await()
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
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", "UNREACHABLE_CODE") // KT-21913
6+
7+
package kotlinx.coroutines
8+
9+
import kotlin.test.*
10+
import kotlin.time.Duration
11+
import kotlin.time.ExperimentalTime
12+
import kotlin.time.milliseconds
13+
import kotlin.time.seconds
14+
15+
@ExperimentalTime
16+
class WithTimeoutDurationTest : TestBase() {
17+
/**
18+
* Tests a case of no timeout and no suspension inside.
19+
*/
20+
@Test
21+
fun testBasicNoSuspend() = runTest {
22+
expect(1)
23+
val result = withTimeout(10.seconds) {
24+
expect(2)
25+
"OK"
26+
}
27+
assertEquals("OK", result)
28+
finish(3)
29+
}
30+
31+
/**
32+
* Tests a case of no timeout and one suspension inside.
33+
*/
34+
@Test
35+
fun testBasicSuspend() = runTest {
36+
expect(1)
37+
val result = withTimeout(10.seconds) {
38+
expect(2)
39+
yield()
40+
expect(3)
41+
"OK"
42+
}
43+
assertEquals("OK", result)
44+
finish(4)
45+
}
46+
47+
/**
48+
* Tests proper dispatching of `withTimeout` blocks
49+
*/
50+
@Test
51+
fun testDispatch() = runTest {
52+
expect(1)
53+
launch {
54+
expect(4)
55+
yield() // back to main
56+
expect(7)
57+
}
58+
expect(2)
59+
// test that it does not yield to the above job when started
60+
val result = withTimeout(1.seconds) {
61+
expect(3)
62+
yield() // yield only now
63+
expect(5)
64+
"OK"
65+
}
66+
assertEquals("OK", result)
67+
expect(6)
68+
yield() // back to launch
69+
finish(8)
70+
}
71+
72+
73+
/**
74+
* Tests that a 100% CPU-consuming loop will react on timeout if it has yields.
75+
*/
76+
@Test
77+
fun testYieldBlockingWithTimeout() = runTest(
78+
expected = { it is CancellationException }
79+
) {
80+
withTimeout(100.milliseconds) {
81+
while (true) {
82+
yield()
83+
}
84+
}
85+
}
86+
87+
/**
88+
* Tests that [withTimeout] waits for children coroutines to complete.
89+
*/
90+
@Test
91+
fun testWithTimeoutChildWait() = runTest {
92+
expect(1)
93+
withTimeout(100.milliseconds) {
94+
expect(2)
95+
// launch child with timeout
96+
launch {
97+
expect(4)
98+
}
99+
expect(3)
100+
// now will wait for child before returning
101+
}
102+
finish(5)
103+
}
104+
105+
@Test
106+
fun testBadClass() = runTest {
107+
val bad = BadClass()
108+
val result = withTimeout(100.milliseconds) {
109+
bad
110+
}
111+
assertSame(bad, result)
112+
}
113+
114+
class BadClass {
115+
override fun equals(other: Any?): Boolean = error("Should not be called")
116+
override fun hashCode(): Int = error("Should not be called")
117+
override fun toString(): String = error("Should not be called")
118+
}
119+
120+
@Test
121+
fun testExceptionOnTimeout() = runTest {
122+
expect(1)
123+
try {
124+
withTimeout(100.milliseconds) {
125+
expect(2)
126+
delay(1000.milliseconds)
127+
expectUnreached()
128+
"OK"
129+
}
130+
} catch (e: CancellationException) {
131+
assertEquals("Timed out waiting for 100 ms", e.message)
132+
finish(3)
133+
}
134+
}
135+
136+
@Test
137+
fun testSuppressExceptionWithResult() = runTest(
138+
expected = { it is CancellationException }
139+
) {
140+
expect(1)
141+
withTimeout(100.milliseconds) {
142+
expect(2)
143+
try {
144+
delay(1000.milliseconds)
145+
} catch (e: CancellationException) {
146+
finish(3)
147+
}
148+
"OK"
149+
}
150+
expectUnreached()
151+
}
152+
153+
@Test
154+
fun testSuppressExceptionWithAnotherException() = runTest {
155+
expect(1)
156+
try {
157+
withTimeout(100.milliseconds) {
158+
expect(2)
159+
try {
160+
delay(1000.milliseconds)
161+
} catch (e: CancellationException) {
162+
expect(3)
163+
throw TestException()
164+
}
165+
expectUnreached()
166+
"OK"
167+
}
168+
expectUnreached()
169+
} catch (e: TestException) {
170+
finish(4)
171+
}
172+
}
173+
174+
@Test
175+
fun testNegativeTimeout() = runTest {
176+
expect(1)
177+
try {
178+
withTimeout(-1.milliseconds) {
179+
expectUnreached()
180+
"OK"
181+
}
182+
} catch (e: TimeoutCancellationException) {
183+
assertEquals("Timed out immediately", e.message)
184+
finish(2)
185+
}
186+
}
187+
188+
@Test
189+
fun testExceptionFromWithinTimeout() = runTest {
190+
expect(1)
191+
try {
192+
expect(2)
193+
withTimeout(1.seconds) {
194+
expect(3)
195+
throw TestException()
196+
}
197+
expectUnreached()
198+
} catch (e: TestException) {
199+
finish(4)
200+
}
201+
}
202+
203+
@Test
204+
fun testIncompleteWithTimeoutState() = runTest {
205+
lateinit var timeoutJob: Job
206+
val handle = withTimeout(Duration.INFINITE) {
207+
timeoutJob = coroutineContext[Job]!!
208+
timeoutJob.invokeOnCompletion { }
209+
}
210+
211+
handle.dispose()
212+
timeoutJob.join()
213+
assertTrue(timeoutJob.isCompleted)
214+
assertFalse(timeoutJob.isActive)
215+
assertFalse(timeoutJob.isCancelled)
216+
}
217+
}

0 commit comments

Comments
 (0)