-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Provide an API to race coroutines ("losers" are cancelled) #2867
Comments
Hi, Could you please show few examples of |
In the main project on my day job, I have close to 100 usages of that function FYI. I'm going to bring a curated summary of the most interesting use cases a bit later. |
A race similar API has already been discussed in #424 @qwwdfsad are you looking for this? #424 (comment) |
First simple and compelling use case is for timeouts: suspend fun whatever(
timeout: Duration = Duration.seconds(6)
): SomeResult = raceOf({
SomeResult.Yay(value = doStuffAndGetSomethingQuiteQuickly())
}, {
delay(timeout)
SomeResult.TimedOut(after = timeout)
}) Then it works well to surface cancellability in a way that keeps everything sequential: suspend fun tryDoingStuff(awaitCancelRequest: suspend () -> Unit) = raceOf({
doStuff() // When complete, awaitCancelRequest() will be cancelled.
}, {
awaitCancelRequest() // doStuff() would be cancelled automatically.
}) It also works well when you have multiple ways to provide some input data: suspend fun getToken(ui: RequiresLoginUi) = raceOf({
getTokenFromStorage()?.takeUnless { it.isStale() } ?: awaitCancellation()
}, login@{
repeatWhileActive {
val credentials = raceOf({
getSavedCredentialsFromAutoFill()
}, {
ui.awaitCredentialsSubmitting()
})
when (val loginResult = attemptLogin(credentials)) {
is Attempt.Success -> return@login loginResult.token
is Attempt.Failure -> ui.awaitLoginFailureAcknowledgement(loginResult)
}
}
})
suspend inline fun repeatWhileActive(block: () -> Unit): Nothing {
while (true) {
coroutineContext.ensureActive()
block()
}
} It works well when you want to enforce a certain UX process where there is ambiguity, regardless of how the UI is implemented: suspend awaitTrigger() = raceOf({
awaitAutomaticTrigger()
}, {
awaitManualTrigger()
}) suspend awaitDoorLockRequest(
voiceTrigger: VoiceTrigger,
awaitLockButtonClick: suspend () -> Unit
) = raceOf({
voiceTrigger.awaitHotPhrase()
}, {
awaitLockButtonClick()
}) suspend fun runSomeOnboardingStep(ui: SomeUi) = raceOf({
ui.awaitExistingUserConfirmation()
}, {
ui.awaitRequestToWatchQuickIntro()
raceOf({
ui.showQuickIntroVideoUntilComplete()
}, {
ui.awaitSkipRequest()
})
}) |
The following are just some thoughts on the matter. In general, this can be thought of as non-atomic So,
|
The post-processing is only cancelling "losers" here, unless we want to consider an API that allows a given set of winners, which would be interesting, albeit beyond the scope of this specific issue that already has clear use cases IMHO. Or would the risk you're mentioning be able to cause cross-cancellation, where no one wins because there's an ex-aequo? I think such a behavior would not work well for application use cases, best to pick first, or be random for this rare case. |
By post-processing I mean the part after the asynchronous part of the operation is complete. In select<Int> {
channel1.onReceive { v ->
Thread.sleep(1000) // post-processing
v
}
channel2.onReceive { v ->
Thread.sleep(1000) // post-processing
v
}
} In A middle ground between |
Note, that you can already use
I'd suggest starting directly from here and splitting it into two features. The first one is the combination of The other is a combination of
This construct is more composable than a plain P.S. Naming needs a careful design here. |
This looks like #1065 |
It does not have to involve #1065 at all. In order for the race DSL to be clear, it is critical not to expose the internal It is quite important here to clearly name all |
@elizarov I think this use case is one more specific than #1065, a recurring problem like #424. Using the #1065 idea, it is possible to implement your idea: val result = coroutineScope {
select<String> {
async (onScopeTermination = ScopeTermination.CANCEL) {
delay(300)
println("slow")
"Slow Result"
}.onAwait { it }
async (onScopeTermination = ScopeTermination.CANCEL) {
delay(200)
println("fast")
"Fast Result"
}.onAwait { it }
}
}
println("Result=$result") I understand that this version is more verbose because the syntax is not specific to this issue. |
The wrapping coroutineScope looks like boilerplate and unwanted indent in this snippet posted just above. Is it required? |
I reused the #1065 idea, similar to the elizarov's first example, so an extra I am not convinced that the suspend fun main() {
val result = select<String> {
__onAwait__ {
delay(300)
println("slow")
"Slow Result"
}
__onAwait__ {
delay(200)
println("fast")
"Fast Result"
}
}
println("Result=$result")
} |
Usually, I find the most convenient idiom for this
...instead of bothering with any |
I'm wondering whether that interesting solution leveraging |
Hello,
I keep seeing this suspend functions racing use case being asked on Kotlin's Slack, and I personally need it quite often as it suits many needs in "coroutines-heavy" code.
I've been using my own solution for a long time now, which I also published as a library in Splitties for that (available on MavenCentral), and I didn't find any better API than
raceOf({…}, { … }, …)
andrace { launchRacer { … } }
.Here is an example snippet of
raceOf
in action taken straight from the README of Splitties Coroutines:and here's a permalink to the code of these 2 APIs.
Can this API or a better alternative be considered for addition inside kotlinx.coroutines?
The text was updated successfully, but these errors were encountered: