-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
fix: handle cancelled scope and empty flow in Flow.stateIn
#4327
fix: handle cancelled scope and empty flow in Flow.stateIn
#4327
Conversation
@@ -340,6 +341,9 @@ private fun <T> CoroutineScope.launchSharingDeferred( | |||
} | |||
} | |||
} | |||
if (state == null) { | |||
result.completeExceptionally(NoSuchElementException("Flow is empty")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Notice: I think we shouldn't cancel the collecting scope here, as there is nothing wrong with the flow collection itself, the exception should be limited to stateIn
IMHO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good point, let's not cancel the collecting scope. This is tricky to do ensure cleanly, though! Here's a slightly convoluted solution:
/* If `scope`'s job is cancelled, both `deferredParentJob` and the deferred will be cancelled.
If the deferred is cancelled, this will not cancel `deferredParentJob` (as it's a `SupervisorJob`),
and `CoroutineExceptionHandler` present in `scope` (if any) won't get invoked,
as `Deferred` values are themselves responsible for propagating their exceptions. */
val deferredParentJob = SupervisorJob(scope.coroutineContext[Job])
val result = CompletableDeferred<StateFlow<T>>(deferredParentJob)
scope.launchSharingDeferred(config.context, config.upstream, result)
return try {
result.await()
} finally {
deferredParentJob.cancel()
}
then the modified test is:
@Test
fun testThrowsNoSuchElementExceptionOnEmptyFlow() = runTest {
val flow = emptyFlow<Any>()
assertFailsWith<NoSuchElementException> {
flow.stateIn(this)
}
assertEquals(true, coroutineContext[Job]?.isActive)
coroutineContext.cancelChildren()
}
(Note this runTest
is not kotlinx.coroutines.test.runTest
!) If the main coroutine of runTest
gets cancelled, the whole test fails; also, if there are any uncaught exceptions in child coroutines of runTest
, they do get reported at the end of the test. So, this test does check that NoSuchElementException
doesn't propagate upwards.
The only other option I see is to rely on our internal low-level job.invokeOnCompletion
function to avoid the parent-child relationship between scope
and result
altogether. After implementing that option, I don't like how it recreates parts of child cancellation logic. This looks like a weird micro-optimization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another simpler (not elegant) trick is to make CompletableDeferred
of type CompletableDeferred<Result<StateFlow<T>>>
and complete the deferred successfully but with Result.failure
and unpack with getOrThrow
it stateIn
side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@francescotescari, would you like to implement this, or should I do it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the delay, addressed it now.
LMK if it looks good to you 🙏
kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt
Outdated
Show resolved
Hide resolved
Thank you both for the bug report and a well-thought-out fix! |
Fix for #4322