Skip to content

Commit ab31438

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

File tree

7 files changed

+681
-0
lines changed

7 files changed

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

0 commit comments

Comments
 (0)