Skip to content

Commit ffb2d4a

Browse files
committed
make withDeadline throw TimeoutException
See Kotlin/kotlinx.coroutines#1374
1 parent 6bb75ac commit ffb2d4a

File tree

2 files changed

+54
-4
lines changed

2 files changed

+54
-4
lines changed

src/main/kotlin/ru/kontur/kinfra/commons/time/Deadline.kt

+27-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package ru.kontur.kinfra.commons.time
22

33
import kotlinx.coroutines.CoroutineScope
4-
import kotlinx.coroutines.TimeoutCancellationException
54
import kotlinx.coroutines.time.withTimeout
5+
import kotlinx.coroutines.time.withTimeoutOrNull
66
import kotlinx.coroutines.withContext
77
import ru.kontur.kinfra.commons.time.MonotonicInstant.Companion.now
88
import java.time.Duration
99
import java.time.temporal.ChronoUnit
10+
import java.util.*
11+
import java.util.concurrent.TimeoutException
12+
import kotlin.contracts.ExperimentalContracts
13+
import kotlin.contracts.InvocationKind
14+
import kotlin.contracts.contract
1015
import kotlin.coroutines.CoroutineContext
1116
import kotlin.coroutines.coroutineContext
1217

@@ -96,18 +101,31 @@ public class Deadline private constructor(
96101

97102
/**
98103
* Runs a given suspending [block] of code inside a coroutine with a specified [deadline][Deadline]
99-
* and throws a [TimeoutCancellationException] when the deadline passes.
104+
* and throws a [TimeoutException] when the deadline passes.
100105
*
101106
* If current deadline is less than the specified one, it will be used instead.
102107
*/
108+
@OptIn(ExperimentalContracts::class)
103109
public suspend fun <R> withDeadline(deadline: Deadline, block: suspend CoroutineScope.() -> R): R {
110+
contract {
111+
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
112+
}
113+
104114
val currentDeadline = coroutineContext[Deadline]
105115
val newDeadline = currentDeadline
106116
?.let { minOf(it, deadline) }
107117
?: deadline
108118

109-
return withTimeout(newDeadline.timeLeft()) {
110-
withContext(newDeadline, block)
119+
// withTimeout is not used because of https://github.com/Kotlin/kotlinx.coroutines/issues/1374
120+
val timeout = newDeadline.timeLeft()
121+
val result = withTimeoutOrNull(timeout) {
122+
Optional.ofNullable(withContext(newDeadline, block))
123+
}
124+
if (result != null) {
125+
// "unchecked" cast to nullable type
126+
return result.orElse(null)
127+
} else {
128+
throw TimeoutException("Timed out waiting for ${timeout.toMillis()} ms")
111129
}
112130
}
113131

@@ -117,7 +135,12 @@ public suspend fun <R> withDeadline(deadline: Deadline, block: suspend Coroutine
117135
* @see withDeadline
118136
* @see Deadline.after
119137
*/
138+
@OptIn(ExperimentalContracts::class)
120139
public suspend fun <R> withDeadlineAfter(timeout: Duration, block: suspend CoroutineScope.() -> R): R {
140+
contract {
141+
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
142+
}
143+
121144
val deadline = Deadline.after(timeout)
122145
return withDeadline(deadline, block)
123146
}

src/test/kotlin/ru/kontur/kinfra/commons/time/DeadlineTest.kt

+27
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package ru.kontur.kinfra.commons.time
22

3+
import kotlinx.coroutines.delay
34
import kotlinx.coroutines.runBlocking
45
import org.assertj.core.api.Assertions.assertThat
56
import org.junit.jupiter.api.Test
67
import org.junit.jupiter.api.assertThrows
78
import java.time.Duration
9+
import java.util.concurrent.TimeoutException
810

911
class DeadlineTest {
1012

@@ -50,6 +52,31 @@ class DeadlineTest {
5052
}
5153
}
5254

55+
@Test
56+
fun withDeadline_return_value(): Unit = runBlocking {
57+
val result = withDeadlineAfter(Duration.ofSeconds(1)) {
58+
"foo"
59+
}
60+
assertThat(result).isEqualTo("foo")
61+
}
62+
63+
@Test
64+
fun withDeadline_return_null_value(): Unit = runBlocking {
65+
val result = withDeadlineAfter<String?>(Duration.ofSeconds(1)) {
66+
null
67+
}
68+
assertThat(result).isNull()
69+
}
70+
71+
@Test
72+
fun withDeadline_timeout(): Unit = runBlocking {
73+
assertThrows<TimeoutException> {
74+
withDeadlineAfter(Duration.ofMillis(1)) {
75+
delay(2)
76+
}
77+
}
78+
}
79+
5380
@Test
5481
fun time_left_instantly() {
5582
val timeout = Duration.ofSeconds(1)

0 commit comments

Comments
 (0)