-
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
Structured concurrency with two parent jobs #1001
Comments
This behavior is by design. |
Thanks for explanation. It was not clear from coroutine documentation how exactly parent-child tracking works. So to make a "job" behave as an extra trigger and maintain structured concurrency the code should be written as: val job = Job(coroutineContext[Job]!!)
launch(context = coroutineContext + job) {
} Is it correct? Another similar example which is currently not clear (example from android Activity, but could be e.g. some backend BackgroundService): class A : CoroutineScope {
val job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
suspend fun run() = withContext(coroutineContext) {
while (isActive) {
println("RUNNING")
delay(400)
}
}
fun onDestroy() { job.cancel() }
}
runBlocking {
val a = A()
val job = launch { a.run() }
delay(100)
job.cancel()
delay(4000)
}
} I pretend when cancelling 'job' it will cancel computations, but it is running indefinitely (actually this is currently in our production system :-) |
The idiomatic way to fix your code is to either move
or remove Anyway, writing less code makes it work correctly and it is so by design. The general idea that the shortest possible code shall be a correct one. The docs on mixing |
I think this is dangerous. Now we have situation when due to a little code changes, the semantic changes dramatically. Moreover, in some cases the shortest code will be incorrect. For example, if I want to start child but non cancellable async coroutine, I will write the simplest/shortest code like this:
but it is not correct. Now I can do this only like
I suppose, even with updated documentation current behaviour will be difficult and dangerous because it differs from "default expected behaviour". |
@Dougrinch can you, please, clarify what is the use-case for "child but non cancellable async coroutine". What you are trying to achieve by writing this code? (I'm asking because I'd like to see if there any existing or proposed solution to your use-cases that is actually simpler to code) |
To be honest, there is not real use case. I found it while reading the coroutine docs and "playing with code to understand". I just was confused an implicitness of this. I agree with @antanas-arvasevicius that |
Hi,
(As of writing test example, I've found more strange things, seems internally there are parent-child relationships independent of Job objects, see below) It leads to confusion in this examples: val workers = Dispatchers.Default + Job()
suspend fun test() = withContext(workers) {
while (isActive) {
println("doing work")
delay(1000)
}
println("work completed")
}
runBlocking {
val job = launch {
test()
}.apply {
invokeOnCompletion {
println("completed") // <- never prints "completed"
}
}
delay(200)
job.cancel()
job.join() // <- join is waiting also for test() to complete? But how? it's on different Job()
println("done") // <- never reaching here
} Could you explain how it works? Looks like Other: val workers = Dispatchers.Default + Job()
suspend fun test() = coroutineScope {
launch(workers) {
while (isActive) {
println("doing work")
delay(1000)
}
println("work completed")
}
}
runBlocking {
val job = launch {
test()
}.apply {
invokeOnCompletion {
println("completed")
}
}
delay(200)
job.cancel()
job.join()
println("done")
delay(5000)
//..
//still producing "doing work"
} Now it joins, but never cancels. And all works here: suspend fun test() = coroutineScope {
launch { //<- not using context with "Job"
while (isActive) {
println("doing work")
delay(1000)
}
println("work completed")
}
}
runBlocking {
val job = launch {
test()
}.apply {
invokeOnCompletion {
println("completed")
}
}
delay(200)
launch { delay(4000); job.cancel() }
job.join()
println("done")
// completes after 4 seconds. all ok.
}
} Shouldn't |
In you first example you cancel the outher On the other hand note, that if you do |
Hi, suspend fun test() = coroutineScope { launch(workers) { while (isActive) { } } and code suspend fun test() = withContext(workers) { while (isActive) { } } works differently when calling suspend fun test() = coroutineScope { launch/*(workers)*/ { while (isActive) { } } Some conslusions:
|
There is nothing hidden.
Does it explain the results? |
This part is clear, what is not clear is how then val workers = Dispatchers.Default + Job()
suspend fun test() = withContext(workers) { while (isActive) { } }
launch { test() }.invokeOnCompletion { println("completed") } so difference from But what concerns me for this that using withContext with Job attribute made calls to "test()" not cancellable by a context on which it was called, but it blocks and waits for completion of "test()". It may be useful in some scenarios but not a daily default. And how to implement cancellation? Because withContext do not provide result's Job so we cannot cancel it explicitly: pseudo code would be: val workers = Dispatchers.Default + Job()
suspend fun test() {
coroutineContext[Job]!!.invokeOnCompletion(true) {
j.children.find { /* how to find children ? */ }?.cancel()
}
withContext(workers) { while (isActive) { } }
}
// or with launch
val workers = Dispatchers.Default + Job()
suspend fun test() {
val childJob = launch(workers) { while (isActive) { } }
coroutineContext[Job]!!.invokeOnCompletion(true) { childJob.cancel() }
childJob.join()
} By this example it possible to explicitly cancel actions on call sites of test() and also cancel globally with Question is why I see problem in official examples of CoroutineScope android activity implementation, because it's maybe ok for android, but applying the same for services is not OK because all calls is not cancellable by default, e.g. we need to write services as this to be able callers to cancel it's actions. class FileUploaderService : CoroutineScope {
val job = Job()
override val coroutineContext get() = Dispatchers.Default + job
suspend fun uploadFile() {
val childJob = launch {
// do uploading...
}
coroutineContext[Job]!!.invokeOnCompletion(true) { childJob.cancel() }
childJob.join()
}
fun destroy() {
coroutineContext.cancelChildren()
}
} And it could be nice that this code could be replaced with just one liner More I'm elaborating on this more strangely it looks and yes it could be documented much, but it will be hard to avoid WTF moments and what I'm afraid that many developers will be hit by this and then search for manuals later to find out this behaviour. And sorry for your taken time. |
there are two use-cases:
The same use-case apply to
So, sometimes injecting an explicit Concerning this example:
What is the purpose of writing this complicated code? You can just write:
|
No, I cannot write that way because it won't run on a Example: class A : CoroutineScope {
override val coroutineContext: CoroutineContext get() = Dispatchers.Default
suspend fun test() {
launch { while (isActive) { delay(1000) } }
launch { while (isActive) { delay(2000) } }
}
}
runBlocking {
val a = A()
val job = launch { a.test() }
delay(1000)
println(a.coroutineContext[Job]!!.children.toList()) //<- KotlinNullPointerException
job.join()
} So, actually, using CoroutineScope interface without Job attribute is barely useful. |
Hi, suspend fun <T> withCancellableContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T {
val job = coroutineContext[Job]
return withContext(context) {
if (context[Job] != NonCancellable) {
job?.invokeOnCompletion(onCancelling = true) { coroutineContext[Job]?.cancel() }
}
block()
}
} fun CoroutineScope.cancellableLaunch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job = launch(context, start, block).also { job ->
if (context[Job] != NonCancellable) {
coroutineContext[Job]?.invokeOnCompletion(onCancelling = true) { job.cancel() }
}
} |
Not sure if related to this thread, i have custom CoroutineScope:
And i use:
Even i cancel the Edit: is this related to this section in documentation (making computation code cancellable)? |
@antanas-arvasevicius You don't need to pass explicit job. There is no need to write all this code and hence no need to have in the library. You can just use the job you already have in the coroutine context. If you need to cancel some job, like a file upload, just write:
|
@elizarov Are you say that if I want to write |
I see. This
The proposed How can we add it to
What realistic options to we have? I see one way at the moment (but I don't like it very much): We can have some new variant of |
Hi,
How about not going in two parents route, but just create new job object
which will be added as a child in caller's scope if scopes will differ.
XxxJob's responsibility would be to delegate cancellation only. Could be
named EntangledJob :))
How about this?
There is seems missing concept - creation graph of coroutines separate from
execution graph of coroutines (as currently job children-parent does).
Don't know if it is good way to decouple that and made explicit concept.
Maybe debug stack tracking will work better with creation graph instead of
current implementation.
…On Sun, 31 Mar 2019 at 14:32, Roman Elizarov ***@***.***> wrote:
I see. This FileUploaderService is an interesting use-case, indeed. We
have a similar problem in Ktor. Basically, what is wanted here is a
(missing now) ability to have a Job with two parents:
1. There is a Job of FileUploaderService itself. When it is cancelled,
we want all uploads to be cancelled.
2. There is a Job in which uploadFile is called. When it is cancelled,
we want *this* upload to be cancelled.
The proposed withCancellableContext by @antanas-arvasevicius
<https://github.com/antanas-arvasevicius> emulates this functionality.
How can we add it to kotlinx-coroutines library? I don't have a good
answer:
1. We cannot just change the way withContext works (it is going to be
a breaking change)/
2. We cannot just add xxxCancellable variant for each builder xxx --
there is going to be too much duplication and it is not composable.
What realistic options to we have? I see one way at the moment (but I
don't like it very much):
We can have some new variant of Job/SupervisorJob constructor function
that works in a such a way, that going withContext(new_job_type) works
like withCancellableContext. That is going to look quite error-prone in
the long-term, so we could augment it with a long-term plan to deprecate
usages of withContext(old_job_type) and maybe, in the future, to
deprecate old Job() and SupervisorJob() constructors altogether, by
replacing them with some other fresh names (this also gives us a chance to
fix asymetry in Job/SupervisorJob naming).
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1001 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AEAcnCwV9xr3_EJA2azX2bhVqtsrUcHPks5vcKrcgaJpZM4bFeeP>
.
|
I don't know if that is what you were thinking, but another solution is going to be to define a completely new kind of entity. Not a |
No, I think it's almost ok as is because now to create an object with its
own scope we just need to define coroutine context with Job() to control
lifecycle, if we'll add new concept apart job it will definitely add more
confusion than there is now.
I think current implementation defines coroutine relationships between
runtime execution (answers on where coroutine is executed on), but not how
coroutines was created (we do not have relationships). Yes in any case
implementation woud be the same as current Job (children-parent). But job
tracks execution relationship and new would track creation relationship.
Yes it seems the same as Job does now, I am also not clear if it is should
be different concepts (creation & execution) or not.
So there are two options to add sentinel job EntagledJob and inject that
job in coroutineContext of caller just before merging of passed context.
Or implement ChildrenGroup and add it to Job for current implementation
and add for creation parent child tracking.
Personally I feel that explicitly separating execution and creation
relationships would help for understanding and for implementation, as now
it looks like they are implicitly interleaved together (but still not
clear now is current Job not covers everything and that case is exception).
I think this needs some prototyping and investigate if it reveals something
or not.
…On Sun, 31 Mar 2019 at 15:15, Roman Elizarov ***@***.***> wrote:
How about not going in two parents route, but just create new job object
which will be added as a child in caller's scope if scopes will differ.
I don't know if that is what you were thinking, but another solution is
going to be to define a completely new kind of entity. Not a Job at all.
Let's call it Group (not a real name, just to simplify discussion). So,
instead of creating a Job inside FileUploaderService it could create Group
and the use withContext(group) { ... }. That would completely abolish the
need to have "two parent Jobs". However, the problem is that Group's API
and implementation is going to very much like the Job.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1001 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AEAcnKCGtZwmH2G1UrfzPLGdJ87LBcZzks5vcLUOgaJpZM4bFeeP>
.
|
I've encountered this recently too. Could you please advise what is the best way to currently achieve the same behaviour without using The way that I currently found is:
This small trick with async {}.await() allows to caller's Job to be completed while async block is running, once caller's job is done completion signal is received and async block can be cancelled. (note the same thing won't work with Is there a nicer / simpler way to express it? |
What about this?
|
I've always assumed that this already works out of the box because it's such a common use case. I'm surprised that it doesn't. I have the use case described by @antanas-arvasevicius and @elizarov (#1001 (comment)) quite often. There is a wider scope (a service, a worker, an API/HTTP client, etc.) and a narrower scope (a unit of work, an HTTP request, etc.). For some reason I've assumed the following to work out of the box: class Worker(coroutineContext: CoroutineContext) {
private val coroutineContext = coroutineContext + SupervisorJob() + CoroutineName("Worker") // "wide scope"
suspend fun process(work: Work) { // "narrow scope"
withContext(coroutineContext) {
// process work
// canceled if either the caller's Job is canceled or the Worker's Job is canceled
}
}
fun stop() {
coroutineContext.cancel()
}
} What's the best approximate to do that with what we already have? |
I can't believe I ended up in this thread again with another unexpected behavior 😅 The following code does… absolutely nothing. suspend fun main() {
val flow = flowOf(1)
coroutineScope {
launch(NonCancellable) {
flow.collect { number ->
println("Processing number $number…")
delay(5_000)
println("Done processing number $number.")
}
}
}
} Description of
The side-effect that the mentioned "non-cancelable job" breaks the parent-child relationships comes quite surprising, at least to me. So the main function returns and the program terminates even before the new coroutine was launched. What I would have expected, from both "reading the code" and from the documentation:
Unfortunately that's not the case. Background
With the current behavior pending work would continue due to Making the entire back-end cooperatively wait for downstream work would likely add a lot of unnecessary complexity for something that could work out-of-the-box with coroutines. Regarding multiple parent jobs: |
The best explanation is in this blog post of mine: https://elizarov.medium.com/coroutine-context-and-scope-c8b255d59055 We still have no "easy to use" solution to this particular problem for this particular use-case of two parent jobs. |
Yeah, @fluidsonic I've been in your situation and I know what it feels. All theoretically nice "structured concurrency" just breaks apart if you pass new Job instance (and NonCancellable) in coroutine context. I'm proposing changes is: Diagram for suggestion:
|
@elizarov do you mean the explanation regarding The only info that's not in the documentation is this:
The documentation doesn't make that clear at all. Only if you are highly familiar with how It should renamed to |
My suggestion is that API must prohibit any possibility of breaking the chain of parent-child relationships and those relationships can be created only by calling CoroutineScope.launch or CoroutineScope.withContext or if you need "new" chain you'll call GlobalScope.launch () { } Btw. with android examples where were a classes with internal customScope Also will break structured concurrency if you'll do something like:
|
I agree. It's super easy to mess up that relationship without noticing, esp. when you're new to coroutines and even if you're intermediate. We always have to wonder what context a job comes from - if it has any, when we (accidentally) switch to another job tree, and what happens if multiple jobs collide.
And if something needs a I've read all info on coroutines that I could find, sometimes multiple times. I've used them a lot. And I still keep getting it horribly wrong. It's quite a steep learning curve. |
The rule of thumb that is we use in API:
What we are seeing here, is that violations of the 1st rule break the structured concurrency without being apparent. I'd love to enforce it stronger and require an explicit opt-in for breaking structured concurrency, but I don't have any nice ideas on how we might implement this kind of enforcement in a backward-compatible way. The only easy fix is more docs: #2633 For more substantive fixes, I see the following potential directions:
However, I don't see any easy way to detect and forbid code like this:
|
The code of coroutine builders like
The default value could then move forward across major kotlinx.coroutines, to have developers take notice and fix possible violations. |
It is not a very good idea to emit runtime warnings from the library. We could do it, but that is a last-resort measure for problems of a grandiose scale. |
Why is it not a very good idea? While I agree that runtime warnings are likely to never be seen, there's solutions to this problem. Here's one from the top of my head: I'd love such a thing landing in the Kotlin ecosystem, even if it starts with a barebones approach. |
Hello,
Today we've found out that this code do not conform to official structured concurrency concept:
When canceling "mainJob" it won't cancel all children jobs inside.
Seems like calling launch with currentContext + job breaks parent-child relationships.
Logically we thought that custom job property will be used only as extra control for coroutine cancellation and not completely break parent-child relationships.
Is this behaviour by design or it's a bug?
Solutions I think could be one of:
The text was updated successfully, but these errors were encountered: