Skip to content

Commit 34eaa5c

Browse files
committed
Add coercion rationale, improve test coverage
1 parent cc42d75 commit 34eaa5c

File tree

4 files changed

+168
-88
lines changed

4 files changed

+168
-88
lines changed

integration/kotlinx-coroutines-jdk8/src/time/Time.kt

+32-25
Original file line numberDiff line numberDiff line change
@@ -3,48 +3,55 @@
33
*/
44
package kotlinx.coroutines.time
55

6-
import kotlinx.coroutines.CoroutineScope
7-
import kotlinx.coroutines.selects.SelectBuilder
8-
import java.time.Duration
9-
import java.time.temporal.ChronoUnit
6+
import kotlinx.coroutines.*
7+
import kotlinx.coroutines.selects.*
8+
import java.time.*
9+
import java.time.temporal.*
1010

1111
/**
12-
* "java.time" adapter method for [kotlinx.coroutines.delay]
12+
* "java.time" adapter method for [kotlinx.coroutines.delay].
1313
*/
1414
public suspend fun delay(duration: Duration) =
15-
kotlinx.coroutines.delay(duration.toMillisDelay())
15+
kotlinx.coroutines.delay(duration.coerceToMillis())
1616

1717
/**
18-
* "java.time" adapter method for [SelectBuilder.onTimeout]
18+
* "java.time" adapter method for [SelectBuilder.onTimeout].
1919
*/
2020
public fun <R> SelectBuilder<R>.onTimeout(duration: Duration, block: suspend () -> R) =
21-
onTimeout(duration.toMillisDelay(), block)
21+
onTimeout(duration.coerceToMillis(), block)
2222

2323
/**
24-
* "java.time" adapter method for [kotlinx.coroutines.withTimeout]
24+
* "java.time" adapter method for [kotlinx.coroutines.withTimeout].
2525
*/
2626
public suspend fun <T> withTimeout(duration: Duration, block: suspend CoroutineScope.() -> T): T =
27-
kotlinx.coroutines.withTimeout(duration.toMillisDelay(), block)
27+
kotlinx.coroutines.withTimeout(duration.coerceToMillis(), block)
2828

2929
/**
30-
* "java.time" adapter method for [kotlinx.coroutines.withTimeoutOrNull]
30+
* "java.time" adapter method for [kotlinx.coroutines.withTimeoutOrNull].
3131
*/
3232
public suspend fun <T> withTimeoutOrNull(duration: Duration, block: suspend CoroutineScope.() -> T): T? =
33-
kotlinx.coroutines.withTimeoutOrNull(duration.toMillisDelay(), block)
33+
kotlinx.coroutines.withTimeoutOrNull(duration.coerceToMillis(), block)
3434

