1 /*
<lambda>null2 * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3 */
4 @file:OptIn(ExperimentalContracts::class)
5
6 package kotlinx.coroutines
7
8 import kotlinx.coroutines.internal.*
9 import kotlinx.coroutines.intrinsics.*
10 import kotlinx.coroutines.selects.*
11 import kotlin.contracts.*
12 import kotlin.coroutines.*
13 import kotlin.coroutines.intrinsics.*
14 import kotlin.jvm.*
15 import kotlin.time.*
16 import kotlin.time.Duration.Companion.milliseconds
17
18 /**
19 * Runs a given suspending [block] of code inside a coroutine with a specified [timeout][timeMillis] and throws
20 * a [TimeoutCancellationException] if the timeout was exceeded.
21 * If the given [timeMillis] is non-positive, [TimeoutCancellationException] is thrown immediately.
22 *
23 * The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of
24 * the cancellable suspending function inside the block throws a [TimeoutCancellationException].
25 *
26 * The sibling function that does not throw an exception on timeout is [withTimeoutOrNull].
27 * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
28 *
29 * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time,
30 * even right before the return from inside the timeout [block]. Keep this in mind if you open or acquire some
31 * resource inside the [block] that needs closing or release outside the block.
32 * See the
33 * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources]
34 * section of the coroutines guide for details.
35 *
36 * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
37 *
38 * @param timeMillis timeout time in milliseconds.
39 */
40 public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T): T {
41 contract {
42 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
43 }
44 if (timeMillis <= 0L) throw TimeoutCancellationException("Timed out immediately")
45 return suspendCoroutineUninterceptedOrReturn { uCont ->
46 setupTimeout(TimeoutCoroutine(timeMillis, uCont), block)
47 }
48 }
49
50 /**
51 * Runs a given suspending [block] of code inside a coroutine with the specified [timeout] and throws
52 * a [TimeoutCancellationException] if the timeout was exceeded.
53 * If the given [timeout] is non-positive, [TimeoutCancellationException] is thrown immediately.
54 *
55 * The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of
56 * the cancellable suspending function inside the block throws a [TimeoutCancellationException].
57 *
58 * The sibling function that does not throw an exception on timeout is [withTimeoutOrNull].
59 * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
60 *
61 * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time,
62 * even right before the return from inside the timeout [block]. Keep this in mind if you open or acquire some
63 * resource inside the [block] that needs closing or release outside the block.
64 * See the
65 * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources]
66 * section of the coroutines guide for details.
67 *
68 * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
69 */
withTimeoutnull70 public suspend fun <T> withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T {
71 contract {
72 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
73 }
74 return withTimeout(timeout.toDelayMillis(), block)
75 }
76
77 /**
78 * Runs a given suspending block of code inside a coroutine with a specified [timeout][timeMillis] and returns
79 * `null` if this timeout was exceeded.
80 * If the given [timeMillis] is non-positive, `null` is returned immediately.
81 *
82 * The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of
83 * cancellable suspending function inside the block throws a [TimeoutCancellationException].
84 *
85 * The sibling function that throws an exception on timeout is [withTimeout].
86 * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
87 *
88 * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time,
89 * even right before the return from inside the timeout [block]. Keep this in mind if you open or acquire some
90 * resource inside the [block] that needs closing or release outside the block.
91 * See the
92 * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources]
93 * section of the coroutines guide for details.
94 *
95 * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
96 *
97 * @param timeMillis timeout time in milliseconds.
98 */
withTimeoutOrNullnull99 public suspend fun <T> withTimeoutOrNull(timeMillis: Long, block: suspend CoroutineScope.() -> T): T? {
100 if (timeMillis <= 0L) return null
101
102 var coroutine: TimeoutCoroutine<T?, T?>? = null
103 try {
104 return suspendCoroutineUninterceptedOrReturn { uCont ->
105 val timeoutCoroutine = TimeoutCoroutine(timeMillis, uCont)
106 coroutine = timeoutCoroutine
107 setupTimeout<T?, T?>(timeoutCoroutine, block)
108 }
109 } catch (e: TimeoutCancellationException) {
110 // Return null if it's our exception, otherwise propagate it upstream (e.g. in case of nested withTimeouts)
111 if (e.coroutine === coroutine) {
112 return null
113 }
114 throw e
115 }
116 }
117
118 /**
119 * Runs a given suspending block of code inside a coroutine with the specified [timeout] and returns
120 * `null` if this timeout was exceeded.
121 * If the given [timeout] is non-positive, `null` is returned immediately.
122 *
123 * The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of
124 * cancellable suspending function inside the block throws a [TimeoutCancellationException].
125 *
126 * The sibling function that throws an exception on timeout is [withTimeout].
127 * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
128 *
129 * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time,
130 * even right before the return from inside the timeout [block]. Keep this in mind if you open or acquire some
131 * resource inside the [block] that needs closing or release outside the block.
132 * See the
133 * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources]
134 * section of the coroutines guide for details.
135 *
136 * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
137 */
withTimeoutOrNullnull138 public suspend fun <T> withTimeoutOrNull(timeout: Duration, block: suspend CoroutineScope.() -> T): T? =
139 withTimeoutOrNull(timeout.toDelayMillis(), block)
140
141 private fun <U, T : U> setupTimeout(
142 coroutine: TimeoutCoroutine<U, T>,
143 block: suspend CoroutineScope.() -> T
144 ): Any? {
145 // schedule cancellation of this coroutine on time
146 val cont = coroutine.uCont
147 val context = cont.context
148 coroutine.disposeOnCompletion(context.delay.invokeOnTimeout(coroutine.time, coroutine, coroutine.context))
149 // restart the block using a new coroutine with a new job,
150 // however, start it undispatched, because we already are in the proper context
151 return coroutine.startUndispatchedOrReturnIgnoreTimeout(coroutine, block)
152 }
153
154 private class TimeoutCoroutine<U, in T : U>(
155 @JvmField val time: Long,
156 uCont: Continuation<U> // unintercepted continuation
157 ) : ScopeCoroutine<T>(uCont.context, uCont), Runnable {
runnull158 override fun run() {
159 cancelCoroutine(TimeoutCancellationException(time, context.delay, this))
160 }
161
nameStringnull162 override fun nameString(): String =
163 "${super.nameString()}(timeMillis=$time)"
164 }
165
166 /**
167 * This exception is thrown by [withTimeout] to indicate timeout.
168 */
169 public class TimeoutCancellationException internal constructor(
170 message: String,
171 @JvmField @Transient internal val coroutine: Job?
172 ) : CancellationException(message), CopyableThrowable<TimeoutCancellationException> {
173 /**
174 * Creates a timeout exception with the given message.
175 * This constructor is needed for exception stack-traces recovery.
176 */
177 internal constructor(message: String) : this(message, null)
178
179 // message is never null in fact
180 override fun createCopy(): TimeoutCancellationException =
181 TimeoutCancellationException(message ?: "", coroutine).also { it.initCause(this) }
182 }
183
TimeoutCancellationExceptionnull184 internal fun TimeoutCancellationException(
185 time: Long,
186 delay: Delay,
187 coroutine: Job
188 ) : TimeoutCancellationException {
189 val message = (delay as? DelayWithTimeoutDiagnostics)?.timeoutMessage(time.milliseconds)
190 ?: "Timed out waiting for $time ms"
191 return TimeoutCancellationException(message, coroutine)
192 }
193