I find myself with this piece of K/JS (well, for t...
# coroutines
e
I find myself with this piece of K/JS (well, for the purpose of the example, in reality it's multiplatform) code, from which I have no idea how to get out.
Copy code
override fun disposeAsync(): Promise<Unit> =
  coroutineScope.promise {
    delegate.dispose() // This is the original suspending method I'm wrapping

    // TODO: are we sure this will work?
    //   It doesn't feel right because we are cancelling
    //   children inside one of those children
    coroutineScope.coroutineContext.cancelChildren()
  }
Note the TODO I had left in there. This obviously will throw an exception when run. Any suggestion? Maybe I should switch to
GlobalScope
?
s
I’d flip the approach and set up the coroutine scope so that it calls
dispose
on cancellation. As soon as you create the scope, launch a job containing a
finally
block that will run after the scope is cancelled.
Copy code
coroutineScope.launch(start = ATOMIC) {
    try {
        awaitCancellation()
    } finally {
        withContext(NonCancellable) { 
            delegate.dispose() 
        }
    }
}
Then
disposeAsync()
can simply call
coroutineScope.cancel()
and it will automatically run the
dispose
during the cancellation.
🔝 1
e
That's interesting! So basically I'll have a dormant job awaiting scope cancellation. Can I ask why the
ATOMIC
, and why the
NonCancellable
?
Ahhhh, got it. Basically it's immune to cancellation. Had a look at the docs
s
Yeah 👍 the atomic start is to just avoid the possibility of the scope being cancelled before the job is launched at all
And
NonCancellable
is so that dispose will still be able to run even though the scope was already cancelled
e
There is one difference in behavior tho. If I return a
Promise
, I can await it, and that means processing will go on only after the
delegate
has been disposed. Not sure how that will work with this approach.
s
You could use
cancelAndJoin
to wait for the dispose call to complete. Join will wait for all jobs in the scope to finish, even after cancellation.
Though I guess that still leaves you needing to launch another coroutine to create the promise 😄🤦
e
Yeah I was trying to code what you suggested and I was blocked thinking about how to do that hahaha
s
Okay, how about this. Make the dormant background job and the promise be the same job.
e
What about
Copy code
coroutineScope.promise {
  try {
    coroutineScope.coroutineContext.cancelChildren()
    awaitCancellation()
  } finally {
    withContext(NonCancellable) {
      delegate.dispose()
    }
  }
}
Ok maybe we had the same idea more or less
The code looks strange, gotta be honest, but maybe it works
Just tested, and this works fine:
Copy code
override fun disposeAsync(): Promise<Unit> =
  coroutineScope.promise {
    try {
      coroutineContext.cancelChildren()
    } finally {
      withContext(NonCancellable) {
        delegate.dispose()
      }
    }
  }
I'm also trying in another way, by storing the dormant dispose job and returning it from
disposeAsync
.
So I've tried in the other way, but not good. The only way it works correctly is the one above. And also, if instead of
Copy code
coroutineContext.cancelChildren()
I use
Copy code
cancel()
to cancel the entire scope, the awaited
Promise
will throw an error. --- Another approach that seem to work.
Copy code
@OptIn(ExperimentalCoroutinesApi::class)
private val disposeJob = coroutineScope.launch(start = CoroutineStart.ATOMIC) {
  try {
    awaitCancellation()
  } finally {
    withContext(NonCancellable) {
      delegate.dispose()
    }
  }
}

...

override fun disposeAsync(): Promise<Unit> {
  coroutineScope.cancel()

  @OptIn(DelicateCoroutinesApi::class)
  return GlobalScope.promise {
    disposeJob.cancelAndJoin()
  }
}
s
Yeah, the second approach you showed there is very close to what I was trying to come up with. I wanted to find a way to make it work without the GlobalScope, though I don't think using GlobalScope is too bad in that scenario 👍
✔️ 1
e
I think my question would be: what does it mean not cancelling the scope (but only its children) in terms of allocated resources?
Because if I can leave the scope there, then the first approach is fine
s
It depends a little on how you created the scope. Assuming it's a scope you created with your own custom
Job()
or
SupervisorJob()
, that job itself won't have a coroutine or resources, it will just be used as the parent for all the other jobs. So the only difference is that cancelling the scope prevents any new coroutines from being started in that scope, whereas just cancelling the children means you would be able to continue using the scope to launch new coroutines later.
e
Yup I creare it with a
Job
or
SupervisorJob
. My main concern was about the allocated Dispatcher tho. I use an
IO.limitedParallelism
dispatcher.
s
There's no need to close any resources for that dispatcher 👍. Even for dispatchers that do need explicit shutdown, like a custom executor service, that's not something a coroutine scope would do automatically on cancellation.
gratitude thank you 1