3535
/**
36-
* Convert the [Duration] to millisecond delay.
36+
* Coerces the given [Duration] to a millisecond delay.
37+
* Negative values are coerced to zero, values that cannot
38+
* be represented in milliseconds as long ("infinite" duration) are coerced to [Long.MAX_VALUE]
39+
* and durations lesser than a millisecond are coerced to 1 millisecond.
3740
*
38-
* @return strictly positive duration is coerced to 1..[Long.MAX_VALUE] ms, `0` otherwise.
41+
* The rationale of coercion:
42+
* 1) Too large durations typically indicate infinity and Long.MAX_VALUE is the
43+
* best approximation of infinity we can provide.
44+
* 2) Coercing too small durations to 1 instead of 0 is crucial for two patterns:
45+
* - Programming with deadlines and delays
46+
* - Non-suspending fast-paths (e.g. `withTimeout(1 nanosecond) { 42 }` should not throw)
3947
*/
40-
private fun Duration.toMillisDelay(): Long =
41-
if (this <= ChronoUnit.MILLIS.duration) {
42-
if (this <= Duration.ZERO) 0
43-
else 1
44-
} else {
45-
// values of Duration.ofMillis(Long.MAX_VALUE)
46-
val maxSeconds = 9223372036854775
47-
val maxNanos = 807000000
48-
if (seconds < maxSeconds || seconds == maxSeconds && nano < maxNanos) toMillis()
49-
else Long.MAX_VALUE
50-
}
48+
private fun Duration.coerceToMillis(): Long {
49+
if (isNegative) return 0
50+
if (this <= ChronoUnit.MILLIS.duration) return 1
51+
52+
// Maximum scalar values of Duration.ofMillis(Long.MAX_VALUE)
53+
val maxSeconds = 9223372036854775
54+
val maxNanos = 807000000
55+
return if (seconds < maxSeconds || seconds == maxSeconds && nano < maxNanos) toMillis()
56+
else Long.MAX_VALUE
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
package kotlinx.coroutines.time
6+
7+
import kotlinx.coroutines.*
8+
import kotlinx.coroutines.selects.*
9+
import org.junit.Test
10+
import java.time.*
11+
import java.time.temporal.*
12+
import kotlin.test.*
13+
14+
class DurationOverflowTest : TestBase() {
15+
16+
private val durations = ChronoUnit.values().map { it.duration }
17+
18+
@Test
19+
fun testDelay() = runTest {
20+
var counter = 0
21+
for (duration in durations) {
22+
expect(++counter)
23+
delay(duration.negated()) // Instant bail out from negative values
24+
launch(start = CoroutineStart.UNDISPATCHED) {
25+
expect(++counter)
26+
delay(duration)
27+
}.cancelAndJoin()
28+
expect(++counter)
29+
}
30+
31+
finish(++counter)
32+
}
33+
34+
@Test
35+
fun testOnTimeout() = runTest {
36+
for (duration in durations) {
37+
// Does not crash on overflows
38+
select<Unit> {
39+
onTimeout(duration) {}
40+
onTimeout(duration.negated()) {}
41+
}
42+
}
43+
}
44+
45+
@Test
46+
fun testWithTimeout() = runTest {
47+
for (duration in durations) {
48+
withTimeout(duration) {}
49+
}
50+
}
51+
52+
@Test
53+
fun testWithTimeoutOrNull() = runTest {
54+
for (duration in durations) {
55+
withTimeoutOrNull(duration) {}
56+
}
57+
}
58+
59+
@Test
60+
fun testWithTimeoutOrNullNegativeDuration() = runTest {
61+
val result = withTimeoutOrNull(Duration.ofSeconds(1).negated()) {
62+
1
63+
}
64+
65+
assertNull(result)
66+
}
67+
68+
}

integration/kotlinx-coroutines-jdk8/test/time/TimeTest.kt

-63
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.time
6+
7+
import kotlinx.coroutines.*
8+
import org.junit.Test
9+
import java.time.*
10+
import kotlin.test.*
11+
12+
class WithTimeoutTest : TestBase() {
13+
14+
@Test
15+
fun testWithTimeout() = runTest {
16+
expect(1)
17+
val result = withTimeout(Duration.ofMillis(10_000)) {
18+
expect(2)
19+
delay(Duration.ofNanos(1))
20+
expect(3)
21+
42
22+
}
23+
24+
assertEquals(42, result)
25+
finish(4)
26+
}
27+
28+
@Test
29+
fun testWithTimeoutOrNull() = runTest {
30+
expect(1)
31+
val result = withTimeoutOrNull(Duration.ofMillis(10_000)) {
32+
expect(2)
33+
delay(Duration.ofNanos(1))
34+
expect(3)
35+
42
36+
}
37+
38+
assertEquals(42, result)
39+
finish(4)
40+
}
41+
42+
@Test
43+
fun testWithTimeoutOrNullExceeded() = runTest {
44+
expect(1)
45+
val result = withTimeoutOrNull(Duration.ofMillis(3)) {
46+
expect(2)
47+
delay(Duration.ofSeconds(Long.MAX_VALUE))
48+
expectUnreached()
49+
}
50+
51+
assertNull(result)
52+
finish(3)
53+
}
54+
55+
@Test
56+
fun testWithTimeoutExceeded() = runTest {
57+
expect(1)
58+
try {
59+
withTimeout(Duration.ofMillis(3)) {
60+
expect(2)
61+
delay(Duration.ofSeconds(Long.MAX_VALUE))
62+
expectUnreached()
63+
}
64+
} catch (e: TimeoutCancellationException) {
65+
finish(3)
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)