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: abort transaction in all cases #881
Conversation
catching RuntimeException all other subclasses of Exception are not caught and don't abort the transaction
a bit of context: maybe this can happen only in Kotlin you can actually compile code that throws checked exceptions or calling java code that throws checked exceptions without declaring them with throws in the TransactionBody interface |
here is some code to reproduce the error https://github.com/emasab/mongo-client-fix-881 |
@@ -209,7 +209,7 @@ public <T> T withTransaction(final TransactionBody<T> transactionBody, final Tra | |||
try { | |||
startTransaction(options); | |||
retVal = transactionBody.execute(); | |||
} catch (RuntimeException e) { | |||
} catch (Throwable e) { |
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.
Such a tiny change, and so many things come to mind when thinking about it.
Error
s in Java the way they are, make no sense
My view on this is expressed here. A consequence of this view is that we should not catch Error
s, which means we should catch Exception
s instead of catching Throwable
s each time we want to catch "everything" that makes sense catching (surely, there are exceptions from this rule, like catching assertion errors in test frameworks, or catching specific Error
s that should have been Exception
s, but were made Error
s instead).
Unlike Java, Rust uses two drastically different mechanisms to represent recoverable and unrecoverable errors:
- Recoverable errors are represented via return values. Unlike in C, the compiler forces a caller to either handle them or to explicitly propagate as the caller's return value via the
?
operator. - Unrecoverable errors are called panics, which can be implemented via unwinding or aborting. Unwinding is similar to throwing an exception in Java, and an unwinding panic can even be caught.
It is worth pointing out that the ability to catch unwinding panics in Rust should not be interpreted as "see, even in Rust catching unrecoverable errors is possible, which suggests that trying to handle unrecoverable errors (whether in Java or in Rust) may make sense!". The reason Rust allows catching unwinding panics is that "Rust's unwinding strategy is not specified to be fundamentally compatible with any other language's unwinding. As such, unwinding into Rust from another language, or unwinding into another language from Rust is Undefined Behavior. You must absolutely catch any panics at the FFI boundary!" This point leads us directly to the issue that this PR is trying to address.
Kotlin's exception handling is not compatible with Java's exception handling
Unlike Java, Kotlin does not have the concept of checked exceptions (good for Kotlin). However, this means that Kotlin's exception mechanism is not compatible with Java's exception mechanism, and just like Rust code must catch any unwinding panics at the FFI boundary, Kotlin generally should catch all exceptions that are considered checked in Java (the check is simple: they are of the Exception
type and not of the RuntimeException
type) at the Kotlin-Java boundary. Unfortunately, the Kotlin's interop guide erroneously does not require anything like this. It partially addresses the incompatibility for a subset of cases, but that subset does not include the case addressed in this PR.
Conclusion
- Even though I consider catching
Throwable
s instead ofException
s an undesirable practice, the code of the driver contains many places where this is done, which is why introducing one more such catch changes nothing. - Even though having unchecked exception thrown from Kotlin code that implements a Java API method that does not declare checked exceptions is a problem in the Kotlin code, we better work around it as suggested in the PR. Also, from now on we should remember to consider any method as being able to throw checked exception even it is not declared1 (despite this not being true for all methods, for simplicity and robustness it is better to treat all of them this way).
1 One may argue that even without calling Kotlin code, sneaky throws are possible in Java due to the bug-feature it introduced. However, in this case it is easy to argue that whoever uses the technique is fully responsible for the consequences. The same is much harder to say about Kotlin code because it is more abundant and Kotlin's documentation fails to require the right thing to be done for interoperability.
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.
Nice reasoning. I add that if you decompile a Kotlin class file you see that a checked exception is wrapped inside a Throwable and then thrown, I've not tested it but I'd guess that it's done in Scala and Groovy too.
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.
Interesting, it is not obvious to me why Kotlin compiler wraps checked exceptions in Throwable
given that an exception of the Throwable
type is considered checked by Java compiler.
Thanks, @emasab, @esabellico-facephi, for informing the driver team about the problem. JAVA-4575 Driver code should catch Exception instead of RuntimeException even if no checked exception is declared will address the problem in other places in the code. |
Not at all @stIncMale. Correction about the Kotlin bytecode: it's not wrapped, it's casted to Throwable. And as it's checked in Java, it should be the Kotlin compiler that, not doing the checks, allows to compile to bytecode. If you decompile the Kotlin bytecode to Java it gives the error: you have to declare it with throws. |
@stIncMale may I ask another question, given that we're still experiencing a case where the same sessionId is used with two different transaction ids even if the session is started and closed with the transaction. In the current version: https://github.com/emasab/mongo-java-driver/blob/1294de8a3daf0bcc0403d127f8c932511a51ea18/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
Thanks! |
@emasab I have not been familiar with this code, but it appears that the code is written with the following idea behind it: a client may try to either commit or rollback a transaction, but cannot try both:
If you still think you are observing a bug, it may be worth reporting it via Jira. And if you have a reproducer, it would be very helpful. |
@stIncMale thanks for the explanation it's much clearer now, about the txnId it's giving this error |
See JAVA-4575 for more details.
catching RuntimeException all other subclasses of Exception are not caught and don't abort the transaction