Skip to content

Commit

Permalink
Properly round Duration instances to milliseconds
Browse files Browse the repository at this point in the history
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
  • Loading branch information
kevincianfarini committed Oct 23, 2023
1 parent ed0cf7a commit de265b0
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 6 deletions.
19 changes: 13 additions & 6 deletions kotlinx-coroutines-core/common/src/Delay.kt
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ internal interface DelayWithTimeoutDiagnostics : Delay {
public suspend fun awaitCancellation(): Nothing = suspendCancellableCoroutine {}

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

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

/**
* Convert this duration to its millisecond value.
* Positive durations are coerced at least `1`.
* Convert this duration to its millisecond value. Durations which have a nanosecond component less than
* a single millisecond will be rounded up to the next largest millisecond.
*/
internal fun Duration.toDelayMillis(): Long =
if (this > Duration.ZERO) inWholeMilliseconds.coerceAtLeast(1) else 0
internal fun Duration.toDelayMillis(): Long {
val millis = inWholeMilliseconds
val nanosecondsInMillisecond = 1_000_000
return when {
this <= Duration.ZERO -> 0L
millis * nanosecondsInMillisecond < inWholeNanoseconds -> millis + 1
else -> millis
}
}
42 changes: 42 additions & 0 deletions kotlinx-coroutines-core/common/test/DurationToMillisTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.coroutines

import kotlin.test.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.Duration.Companion.seconds

class DurationToMillisTest {

@Test fun negative_duration_coerced_to_zero_millis() = assertEquals(
expected = 0L,
actual = (-1).seconds.toDelayMillis(),
)

@Test fun zero_duration_coerced_to_zero_millis() = assertEquals(
expected = 0L,
actual = 0.seconds.toDelayMillis(),
)

@Test fun one_nanosecond_coerced_to_one_millisecond() = assertEquals(
expected = 1L,
actual = 1.nanoseconds.toDelayMillis(),
)

@Test fun one_second_coerced_to_1000_milliseconds() = assertEquals(
expected = 1_000L,
actual = 1.seconds.toDelayMillis(),
)

@Test fun mixed_component_duration_rounded_up_to_next_millisecond() = assertEquals(
expected = 999L,
actual = (998.milliseconds + 75909.nanoseconds).toDelayMillis(),
)

@Test fun one_extra_nanosecond_rounded_up_to_next_millisecond() = assertEquals(
expected = 999L,
actual = (998.milliseconds + 1.nanoseconds).toDelayMillis(),
)
}

0 comments on commit de265b0

Please sign in to comment.