blob: e47035feb3228a3f7a21c22587e2cf654b8bf64a [file] [log] [blame]
Roman Elizarovf16fd272017-02-07 11:26:00 +03001/*
2 * Copyright 2016-2017 JetBrains s.r.o.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Roman Elizarov3754f952017-01-18 20:47:54 +030017package kotlinx.coroutines.experimental
18
Roman Elizarovaa461cf2018-04-11 13:20:29 +030019import kotlinx.coroutines.experimental.internalAnnotations.*
Roman Elizarov2adf8bc2018-01-24 20:09:57 +030020import kotlinx.coroutines.experimental.intrinsics.*
Roman Elizarovdd90bf12018-06-08 16:32:41 +030021import kotlinx.coroutines.experimental.selects.*
Roman Elizarovaa461cf2018-04-11 13:20:29 +030022import kotlinx.coroutines.experimental.timeunit.*
Roman Elizarov2adf8bc2018-01-24 20:09:57 +030023import kotlin.coroutines.experimental.*
24import kotlin.coroutines.experimental.intrinsics.*
Roman Elizarov3754f952017-01-18 20:47:54 +030025
Roman Elizarov3754f952017-01-18 20:47:54 +030026/**
Roman Elizarov507f5d42017-04-19 19:15:34 +030027 * Runs a given suspending [block] of code inside a coroutine with a specified timeout and throws
Roman Elizarov63f6ea22017-09-06 18:42:34 +030028 * [TimeoutCancellationException] if timeout was exceeded.
Roman Elizarovdaa1d9d2017-03-02 19:00:50 +030029 *
Roman Elizarovca9d5be2017-04-20 19:23:18 +030030 * The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of
Roman Elizarov8b38fa22017-09-27 17:44:31 +030031 * cancellable suspending function inside the block throws [TimeoutCancellationException].
32 * Even if the code in the block suppresses [TimeoutCancellationException], it
33 * is still thrown by `withTimeout` invocation.
Roman Elizarov507f5d42017-04-19 19:15:34 +030034 *
35 * The sibling function that does not throw exception on timeout is [withTimeoutOrNull].
36 * Note, that timeout action can be specified for [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
Roman Elizarov9d61b3e2017-04-19 18:32:11 +030037 *
Roman Elizarovdaa1d9d2017-03-02 19:00:50 +030038 * This function delegates to [Delay.invokeOnTimeout] if the context [CoroutineDispatcher]
39 * implements [Delay] interface, otherwise it tracks time using a built-in single-threaded scheduled executor service.
Roman Elizarov9d61b3e2017-04-19 18:32:11 +030040 *
Roman Elizarov19f48452017-12-21 18:48:20 +030041 * @param time timeout time in milliseconds.
42 */
Roman Elizarovaa461cf2018-04-11 13:20:29 +030043public suspend fun <T> withTimeout(time: Int, block: suspend CoroutineScope.() -> T): T =
Roman Elizarov19f48452017-12-21 18:48:20 +030044 withTimeout(time.toLong(), TimeUnit.MILLISECONDS, block)
45
46/**
47 * Runs a given suspending [block] of code inside a coroutine with a specified timeout and throws
48 * [TimeoutCancellationException] if timeout was exceeded.
49 *
50 * The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of
51 * cancellable suspending function inside the block throws [TimeoutCancellationException].
52 * Even if the code in the block suppresses [TimeoutCancellationException], it
53 * is still thrown by `withTimeout` invocation.
54 *
55 * The sibling function that does not throw exception on timeout is [withTimeoutOrNull].
56 * Note, that timeout action can be specified for [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
57 *
58 * This function delegates to [Delay.invokeOnTimeout] if the context [CoroutineDispatcher]
59 * implements [Delay] interface, otherwise it tracks time using a built-in single-threaded scheduled executor service.
60 *
Roman Elizarov9d61b3e2017-04-19 18:32:11 +030061 * @param time timeout time
62 * @param unit timeout unit (milliseconds by default)
Roman Elizarov3754f952017-01-18 20:47:54 +030063 */
Roman Elizarov8b38fa22017-09-27 17:44:31 +030064public suspend fun <T> withTimeout(time: Long, unit: TimeUnit = TimeUnit.MILLISECONDS, block: suspend CoroutineScope.() -> T): T {
Roman Elizarov3754f952017-01-18 20:47:54 +030065 if (time <= 0L) throw CancellationException("Timed out immediately")
Roman Elizarovca9d5be2017-04-20 19:23:18 +030066 return suspendCoroutineOrReturn { cont: Continuation<T> ->
Roman Elizarov8b38fa22017-09-27 17:44:31 +030067 setupTimeout(TimeoutCoroutine(time, unit, cont), block)
Roman Elizarov3754f952017-01-18 20:47:54 +030068 }
69}
Roman Elizarovdaa1d9d2017-03-02 19:00:50 +030070
Roman Elizarov8b38fa22017-09-27 17:44:31 +030071private fun <U, T: U> setupTimeout(
72 coroutine: TimeoutCoroutine<U, T>,
73 block: suspend CoroutineScope.() -> T
74): Any? {
75 // schedule cancellation of this coroutine on time
76 val cont = coroutine.cont
77 val context = cont.context
78 coroutine.disposeOnCompletion(context.delay.invokeOnTimeout(coroutine.time, coroutine.unit, coroutine))
Roman Elizarov8b38fa22017-09-27 17:44:31 +030079 // restart block using new coroutine with new job,
80 // however start it as undispatched coroutine, because we are already in the proper context
Roman Elizarov2adf8bc2018-01-24 20:09:57 +030081 return coroutine.startUndispatchedOrReturn(coroutine, block)
Roman Elizarov8b38fa22017-09-27 17:44:31 +030082}
83
84/**
85 * @suppress **Deprecated**: for binary compatibility only
86 */
87@Deprecated("for binary compatibility only", level=DeprecationLevel.HIDDEN)
88public suspend fun <T> withTimeout(time: Long, unit: TimeUnit = TimeUnit.MILLISECONDS, block: suspend () -> T): T =
89 withTimeout(time, unit) { block() }
90
91private open class TimeoutCoroutine<U, in T: U>(
92 @JvmField val time: Long,
93 @JvmField val unit: TimeUnit,
94 @JvmField val cont: Continuation<U>
95) : AbstractCoroutine<T>(cont.context, active = true), Runnable, Continuation<T> {
96 override val defaultResumeMode: Int get() = MODE_DIRECT
97
Roman Elizarove22f14a2017-06-22 19:04:52 +030098 @Suppress("LeakingThis")
Roman Elizarov8b38fa22017-09-27 17:44:31 +030099 override fun run() {
100 cancel(TimeoutCancellationException(time, unit, this))
101 }
102
103 @Suppress("UNCHECKED_CAST")
Roman Elizarovebc88662018-01-24 23:58:56 +0300104 internal override fun onCompletionInternal(state: Any?, mode: Int) {
Roman Elizarov6640b2b2018-01-17 19:08:55 +0300105 if (state is CompletedExceptionally)
Vsevolod Tolstopyatovc1092d52018-04-12 20:22:25 +0300106 cont.resumeWithExceptionMode(state.cause, mode)
Roman Elizarov8b38fa22017-09-27 17:44:31 +0300107 else
108 cont.resumeMode(state as T, mode)
109 }
110
111 override fun nameString(): String =
112 "${super.nameString()}($time $unit)"
Roman Elizarovca9d5be2017-04-20 19:23:18 +0300113}
114
Roman Elizarov507f5d42017-04-19 19:15:34 +0300115/**
116 * Runs a given suspending block of code inside a coroutine with a specified timeout and returns
Roman Elizarov607f8932017-06-09 11:36:21 +0300117 * `null` if this timeout was exceeded.
Roman Elizarov507f5d42017-04-19 19:15:34 +0300118 *
Roman Elizarovca9d5be2017-04-20 19:23:18 +0300119 * The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of
Roman Elizarov8b38fa22017-09-27 17:44:31 +0300120 * cancellable suspending function inside the block throws [TimeoutCancellationException].
121 * Even if the code in the block suppresses [TimeoutCancellationException], this
122 * invocation of `withTimeoutOrNull` still returns `null`.
Roman Elizarov507f5d42017-04-19 19:15:34 +0300123 *
124 * The sibling function that throws exception on timeout is [withTimeout].
125 * Note, that timeout action can be specified for [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
126 *
127 * This function delegates to [Delay.invokeOnTimeout] if the context [CoroutineDispatcher]
128 * implements [Delay] interface, otherwise it tracks time using a built-in single-threaded scheduled executor service.
129 *
Roman Elizarov19f48452017-12-21 18:48:20 +0300130 * @param time timeout time in milliseconds.
131 */
Roman Elizarovaa461cf2018-04-11 13:20:29 +0300132public suspend fun <T> withTimeoutOrNull(time: Int, block: suspend CoroutineScope.() -> T): T? =
Roman Elizarov19f48452017-12-21 18:48:20 +0300133 withTimeoutOrNull(time.toLong(), TimeUnit.MILLISECONDS, block)
134
135/**
136 * Runs a given suspending block of code inside a coroutine with a specified timeout and returns
137 * `null` if this timeout was exceeded.
138 *
139 * The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of
140 * cancellable suspending function inside the block throws [TimeoutCancellationException].
141 * Even if the code in the block suppresses [TimeoutCancellationException], this
142 * invocation of `withTimeoutOrNull` still returns `null`.
143 *
144 * The sibling function that throws exception on timeout is [withTimeout].
145 * Note, that timeout action can be specified for [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
146 *
147 * This function delegates to [Delay.invokeOnTimeout] if the context [CoroutineDispatcher]
148 * implements [Delay] interface, otherwise it tracks time using a built-in single-threaded scheduled executor service.
149 *
Roman Elizarov507f5d42017-04-19 19:15:34 +0300150 * @param time timeout time
151 * @param unit timeout unit (milliseconds by default)
152 */
Roman Elizarov8b38fa22017-09-27 17:44:31 +0300153public suspend fun <T> withTimeoutOrNull(time: Long, unit: TimeUnit = TimeUnit.MILLISECONDS, block: suspend CoroutineScope.() -> T): T? {
Roman Elizarov507f5d42017-04-19 19:15:34 +0300154 if (time <= 0L) return null
Roman Elizarovca9d5be2017-04-20 19:23:18 +0300155 return suspendCoroutineOrReturn { cont: Continuation<T?> ->
Roman Elizarov8b38fa22017-09-27 17:44:31 +0300156 setupTimeout(TimeoutOrNullCoroutine(time, unit, cont), block)
Roman Elizarov507f5d42017-04-19 19:15:34 +0300157 }
158}
159
Roman Elizarov8b38fa22017-09-27 17:44:31 +0300160/**
161 * @suppress **Deprecated**: for binary compatibility only
162 */
163@Deprecated("for binary compatibility only", level=DeprecationLevel.HIDDEN)
164public suspend fun <T> withTimeoutOrNull(time: Long, unit: TimeUnit = TimeUnit.MILLISECONDS, block: suspend () -> T): T? =
165 withTimeoutOrNull(time, unit) { block() }
166
167private class TimeoutOrNullCoroutine<T>(
Roman Elizarove22f14a2017-06-22 19:04:52 +0300168 time: Long,
169 unit: TimeUnit,
170 cont: Continuation<T?>
Roman Elizarov8b38fa22017-09-27 17:44:31 +0300171) : TimeoutCoroutine<T?, T>(time, unit, cont) {
172 @Suppress("UNCHECKED_CAST")
Roman Elizarovebc88662018-01-24 23:58:56 +0300173 internal override fun onCompletionInternal(state: Any?, mode: Int) {
Roman Elizarov6640b2b2018-01-17 19:08:55 +0300174 if (state is CompletedExceptionally) {
Vsevolod Tolstopyatovc1092d52018-04-12 20:22:25 +0300175 val exception = state.cause
Roman Elizarov8b38fa22017-09-27 17:44:31 +0300176 if (exception is TimeoutCancellationException && exception.coroutine === this)
177 cont.resumeMode(null, mode) else
178 cont.resumeWithExceptionMode(exception, mode)
179 } else
180 cont.resumeMode(state as T, mode)
Roman Elizarovca9d5be2017-04-20 19:23:18 +0300181 }
Roman Elizarovdaa1d9d2017-03-02 19:00:50 +0300182}
Roman Elizarov507f5d42017-04-19 19:15:34 +0300183
Roman Elizarovaa461cf2018-04-11 13:20:29 +0300184/**
185 * This exception is thrown by [withTimeout] to indicate timeout.
186 */
187public class TimeoutCancellationException internal constructor(
188 message: String,
189 @JvmField internal val coroutine: Job?
190) : CancellationException(message) {
191 /**
192 * Creates timeout exception with a given message.
193 */
194 public constructor(message: String) : this(message, null)
195}
196
197@Suppress("FunctionName")
198internal fun TimeoutCancellationException(
199 time: Long,
200 unit: TimeUnit,
201 coroutine: Job
202) : TimeoutCancellationException = TimeoutCancellationException("Timed out waiting for $time $unit", coroutine)