Skip to content

Commit de265b0

Browse files
Properly round Duration instances to milliseconds
Prior to this commit Durations used in for delays or timeouts lost their nanosecond granularity when being converted to a millisecond Long value. This effectively meant that delays could resume prior to when they were scheduled to do so. This commit solves this by rounding a Duration with nanosecond components up to the next largest millisecond. Closes Kotlin#3920
1 parent ed0cf7a commit de265b0

File tree

2 files changed

+55
-6
lines changed

2 files changed

+55
-6
lines changed

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ internal interface DelayWithTimeoutDiagnostics : Delay {
106106
public suspend fun awaitCancellation(): Nothing = suspendCancellableCoroutine {}
107107

108108
/**
109-
* Delays coroutine for a given time without blocking a thread and resumes it after a specified time.
109+
* Delays coroutine for at least the given time without blocking a thread and resumes it after a specified time.
110110
* If the given [timeMillis] is non-positive, this function returns immediately.
111111
*
112112
* This suspending function is cancellable.
@@ -133,7 +133,7 @@ public suspend fun delay(timeMillis: Long) {
133133
}
134134

135135
/**
136-
* Delays coroutine for a given [duration] without blocking a thread and resumes it after the specified time.
136+
* Delays coroutine for at least the given [duration] without blocking a thread and resumes it after the specified time.
137137
* If the given [duration] is non-positive, this function returns immediately.
138138
*
139139
* This suspending function is cancellable.
@@ -154,8 +154,15 @@ public suspend fun delay(duration: Duration): Unit = delay(duration.toDelayMilli
154154
internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay
155155

156156
/**
157-
* Convert this duration to its millisecond value.
158-
* Positive durations are coerced at least `1`.
157+
* Convert this duration to its millisecond value. Durations which have a nanosecond component less than
158+
* a single millisecond will be rounded up to the next largest millisecond.
159159
*/
160-
internal fun Duration.toDelayMillis(): Long =
161-
if (this > Duration.ZERO) inWholeMilliseconds.coerceAtLeast(1) else 0
160+
internal fun Duration.toDelayMillis(): Long {
161+
val millis = inWholeMilliseconds
162+
val nanosecondsInMillisecond = 1_000_000
163+
return when {
164+
this <= Duration.ZERO -> 0L
165+
millis * nanosecondsInMillisecond < inWholeNanoseconds -> millis + 1
166+
else -> millis
167+
}
168+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
package kotlinx.coroutines
5+
6+
import kotlin.test.*
7+
import kotlin.time.Duration.Companion.milliseconds
8+
import kotlin.time.Duration.Companion.nanoseconds
9+
import kotlin.time.Duration.Companion.seconds
10+
11+
class DurationToMillisTest {
12+
13+
@Test fun negative_duration_coerced_to_zero_millis() = assertEquals(
14+
expected = 0L,
15+
actual = (-1).seconds.toDelayMillis(),
16+
)
17+
18+
@Test fun zero_duration_coerced_to_zero_millis() = assertEquals(
19+
expected = 0L,
20+
actual = 0.seconds.toDelayMillis(),
21+
)
22+
23+
@Test fun one_nanosecond_coerced_to_one_millisecond() = assertEquals(
24+
expected = 1L,
25+
actual = 1.nanoseconds.toDelayMillis(),
26+
)
27+
28+
@Test fun one_second_coerced_to_1000_milliseconds() = assertEquals(
29+
expected = 1_000L,
30+
actual = 1.seconds.toDelayMillis(),
31+
)
32+
33+
@Test fun mixed_component_duration_rounded_up_to_next_millisecond() = assertEquals(
34+
expected = 999L,
35+
actual = (998.milliseconds + 75909.nanoseconds).toDelayMillis(),
36+
)
37+
38+
@Test fun one_extra_nanosecond_rounded_up_to_next_millisecond() = assertEquals(
39+
expected = 999L,
40+
actual = (998.milliseconds + 1.nanoseconds).toDelayMillis(),
41+
)
42+
}

0 commit comments

Comments
 (0)