Properly round `Duration` instances to milliseconds (#3921)

Prior to this commit Durations used for delays or timeouts
lost their nanosecond granularity during the conversion to a
millisecond Long value. This effectively meant that delays could
resume at most a millisecond in advance.

This commit solves this by rounding a Duration with nanosecond
components up to the next largest millisecond.

Fixes #3920
diff --git a/kotlinx-coroutines-core/common/src/Delay.kt b/kotlinx-coroutines-core/common/src/Delay.kt
index ba06d97..313e873 100644
--- a/kotlinx-coroutines-core/common/src/Delay.kt
+++ b/kotlinx-coroutines-core/common/src/Delay.kt
@@ -7,6 +7,7 @@
 import kotlinx.coroutines.selects.*
 import kotlin.coroutines.*
 import kotlin.time.*
+import kotlin.time.Duration.Companion.nanoseconds
 
 /**
  * This dispatcher _feature_ is implemented by [CoroutineDispatcher] implementations that natively support
@@ -106,7 +107,7 @@
 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.
@@ -133,7 +134,7 @@
 }
 
 /**
- * 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.
@@ -154,8 +155,10 @@
 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 = when (isPositive()) {
+    true -> plus(999_999L.nanoseconds).inWholeMilliseconds
+    false -> 0L
+}
diff --git a/kotlinx-coroutines-core/common/test/DurationToMillisTest.kt b/kotlinx-coroutines-core/common/test/DurationToMillisTest.kt
new file mode 100644
index 0000000..e2ea43d
--- /dev/null
+++ b/kotlinx-coroutines-core/common/test/DurationToMillisTest.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.*
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.nanoseconds
+import kotlin.time.Duration.Companion.seconds
+
+class DurationToMillisTest {
+
+    @Test
+    fun testNegativeDurationCoercedToZeroMillis() {
+        assertEquals(0L, (-1).seconds.toDelayMillis())
+    }
+
+    @Test
+    fun testZeroDurationCoercedToZeroMillis() {
+        assertEquals(0L, 0.seconds.toDelayMillis())
+    }
+
+    @Test
+    fun testOneNanosecondCoercedToOneMillisecond() {
+        assertEquals(1L, 1.nanoseconds.toDelayMillis())
+    }
+
+    @Test
+    fun testOneSecondCoercedTo1000Milliseconds() {
+        assertEquals(1_000L, 1.seconds.toDelayMillis())
+    }
+
+    @Test
+    fun testMixedComponentDurationRoundedUpToNextMillisecond() {
+        assertEquals(999L, (998.milliseconds + 75909.nanoseconds).toDelayMillis())
+    }
+
+    @Test
+    fun testOneExtraNanosecondRoundedUpToNextMillisecond() {
+        assertEquals(999L, (998.milliseconds + 1.nanoseconds).toDelayMillis())
+    }
+
+    @Test
+    fun testInfiniteDurationCoercedToLongMaxValue() {
+        assertEquals(Long.MAX_VALUE, Duration.INFINITE.toDelayMillis())
+    }
+
+    @Test
+    fun testNegativeInfiniteDurationCoercedToZero() {
+        assertEquals(0L, (-Duration.INFINITE).toDelayMillis())
+    }
+
+    @Test
+    fun testNanosecondOffByOneInfinityDoesNotOverflow() {
+        assertEquals(Long.MAX_VALUE / 1_000_000, (Long.MAX_VALUE - 1L).nanoseconds.toDelayMillis())
+    }
+
+    @Test
+    fun testMillisecondOffByOneInfinityDoesNotIncrement() {
+        assertEquals((Long.MAX_VALUE / 2) - 1, ((Long.MAX_VALUE / 2) - 1).milliseconds.toDelayMillis())
+    }
+
+    @Test
+    fun testOutOfBoundsNanosecondsButFiniteDoesNotIncrement() {
+        val milliseconds = Long.MAX_VALUE / 10
+        assertEquals(milliseconds, milliseconds.milliseconds.toDelayMillis())
+    }
+}