Skip to content

Commit 4cb5d19

Browse files
qwwdfsadelizarov
authored andcommitted
Allow negative timeouts in delay, withTimeout and onTimeout on JVM
Fixes #310
1 parent 590c888 commit 4cb5d19

File tree

8 files changed

+165
-12
lines changed

8 files changed

+165
-12
lines changed

common/kotlinx-coroutines-core-common/src/main/kotlin/kotlinx/coroutines/experimental/Delay.kt

-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ public interface Delay {
3535
* immediately resumes with [CancellationException].
3636
*/
3737
suspend fun delay(time: Long, unit: TimeUnit = TimeUnit.MILLISECONDS) {
38-
require(time >= 0) { "Delay time $time cannot be negative" }
3938
if (time <= 0) return // don't delay
4039
return suspendCancellableCoroutine { scheduleResumeAfterDelay(time, unit, it) }
4140
}
@@ -99,7 +98,6 @@ public suspend fun delay(time: Int) =
9998
* @param unit time unit.
10099
*/
101100
public suspend fun delay(time: Long, unit: TimeUnit = TimeUnit.MILLISECONDS) {
102-
require(time >= 0) { "Delay time $time cannot be negative" }
103101
if (time <= 0) return // don't delay
104102
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
105103
cont.context.delay.scheduleResumeAfterDelay(time, unit, cont)

common/kotlinx-coroutines-core-common/src/main/kotlin/kotlinx/coroutines/experimental/Scheduled.kt

-2
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ public suspend fun <T> withTimeout(time: Int, block: suspend CoroutineScope.() -
6161
* @param unit timeout unit (milliseconds by default)
6262
*/
6363
public suspend fun <T> withTimeout(time: Long, unit: TimeUnit = TimeUnit.MILLISECONDS, block: suspend CoroutineScope.() -> T): T {
64-
require(time >= 0) { "Timeout time $time cannot be negative" }
6564
if (time <= 0L) throw CancellationException("Timed out immediately")
6665
return suspendCoroutineOrReturn { cont: Continuation<T> ->
6766
setupTimeout(TimeoutCoroutine(time, unit, cont), block)
@@ -151,7 +150,6 @@ public suspend fun <T> withTimeoutOrNull(time: Int, block: suspend CoroutineScop
151150
* @param unit timeout unit (milliseconds by default)
152151
*/
153152
public suspend fun <T> withTimeoutOrNull(time: Long, unit: TimeUnit = TimeUnit.MILLISECONDS, block: suspend CoroutineScope.() -> T): T? {
154-
require(time >= 0) { "Timeout time $time cannot be negative" }
155153
if (time <= 0L) return null
156154
return suspendCoroutineOrReturn { cont: Continuation<T?> ->
157155
setupTimeout(TimeoutOrNullCoroutine(time, unit, cont), block)

common/kotlinx-coroutines-core-common/src/main/kotlin/kotlinx/coroutines/experimental/selects/Select.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public interface SelectBuilder<in R> {
5252

5353
/**
5454
* Clause that selects the given [block] after a specified timeout passes.
55+
* If timeout is negative or zero, [block] is selected immediately.
5556
*
5657
* @param time timeout time
5758
* @param unit timeout unit (milliseconds by default)
@@ -416,8 +417,7 @@ internal class SelectBuilderImpl<in R>(
416417
}
417418

418419
override fun onTimeout(time: Long, unit: TimeUnit, block: suspend () -> R) {
419-
require(time >= 0) { "Timeout time $time cannot be negative" }
420-
if (time == 0L) {
420+
if (time <= 0L) {
421421
if (trySelect(null))
422422
block.startCoroutineUndispatched(completion)
423423
return

common/kotlinx-coroutines-core-common/src/test/kotlin/kotlinx/coroutines/experimental/WithTimeoutOrNullTest.kt

+14
Original file line numberDiff line numberDiff line change
@@ -192,4 +192,18 @@ class WithTimeoutOrNullTest : TestBase() {
192192
}
193193

194194
private class TestException : Exception()
195+
196+
@Test
197+
fun testNegativeTimeout() = runTest {
198+
expect(1)
199+
var result = withTimeoutOrNull(-1) {
200+
expectUnreached()
201+
}
202+
assertNull(result)
203+
result = withTimeoutOrNull(0) {
204+
expectUnreached()
205+
}
206+
assertNull(result)
207+
finish(2)
208+
}
195209
}

common/kotlinx-coroutines-core-common/src/test/kotlin/kotlinx/coroutines/experimental/WithTimeoutTest.kt

+14
Original file line numberDiff line numberDiff line change
@@ -179,5 +179,19 @@ class WithTimeoutTest : TestBase() {
179179
}
180180

181181
private class TestException : Exception()
182+
183+
@Test
184+
fun testNegativeTimeout() = runTest {
185+
expect(1)
186+
try {
187+
withTimeout(-1) {
188+
expectUnreached()
189+
"OK"
190+
}
191+
} catch (e: CancellationException) {
192+
assertEquals("Timed out immediately", e.message)
193+
finish(2)
194+
}
195+
}
182196
}
183197

common/kotlinx-coroutines-core-common/src/test/kotlin/kotlinx/coroutines/experimental/selects/SelectTimeoutTest.kt

+60-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import kotlinx.coroutines.experimental.*
2020
import kotlin.test.*
2121

2222
class SelectTimeoutTest : TestBase() {
23+
2324
@Test
2425
fun testBasic() = runTest {
2526
expect(1)
@@ -40,4 +41,62 @@ class SelectTimeoutTest : TestBase() {
4041
assertEquals("OK", result)
4142
finish(3)
4243
}
43-
}
44+
45+
@Test
46+
fun testZeroTimeout() = runTest {
47+
expect(1)
48+
val result = select<String> {
49+
onTimeout(1000) {
50+
expectUnreached()
51+
"FAIL"
52+
}
53+
onTimeout(0) {
54+
expect(2)
55+
"OK"
56+
}
57+
}
58+
assertEquals("OK", result)
59+
finish(3)
60+
}
61+
62+
@Test
63+
fun testNegativeTimeout() = runTest {
64+
expect(1)
65+
val result = select<String> {
66+
onTimeout(1000) {
67+
expectUnreached()
68+
"FAIL"
69+
}
70+
onTimeout(-10) {
71+
expect(2)
72+
"OK"
73+
}
74+
}
75+
assertEquals("OK", result)
76+
finish(3)
77+
}
78+
79+
@Test
80+
fun testUnbiasedNegativeTimeout() = runTest {
81+
val counters = intArrayOf(0, 0, 0)
82+
val iterations =10_000
83+
for (i in 0..iterations) {
84+
val result = selectUnbiased<Int> {
85+
onTimeout(-10) {
86+
0
87+
}
88+
onTimeout(0) {
89+
1
90+
}
91+
onTimeout(10) {
92+
expectUnreached()
93+
2
94+
}
95+
}
96+
++counters[result]
97+
}
98+
assertEquals(0, counters[2])
99+
assertTrue { counters[0] > iterations / 4 }
100+
assertTrue { counters[1] > iterations / 4 }
101+
}
102+
}

core/kotlinx-coroutines-core/src/test/kotlin/kotlinx/coroutines/experimental/DelayTest.kt

+16-5
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ import org.hamcrest.core.IsEqual
2121
import org.junit.Test
2222
import java.util.concurrent.Executor
2323
import java.util.concurrent.Executors
24-
import kotlin.coroutines.experimental.AbstractCoroutineContextElement
25-
import kotlin.coroutines.experimental.Continuation
26-
import kotlin.coroutines.experimental.ContinuationInterceptor
27-
import kotlin.coroutines.experimental.CoroutineContext
24+
import kotlin.coroutines.experimental.*
2825

2926
class DelayTest : TestBase() {
3027
/**
@@ -47,7 +44,6 @@ class DelayTest : TestBase() {
4744
pool.shutdown()
4845
}
4946

50-
5147
@Test
5248
fun testDelayWithoutDispatcher() = runBlocking(CoroutineName("testNoDispatcher.main")) {
5349
// launch w/o a specified dispatcher
@@ -58,6 +54,21 @@ class DelayTest : TestBase() {
5854
assertThat(c.await(), IsEqual(42))
5955
}
6056

57+
@Test
58+
fun testNegativeDelay() = runBlocking {
59+
expect(1)
60+
val job = async(coroutineContext) {
61+
expect(3)
62+
delay(0)
63+
expect(4)
64+
}
65+
66+
delay(-1)
67+
expect(2)
68+
job.await()
69+
finish(5)
70+
}
71+
6172
class CustomInterceptor(val pool: Executor) : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
6273
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
6374
Wrapper(pool, continuation)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
2+
package kotlinx.coroutines.experimental
3+
4+
import kotlin.test.*
5+
import java.io.IOException
6+
7+
class WithTimeoutTest : TestBase() {
8+
@Test
9+
fun testExceptionOnTimeout() = runTest {
10+
expect(1)
11+
try {
12+
withTimeout(100) {
13+
expect(2)
14+
delay(1000)
15+
expectUnreached()
16+
"OK"
17+
}
18+
} catch (e: CancellationException) {
19+
assertEquals("Timed out waiting for 100 MILLISECONDS", e.message)
20+
finish(3)
21+
}
22+
}
23+
24+
@Test
25+
fun testSuppressExceptionWithResult() = runTest(
26+
expected = { it is CancellationException }
27+
) {
28+
expect(1)
29+
val result = withTimeout(100) {
30+
expect(2)
31+
try {
32+
delay(1000)
33+
} catch (e: CancellationException) {
34+
finish(3)
35+
}
36+
"OK"
37+
}
38+
expectUnreached()
39+
}
40+
41+
@Test
42+
fun testSuppressExceptionWithAnotherException() = runTest(
43+
expected = { it is IOException }
44+
) {
45+
expect(1)
46+
withTimeout(100) {
47+
expect(2)
48+
try {
49+
delay(1000)
50+
} catch (e: CancellationException) {
51+
finish(3)
52+
throw IOException(e)
53+
}
54+
expectUnreached()
55+
"OK"
56+
}
57+
expectUnreached()
58+
}
59+
}

0 commit comments

Comments
 (0)