Skip to content

Introduce deprecated coroutine builder overloads accepting a Job #4435

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

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from

Conversation

dkhalanskyjb
Copy link
Collaborator

This is a small step towards deprecating the practice of breaking
structured concurrency by passing a Job to a coroutine builder.

Thanks to @LouisCAD for the idea!

See #3670

This is a small step towards deprecating the practice of breaking
structured concurrency by passing a `Job` to a coroutine builder.

See #3670
@dkhalanskyjb dkhalanskyjb requested a review from qwwdfsad May 13, 2025 13:28
@LouisCAD
Copy link
Contributor

Awesome stuff!

* throw IllegalStateException("$it is tired of all this")
* }
* }
* coroutines.joinAll()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do an explicit join here? supervisorScope should wait for its children, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it will. The idea was to highlight how joinAll will not fail in this scenario, but this piece of documentation may not be a good place for that.

Comment on lines +223 to +229
* scope.async(start = CoroutineStart.ATOMIC) {
* withContext(NonCancellable) {
* // this line will be reached even if the parent is cancelled
* }
* // note: the cancellation exception *can* be thrown here,
* // losing the computed value!
* }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that mean you can loose the value, even if there is no code after the withContext block?

scope.async(start = ATOMIC) {
    withContext(NonCancellable) {
        dontWantToLooseThisResource()
    }
    // no code here, or only code without suspension points
}

The documentation for withContext says there will be no cancellation on entry/exit with NonCancellable.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My comment is misleading, but yes, the value can be lost, just not because of withContext. The problem is that if the Deferred is cancelled before returning a value, it will no longer be able to finish with a value, even if the computation succeeds.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, that's a bit surprising to me. I guess it's because it could have children that haven't completed yet?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, even without any children: https://pl.kotl.in/-ZYf_PR9z
The reason can be seen in the diagram in https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/ : once a Job gets cancelled, there is no way back to being Active or Completing or returning a value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, good to know 👍

* } finally {
* // if `await` fails because the current job is cancelled,
* // also cancel the now-unnecessary computations
* deferred.cancel()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be pointed out that just calling cancel on deferred won't prevent the current coroutine from completing before the now cancelled coroutine in the other scope completes?

@LouisCAD
Copy link
Contributor

LouisCAD commented May 19, 2025

With these changes, someScope.launch(NonCancellable) { ... } will no longer compile. I've yet to see a tangible use case for that, where the someScope is useful (for anything but Job, that'd be), but wanted to point it out.

Another construct could be thought out, or an overload taking the NonCancellable type could be introduced, if that actually needs to be addressed.

@dkhalanskyjb
Copy link
Collaborator Author

@LouisCAD, what is the compilation error? I've just checked, and launch(NonCancellable) compiles for me (with a warning, as intended). I consider launch(NonCancellable) to be incorrect, with launch(start = CoroutineStart.ATOMIC) { withContext(NonCancellable) { /* computation */ } } being a strictly better version of the same.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants