-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Confusing behavior when using retry() and wrapping an exception thrown by emit() #2860
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
Comments
Thanks for the detailed write-up and self-contained snippets, it significantly simplifies the reasoning and investigation!
The very first observation is about the fact that this is exactly the exception transparency violation. The transparency is important for the library -- e.g. the downstream should not worry about how the upstream flow is implemented -- if it throws an exception X, then it will be properly rethrown. If the collect is invoked in the context of In your example, you are basically trying to hijack the downstream exception and replace it with your own. It violates the exception transparency and drives the Even though your proposal is quite simple, implementing it will make other things much worse -- now the upstream can hijack downstream exceptions, So before jumping to the potential solutions, it would be nice to know what exactly you are trying to achieve.
Does it solve your issue? |
Thank you for the detailed explanation @qwwdfsad. However my example simplifies the problem here too much. In my actual code I have a much longer Flow builder. What I'd basically have to do is to "catch everything except if thrown by an All of that can be worked around somehow. It's just not too simple if the code is more involved. And it's quite easy to miss those downstream/upstream exception details if you're used to using flow {
try {
// various logic
// database-dependent code
// maybe some networking
// load & parse data
val someLoadedData = …
// even a websocket connection
clientWebsocket {
// do stuff
}
} // + rethrow CancellationException
catch (e: Throwable) {
throw SomeOtherException("operation for $someLoadedData failed", e)
}
} |
Thanks for the explanation!
Just to clarify, you want to enhance only upstream exceptions, right?
I got the problem with the context, but could you please elaborate on what's wrong with "everything" part? If I got your use-case right, |
Exactly. That would help. However it doesn't solve the other problem that it's too easy to miss the accidental catching of Would it also be a solution to wrap downstream exceptions in a special OTOH someone might expect such an exception to be an actual cancellation rather than a downstream failure.
That's totally fine if I want to handle exceptions for all code in the Flow builder the same way. |
…tream (JVM-only yet) with explicit guarantees on context/exception transparency enforcement Pre-design for #2860
…tream (JVM-only yet) with explicit guarantees on context/exception transparency enforcement Pre-design for #2860
Thanks! We'll discuss your use case and potential solutions and the final resolution will be here around Kotlin 1.6.0 |
Small update: we'll try to prototype a slightly different solution. The problem with An alternative approach is to change how exception transparency works -- when upstream fails, do not overwrite an exception from the downstream, but suppress it and add it to the original exception |
…from the upstream when the downstream has been failed, but also suppress such exceptions by the downstream one It solves the problem of graceful shutdown: when the upstream fails unwillingly (e.g. file.close() has thrown in 'finally') block, we cannot treat is as an exception transparency violation (hint: 'finally' is a shortcut for 'catch'+body), but we also cannot leave things as is, otherwise it leads to unforeseen consequences such as successful 'retry' and 'catch' operators that may, or may not, then fail with exception on attempt to emit. Downstream exception supersedes the upstream exception only if it is not an instance of CancellationException, semantically emulating cancellation-friendly 'use' block. Fixes #2860
Kotlin#3017) * Improve exception transparency: explicitly allow throwing exceptions from the upstream when the downstream has been failed, suppress the downstream exception by the new one, but still ignore it in the exception handling operators, that still consider the flow failed. It solves the problem of graceful shutdown: when the upstream fails unwillingly (e.g. `file.close()` has thrown in `finally` block), we cannot treat it as an exception transparency violation (hint: `finally` is a shortcut for `catch` + body that rethrows in the end), but we also cannot leave things as is, otherwise, it leads to unforeseen consequences such as successful `retry` and `catch` operators that may, or may not, then fail with an exception on an attempt to emit. Upstream exception supersedes the downstream exception only if it is not an instance of `CancellationException`, semantically emulating cancellation-friendly 'use' block. Fixes Kotlin#2860
Kotlin#3017) * Improve exception transparency: explicitly allow throwing exceptions from the upstream when the downstream has been failed, suppress the downstream exception by the new one, but still ignore it in the exception handling operators, that still consider the flow failed. It solves the problem of graceful shutdown: when the upstream fails unwillingly (e.g. `file.close()` has thrown in `finally` block), we cannot treat it as an exception transparency violation (hint: `finally` is a shortcut for `catch` + body that rethrows in the end), but we also cannot leave things as is, otherwise, it leads to unforeseen consequences such as successful `retry` and `catch` operators that may, or may not, then fail with an exception on an attempt to emit. Upstream exception supersedes the downstream exception only if it is not an instance of `CancellationException`, semantically emulating cancellation-friendly 'use' block. Fixes Kotlin#2860
The following code is supposed to retry upstream if it throws an exception.
If the exception comes from downstream through the upstream's
emit()
then there it shouldn't retry as the downstream has failed already. This works well if I rethrow the exception raised byemit()
unchanged.However if I add some context to that exception by wrapping it in another exception then retry treats it as an exception that doesn't come from downstream but upstream and actually restarts upstream. That leads to a transparency violation.
This is unintuitive because wrapping exceptions is fairly common to add context.
Independent of whether it's legit to do so or not the behavior is quiet confusing. I expect both Flows to stop on downstream exception.
Also, wrapping exceptions works just fine if no
retry
operator is involved.Output
It gets even more confusing when I extend the
try
block to include the firstemit()
.The downstream keeps collecting but doesn't receive any more elements. The upstream repeatedly emits the first element indefinitely.
Output:
kotlinx.coroutines/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt
Lines 207 to 211 in 5eca49c
The text was updated successfully, but these errors were encountered: