Skip to content

Introduce migration path for long-standing issue of withTimeout #4356

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions kotlinx-coroutines-core/api/kotlinx-coroutines-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -1411,3 +1411,13 @@ public final class kotlinx/coroutines/time/TimeKt {
public static final fun withTimeoutOrNull (Ljava/time/Duration;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class kotlinx/coroutines/timeout/TimeoutException : java/util/concurrent/TimeoutException, kotlinx/coroutines/CopyableThrowable {
public synthetic fun createCopy ()Ljava/lang/Throwable;
public fun createCopy ()Lkotlinx/coroutines/timeout/TimeoutException;
}

public final class kotlinx/coroutines/timeout/TimeoutsKt {
public static final fun withTimeout (JLkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun withTimeout-KLykuaI (JLkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

4 changes: 4 additions & 0 deletions kotlinx-coroutines-core/api/kotlinx-coroutines-core.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,8 @@ final class kotlinx.coroutines.channels/ClosedSendChannelException : kotlin/Ille
constructor <init>(kotlin/String?) // kotlinx.coroutines.channels/ClosedSendChannelException.<init>|<init>(kotlin.String?){}[0]
}

final class kotlinx.coroutines.timeout/TimeoutException : kotlin/Exception // kotlinx.coroutines.timeout/TimeoutException|null[0]

final class kotlinx.coroutines/CompletionHandlerException : kotlin/RuntimeException { // kotlinx.coroutines/CompletionHandlerException|null[0]
constructor <init>(kotlin/String, kotlin/Throwable) // kotlinx.coroutines/CompletionHandlerException.<init>|<init>(kotlin.String;kotlin.Throwable){}[0]
}
Expand Down Expand Up @@ -1019,6 +1021,8 @@ final suspend fun <#A: kotlin/Any?> (kotlinx.coroutines.flow/Flow<#A>).kotlinx.c
final suspend fun <#A: kotlin/Any?> (kotlinx.coroutines.flow/Flow<#A>).kotlinx.coroutines.flow/toSet(kotlin.collections/MutableSet<#A> = ...): kotlin.collections/Set<#A> // kotlinx.coroutines.flow/toSet|[email protected]<0:0>(kotlin.collections.MutableSet<0:0>){0§<kotlin.Any?>}[0]
final suspend fun <#A: kotlin/Any?> (kotlinx.coroutines.flow/FlowCollector<#A>).kotlinx.coroutines.flow/emitAll(kotlinx.coroutines.channels/ReceiveChannel<#A>) // kotlinx.coroutines.flow/emitAll|[email protected]<0:0>(kotlinx.coroutines.channels.ReceiveChannel<0:0>){0§<kotlin.Any?>}[0]
final suspend fun <#A: kotlin/Any?> (kotlinx.coroutines.flow/FlowCollector<#A>).kotlinx.coroutines.flow/emitAll(kotlinx.coroutines.flow/Flow<#A>) // kotlinx.coroutines.flow/emitAll|[email protected]<0:0>(kotlinx.coroutines.flow.Flow<0:0>){0§<kotlin.Any?>}[0]
final suspend fun <#A: kotlin/Any?> kotlinx.coroutines.timeout/withTimeout(kotlin.time/Duration, kotlin.coroutines/SuspendFunction1<kotlinx.coroutines/CoroutineScope, #A>): #A // kotlinx.coroutines.timeout/withTimeout|withTimeout(kotlin.time.Duration;kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,0:0>){0§<kotlin.Any?>}[0]
final suspend fun <#A: kotlin/Any?> kotlinx.coroutines.timeout/withTimeout(kotlin/Long, kotlin.coroutines/SuspendFunction1<kotlinx.coroutines/CoroutineScope, #A>): #A // kotlinx.coroutines.timeout/withTimeout|withTimeout(kotlin.Long;kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,0:0>){0§<kotlin.Any?>}[0]
final suspend fun <#A: kotlin/Any?> kotlinx.coroutines/awaitAll(kotlin/Array<out kotlinx.coroutines/Deferred<#A>>...): kotlin.collections/List<#A> // kotlinx.coroutines/awaitAll|awaitAll(kotlin.Array<out|kotlinx.coroutines.Deferred<0:0>>...){0§<kotlin.Any?>}[0]
final suspend fun <#A: kotlin/Any?> kotlinx.coroutines/coroutineScope(kotlin.coroutines/SuspendFunction1<kotlinx.coroutines/CoroutineScope, #A>): #A // kotlinx.coroutines/coroutineScope|coroutineScope(kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,0:0>){0§<kotlin.Any?>}[0]
final suspend fun <#A: kotlin/Any?> kotlinx.coroutines/supervisorScope(kotlin.coroutines/SuspendFunction1<kotlinx.coroutines/CoroutineScope, #A>): #A // kotlinx.coroutines/supervisorScope|supervisorScope(kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,0:0>){0§<kotlin.Any?>}[0]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
@file:OptIn(ExperimentalContracts::class)
@file:Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND")

/*
* Note: the package is different from the folder structure on purpose,
* to simplify tracking of https://github.com/Kotlin/kotlinx.coroutines/issues/1374
* and to help users to find the right symbol in the IDE.
*/
package kotlinx.coroutines

import kotlinx.coroutines.internal.*
Expand Down Expand Up @@ -35,13 +40,15 @@ import kotlin.time.Duration.Companion.milliseconds
*
* @param timeMillis timeout time in milliseconds.
*/
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@kotlin.internal.LowPriorityInOverloadResolution
public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
if (timeMillis <= 0L) throw TimeoutCancellationException("Timed out immediately")
return suspendCoroutineUninterceptedOrReturn { uCont ->
setupTimeout(TimeoutCoroutine(timeMillis, uCont), block)
setupTimeout(TimeoutLegacyCoroutine(timeMillis, uCont), block)
}
}

Expand All @@ -65,6 +72,8 @@ public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineSco
*
* > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
*/
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@kotlin.internal.LowPriorityInOverloadResolution
public suspend fun <T> withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
Expand Down Expand Up @@ -97,10 +106,10 @@ public suspend fun <T> withTimeout(timeout: Duration, block: suspend CoroutineSc
public suspend fun <T> withTimeoutOrNull(timeMillis: Long, block: suspend CoroutineScope.() -> T): T? {
if (timeMillis <= 0L) return null

var coroutine: TimeoutCoroutine<T?, T?>? = null
var coroutine: TimeoutLegacyCoroutine<T?, T?>? = null
try {
return suspendCoroutineUninterceptedOrReturn { uCont ->
val timeoutCoroutine = TimeoutCoroutine(timeMillis, uCont)
val timeoutCoroutine = TimeoutLegacyCoroutine(timeMillis, uCont)
coroutine = timeoutCoroutine
setupTimeout<T?, T?>(timeoutCoroutine, block)
}
Expand Down Expand Up @@ -136,8 +145,8 @@ public suspend fun <T> withTimeoutOrNull(timeMillis: Long, block: suspend Corout
public suspend fun <T> withTimeoutOrNull(timeout: Duration, block: suspend CoroutineScope.() -> T): T? =
withTimeoutOrNull(timeout.toDelayMillis(), block)

private fun <U, T : U> setupTimeout(
coroutine: TimeoutCoroutine<U, T>,
internal fun <U, T : U> setupTimeout(
coroutine: TimeoutCoroutineBase<U, T>,
block: suspend CoroutineScope.() -> T
): Any? {
// schedule cancellation of this coroutine on time
Expand All @@ -149,18 +158,27 @@ private fun <U, T : U> setupTimeout(
return coroutine.startUndispatchedOrReturnIgnoreTimeout(coroutine, block)
}

private class TimeoutCoroutine<U, in T : U>(
internal abstract class TimeoutCoroutineBase<U, in T : U>(
@JvmField val time: Long,
uCont: Continuation<U> // unintercepted continuation
) : ScopeCoroutine<T>(uCont.context, uCont), Runnable {
override fun run() {
cancelCoroutine(TimeoutCancellationException(time, context.delay, this))
cancelCoroutine(timeoutException())
}

internal abstract fun timeoutException(): Throwable

override fun nameString(): String =
"${super.nameString()}(timeMillis=$time)"
}

internal class TimeoutLegacyCoroutine<U, in T : U>(
time: Long,
uCont: Continuation<U> // unintercepted continuation
) : TimeoutCoroutineBase<U, T>(time, uCont) {
override fun timeoutException(): Throwable = TimeoutCancellationException(time, context.delay, this)
}

/**
* This exception is thrown by [withTimeout] to indicate timeout.
*/
Expand All @@ -183,7 +201,7 @@ internal fun TimeoutCancellationException(
time: Long,
delay: Delay,
coroutine: Job
) : TimeoutCancellationException {
): TimeoutCancellationException {
val message = (delay as? DelayWithTimeoutDiagnostics)?.timeoutMessage(time.milliseconds)
?: "Timed out waiting for $time ms"
return TimeoutCancellationException(message, coroutine)
Expand Down
81 changes: 81 additions & 0 deletions kotlinx-coroutines-core/common/src/timeout/Timeouts.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
@file:OptIn(ExperimentalContracts::class)
@file:Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND")

package kotlinx.coroutines.timeout

import kotlinx.coroutines.*
import kotlin.contracts.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds

/**
* [kotlinx.coroutines.withTimeout] but better
*/
public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
if (timeMillis <= 0L) throw TimeoutException("Timed out immediately")
return suspendCoroutineUninterceptedOrReturn { uCont ->
setupTimeout(TimeoutCoroutine(timeMillis, uCont), block)
}
}

internal class TimeoutCoroutine<U, in T : U>(
time: Long,
uCont: Continuation<U> // unintercepted continuation
) : TimeoutCoroutineBase<U, T>(time, uCont) {
override fun timeoutException(): Throwable = TimeoutException(time, context.delay, this)
}

/**
* [kotlinx.coroutines.withTimeout] but better
*/
public suspend fun <T> withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return withTimeout(timeout.toDelayMillis(), block)
}

/**
* This exception is thrown by [withTimeout] to indicate timeout.
*
* Example of usage:
* ```
* suspend fun main() {
* try {
* val result = withTimeout(100.milliseconds) {
* println("Executing long-running operation")
* delay(1.seconds) // Pretending to be slow operation
* 42
* }
* println("Computation result: $result") // Never printed
* } catch (e: TimeoutException) {
* println("Computation failed: ${e.message}")
* }
* }
* ```
*
* ### Implementation note
*
* On the JVM platform, this exception extends `java.util.concurrent.TimeoutException`.
* The main purpose of that is to make `java.util.concurrent.TimeoutException` and `kotlinx.coroutines.TimeoutException`
* interchangeable from the user perspective (i.e. any of them can be caught) and thus less error-prone,
* while allowing the implementation to store auxilary data along with the exception.
*/
public expect class TimeoutException internal constructor(message: String, coroutine: Job?) : Exception {
internal constructor(message: String)
}

private fun TimeoutException(
time: Long,
delay: Delay,
coroutine: Job
): TimeoutException {
val message = (delay as? DelayWithTimeoutDiagnostics)?.timeoutMessage(time.milliseconds)
?: "Timed out waiting for $time ms"
return TimeoutException(message, coroutine)
}
30 changes: 30 additions & 0 deletions kotlinx-coroutines-core/common/test/WithTimeoutAmbiguityTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package kotlinx.coroutines.disambiguation

import kotlinx.coroutines.testing.TestBase
import kotlinx.coroutines.*
import kotlinx.coroutines.timeout.*
import kotlin.test.Test
import kotlin.time.Duration.Companion.seconds

class WithTimeoutAmbiguityTest : TestBase() {

// The test fails without @LowPriorityInOverloadResolution on the obsolete timeout method
@Test
fun testUnambiguousWithStarImports() = runTest {
expect(1)
// Use 'withTimeoutOrNull'
withTimeoutOrNull(100.seconds) {
expect(2)
"OK"
}
try {
expect(3)
withTimeout(1) {
delay(100.seconds)
}
expectUnreached()
} catch (e: TimeoutException) {
finish(4)
}
}
}
2 changes: 1 addition & 1 deletion kotlinx-coroutines-core/common/test/WithTimeoutTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ class WithTimeoutTest : TestBase() {
}

@Test
fun testSuppressExceptionWithAnotherException() = runTest{
fun testSuppressExceptionWithAnotherException() = runTest {
expect(1)
try {
withTimeout(100) {
Expand Down
11 changes: 11 additions & 0 deletions kotlinx-coroutines-core/jsAndWasmShared/src/Timeout.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package kotlinx.coroutines.timeout

import kotlinx.coroutines.CopyableThrowable
import kotlinx.coroutines.Job

public actual class TimeoutException actual internal constructor(
message: String, internal val coroutine: Job?
) : Exception(message) {

actual internal constructor(message: String) : this(message, null)
}
17 changes: 17 additions & 0 deletions kotlinx-coroutines-core/jvm/src/timeout/Timeout.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package kotlinx.coroutines.timeout

import kotlinx.coroutines.CopyableThrowable
import kotlinx.coroutines.Job
import java.util.concurrent.TimeoutException as JavaTimeoutException


public actual class TimeoutException actual internal constructor(
message: String,
@JvmField @Transient internal val coroutine: Job?
) : JavaTimeoutException(message), CopyableThrowable<TimeoutException> {

actual internal constructor(message: String) : this(message, null)

override fun createCopy(): TimeoutException =
TimeoutException(message ?: "", coroutine).also { it.initCause(this) }
}
2 changes: 1 addition & 1 deletion kotlinx-coroutines-core/jvm/test/MemoryFootprintTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.junit.Test
import org.openjdk.jol.info.*
import kotlin.test.*


@Ignore
class MemoryFootprintTest : TestBase(true) {

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import kotlinx.coroutines.*
import org.junit.*
import org.junit.rules.*

@Ignore
class StackTraceRecoveryWithTimeoutTest : TestBase() {

@get:Rule
Expand Down
11 changes: 11 additions & 0 deletions kotlinx-coroutines-core/native/src/Timeout.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package kotlinx.coroutines.timeout

import kotlinx.coroutines.CopyableThrowable
import kotlinx.coroutines.Job

public actual class TimeoutException actual internal constructor(
message: String, internal val coroutine: Job?
) : Exception(message) {

actual internal constructor(message: String) : this(message, null)
}