Skip to content

Commit 59a3a60

Browse files
committed
Replace the exception in withTimeout
1 parent 4e25853 commit 59a3a60

File tree

4 files changed

+157
-4
lines changed

4 files changed

+157
-4
lines changed

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

+15-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44
@file:OptIn(ExperimentalContracts::class)
5+
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
56

67
package kotlinx.coroutines
78

9+
import kotlin.internal.*
810
import kotlinx.coroutines.internal.*
911
import kotlinx.coroutines.intrinsics.*
1012
import kotlinx.coroutines.selects.*
@@ -36,6 +38,12 @@ import kotlin.time.*
3638
*
3739
* @param timeMillis timeout time in milliseconds.
3840
*/
41+
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
42+
@LowPriorityInOverloadResolution
43+
@Deprecated("Use withTimeout from the 'kotlinx-coroutines-time' package instead.",
44+
ReplaceWith("kotlinx.coroutines.time.withTimeout(timeMillis, block)",
45+
"kotlinx.coroutines.time"),
46+
level = DeprecationLevel.WARNING)
3947
public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T): T {
4048
contract {
4149
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
@@ -66,11 +74,17 @@ public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineSco
6674
*
6775
* > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
6876
*/
77+
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
78+
@LowPriorityInOverloadResolution
79+
@Deprecated("Use withTimeout from the 'kotlinx-coroutines-time' package instead.",
80+
ReplaceWith("kotlinx.coroutines.time.withTimeout(timeout, block)",
81+
"kotlinx.coroutines.time"),
82+
level = DeprecationLevel.WARNING)
6983
public suspend fun <T> withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T {
7084
contract {
7185
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
7286
}
73-
return withTimeout(timeout.toDelayMillis(), block)
87+
return kotlinx.coroutines.time.withTimeout(timeout.toDelayMillis(), block)
7488
}
7589

7690
/**

kotlinx-coroutines-core/common/src/intrinsics/Undispatched.kt

+14
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package kotlinx.coroutines.intrinsics
66

77
import kotlinx.coroutines.*
88
import kotlinx.coroutines.internal.*
9+
import kotlinx.coroutines.time.*
910
import kotlin.coroutines.*
1011
import kotlin.coroutines.intrinsics.*
1112

@@ -101,6 +102,19 @@ internal fun <T, R> ScopeCoroutine<T>.startUndispatchedOrReturnIgnoreTimeout(
101102
}
102103
}
103104

105+
/**
106+
* Same as [startUndispatchedOrReturn], but ignores [TimeoutException] on fast-path.
107+
*/
108+
internal fun <T, R> ScopeCoroutine<T>.startUndispatchedOrReturnIgnoreNewTimeout(
109+
receiver: R, block: suspend R.() -> T
110+
): Any? {
111+
return undispatchedResult({ e ->
112+
!(e is TimeoutException && e.coroutine === this)
113+
}) {
114+
block.startCoroutineUninterceptedOrReturn(receiver, this)
115+
}
116+
}
117+
104118
private inline fun <T> ScopeCoroutine<T>.undispatchedResult(
105119
shouldThrow: (Throwable) -> Boolean,
106120
startBlock: () -> Any?
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
@file:OptIn(ExperimentalContracts::class)
6+
7+
package kotlinx.coroutines.time
8+
9+
import kotlinx.coroutines.*
10+
import kotlinx.coroutines.internal.*
11+
import kotlinx.coroutines.intrinsics.*
12+
import kotlinx.coroutines.selects.*
13+
import kotlin.contracts.*
14+
import kotlin.coroutines.*
15+
import kotlin.coroutines.intrinsics.*
16+
import kotlin.jvm.*
17+
import kotlin.time.*
18+
19+
/**
20+
* Runs a given suspending [block] of code inside a coroutine with a specified [timeout][timeMillis] and throws
21+
* a [TimeoutException] if the timeout was exceeded.
22+
* If the given [timeMillis] is non-positive, [TimeoutException] is thrown immediately.
23+
*
24+
* The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of
25+
* the cancellable suspending function inside the block throws a [TimeoutException].
26+
*
27+
* The sibling function that does not throw an exception on timeout is [withTimeoutOrNull].
28+
* Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
29+
*
30+
* **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time,
31+
* even right before the return from inside the timeout [block]. Keep this in mind if you open or acquire some
32+
* resource inside the [block] that needs closing or release outside the block.
33+
* See the
34+
* [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources]
35+
* section of the coroutines guide for details.
36+
*
37+
* > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
38+
*
39+
* @param timeMillis timeout time in milliseconds.
40+
*/
41+
public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T): T {
42+
contract {
43+
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
44+
}
45+
if (timeMillis <= 0L) throw TimeoutException("Timed out immediately")
46+
return suspendCoroutineUninterceptedOrReturn { uCont ->
47+
setupTimeout(TimeoutCoroutine(timeMillis, uCont), block)
48+
}
49+
}
50+
51+
/**
52+
* Runs a given suspending [block] of code inside a coroutine with the specified [timeout] and throws
53+
* a [TimeoutException] if the timeout was exceeded.
54+
* If the given [timeout] is non-positive, [TimeoutException] is thrown immediately.
55+
*
56+
* The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of
57+
* the cancellable suspending function inside the block throws a [TimeoutException].
58+
*
59+
* The sibling function that does not throw an exception on timeout is [withTimeoutOrNull].
60+
* Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
61+
*
62+
* **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time,
63+
* even right before the return from inside the timeout [block]. Keep this in mind if you open or acquire some
64+
* resource inside the [block] that needs closing or release outside the block.
65+
* See the
66+
* [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources]
67+
* section of the coroutines guide for details.
68+
*
69+
* > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
70+
*/
71+
public suspend fun <T> withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T {
72+
contract {
73+
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
74+
}
75+
return withTimeout(timeout.toDelayMillis(), block)
76+
}
77+
78+
private fun <U, T: U> setupTimeout(
79+
coroutine: TimeoutCoroutine<U, T>,
80+
block: suspend CoroutineScope.() -> T
81+
): Any? {
82+
// schedule cancellation of this coroutine on time
83+
val cont = coroutine.uCont
84+
val context = cont.context
85+
coroutine.disposeOnCompletion(context.delay.invokeOnTimeout(coroutine.time, coroutine, coroutine.context))
86+
// restart the block using a new coroutine with a new job,
87+
// however, start it undispatched, because we already are in the proper context
88+
return coroutine.startUndispatchedOrReturnIgnoreNewTimeout(coroutine, block)
89+
}
90+
91+
private class TimeoutCoroutine<U, in T: U>(
92+
@JvmField val time: Long,
93+
uCont: Continuation<U> // unintercepted continuation
94+
) : ScopeCoroutine<T>(uCont.context, uCont), Runnable {
95+
override fun run() {
96+
cancelCoroutine(TimeoutException(time, this))
97+
}
98+
99+
override fun nameString(): String =
100+
"${super.nameString()}(timeMillis=$time)"
101+
}
102+
103+
/**
104+
* This exception is thrown by [withTimeout] to indicate timeout.
105+
*/
106+
public class TimeoutException internal constructor(
107+
message: String,
108+
@JvmField @Transient internal val coroutine: Job?
109+
) : CancellationException(message), CopyableThrowable<TimeoutException> {
110+
/**
111+
* Creates a timeout exception with the given message.
112+
* This constructor is needed for exception stack-traces recovery.
113+
*/
114+
@Suppress("UNUSED")
115+
internal constructor(message: String) : this(message, null)
116+
117+
// message is never null in fact
118+
override fun createCopy(): TimeoutException =
119+
TimeoutException(message ?: "", coroutine).also { it.initCause(this) }
120+
}
121+
122+
internal fun TimeoutException(
123+
time: Long,
124+
coroutine: Job
125+
) : TimeoutException = TimeoutException("Timed out waiting for $time ms", coroutine)

kotlinx-coroutines-core/jdk8/src/time/Time.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public fun <T> Flow<T>.sample(period: Duration): Flow<T> = sample(period.coerceT
3333
* "java.time" adapter method for [SelectBuilder.onTimeout].
3434
*/
3535
public fun <R> SelectBuilder<R>.onTimeout(duration: Duration, block: suspend () -> R): Unit =
36-
onTimeout(duration.coerceToMillis(), block)
36+
onTimeout(duration.coerceToMillis(), block)
3737

3838
/**
3939
* "java.time" adapter method for [kotlinx.coroutines.withTimeout].
@@ -42,14 +42,14 @@ public suspend fun <T> withTimeout(duration: Duration, block: suspend CoroutineS
4242
contract {
4343
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
4444
}
45-
return kotlinx.coroutines.withTimeout(duration.coerceToMillis(), block)
45+
return withTimeout(duration.coerceToMillis(), block)
4646
}
4747

4848
/**
4949
* "java.time" adapter method for [kotlinx.coroutines.withTimeoutOrNull].
5050
*/
5151
public suspend fun <T> withTimeoutOrNull(duration: Duration, block: suspend CoroutineScope.() -> T): T? =
52-
kotlinx.coroutines.withTimeoutOrNull(duration.coerceToMillis(), block)
52+
withTimeoutOrNull(duration.coerceToMillis(), block)
5353

5454
/**
5555
* Coerces the given [Duration] to a millisecond delay.

0 commit comments

Comments
 (0)