Skip to content

Commit d4098e9

Browse files
committed
Implement withTimeoutOrNull via withTimeout to avoid timing bugs and races.
Remove deprecated API Fixes #498
1 parent 8dd5b06 commit d4098e9

File tree

2 files changed

+46
-32
lines changed

2 files changed

+46
-32
lines changed

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

+14-32
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,6 @@ private fun <U, T: U> setupTimeout(
6969
return coroutine.startUndispatchedOrReturn(coroutine, block)
7070
}
7171

72-
/**
73-
* @suppress **Deprecated**: for binary compatibility only
74-
*/
75-
@Deprecated("for binary compatibility only", level=DeprecationLevel.HIDDEN)
76-
public suspend fun <T> withTimeout(time: Long, unit: TimeUnit = TimeUnit.MILLISECONDS, block: suspend () -> T): T =
77-
withTimeout(time, unit) { block() }
78-
7972
private open class TimeoutCoroutine<U, in T: U>(
8073
@JvmField val time: Long,
8174
@JvmField val unit: TimeUnit,
@@ -140,32 +133,21 @@ public suspend fun <T> withTimeoutOrNull(time: Int, block: suspend CoroutineScop
140133
*/
141134
public suspend fun <T> withTimeoutOrNull(time: Long, unit: TimeUnit = TimeUnit.MILLISECONDS, block: suspend CoroutineScope.() -> T): T? {
142135
if (time <= 0L) return null
143-
return suspendCoroutineUninterceptedOrReturn { uCont ->
144-
setupTimeout(TimeoutOrNullCoroutine(time, unit, uCont), block)
145-
}
146-
}
147-
148-
/**
149-
* @suppress **Deprecated**: for binary compatibility only
150-
*/
151-
@Deprecated("for binary compatibility only", level=DeprecationLevel.HIDDEN)
152-
public suspend fun <T> withTimeoutOrNull(time: Long, unit: TimeUnit = TimeUnit.MILLISECONDS, block: suspend () -> T): T? =
153-
withTimeoutOrNull(time, unit) { block() }
154136

155-
private class TimeoutOrNullCoroutine<T>(
156-
time: Long,
157-
unit: TimeUnit,
158-
uCont: Continuation<T?> // unintercepted continuation
159-
) : TimeoutCoroutine<T?, T>(time, unit, uCont) {
160-
@Suppress("UNCHECKED_CAST")
161-
internal override fun onCompletionInternal(state: Any?, mode: Int) {
162-
if (state is CompletedExceptionally) {
163-
val exception = state.cause
164-
if (exception is TimeoutCancellationException && exception.coroutine === this)
165-
uCont.resumeUninterceptedMode(null, mode) else
166-
uCont.resumeUninterceptedWithExceptionMode(exception, mode)
167-
} else
168-
uCont.resumeUninterceptedMode(state as T, mode)
137+
var coroutine: TimeoutCoroutine<T?, T?>? = null
138+
try {
139+
return suspendCoroutineUninterceptedOrReturn { uCont ->
140+
TimeoutCoroutine(time, unit, uCont).let {
141+
coroutine = it
142+
setupTimeout<T?, T?>(it, block)
143+
}
144+
}
145+
} catch (e: TimeoutCancellationException) {
146+
// Return null iff it's our exception, otherwise propagate it upstream (e.g. in case of nested withTimeouts)
147+
if (e.coroutine === coroutine) {
148+
return null
149+
}
150+
throw e
169151
}
170152
}
171153

common/kotlinx-coroutines-core-common/test/WithTimeoutOrNullTest.kt

+32
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77

88
package kotlinx.coroutines.experimental
99

10+
import kotlinx.coroutines.experimental.channels.*
1011
import kotlin.coroutines.experimental.*
1112
import kotlin.test.*
1213

1314
class WithTimeoutOrNullTest : TestBase() {
15+
1416
/**
1517
* Tests a case of no timeout and no suspension inside.
1618
*/
@@ -81,6 +83,23 @@ class WithTimeoutOrNullTest : TestBase() {
8183
finish(2)
8284
}
8385

86+
@Test
87+
fun testSmallTimeout() = runTest {
88+
val channel = Channel<Int>(1)
89+
val value = withTimeoutOrNull(1) {
90+
channel.receive()
91+
}
92+
93+
assertNull(value)
94+
}
95+
96+
@Test
97+
fun testThrowException() = runTest(expected = {it is AssertionError}) {
98+
withTimeoutOrNull(Long.MAX_VALUE) {
99+
throw AssertionError()
100+
}
101+
}
102+
84103
@Test
85104
fun testInnerTimeoutTest() = runTest(
86105
expected = { it is CancellationException }
@@ -96,6 +115,19 @@ class WithTimeoutOrNullTest : TestBase() {
96115
expectUnreached() // will timeout
97116
}
98117

118+
@Test
119+
fun testNestedTimeout() = runTest(expected = { it is TimeoutCancellationException }) {
120+
withTimeoutOrNull(Long.MAX_VALUE) {
121+
// Exception from this withTimeout is not suppressed by withTimeoutOrNull
122+
withTimeout(10) {
123+
delay(Long.MAX_VALUE)
124+
1
125+
}
126+
}
127+
128+
expectUnreached()
129+
}
130+
99131
@Test
100132
fun testOuterTimeoutTest() = runTest {
101133
var counter = 0

0 commit comments

Comments
 (0)