You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Transactional method returning Future will sometimes be rollback, sometimes not, depending on time that Future is exceptionally completed. Here is example:
@TransactionalpublicCompletableFuture<Void> asyncMethod() {
doSomeDatabaseModification();
if (!isHealth()) {
returnCompletableFuture.failedFuture(newRuntimeException()); // case 1
}
finalCompletableFuture<Void> result = newCompletableFuture<>();
newThread(() -> {
try {
Thread.sleep(10_000);
} catch (InterruptedExceptione) {
result.completeExceptionally(e); // case 2return;
}
result.complete(null);
}).start();
returnresult;
}
In case 1 transaction will be rolled back, in case 2 transaction will be committed, despite in both cases CompletableFuture is completed with error. From that reason method user cannot make any assumptions about transaction state after failure.
For example it could lead to unintended behaviour in method methodDependingOnRollback():
@TransactionalpublicvoidmethodDependingOnRollback() {
finalCompletableFuture<Void> result = asyncMethod();
while (!result.isDone()); // ugly but only on purpose of example// assuming that in case of asyncMethod() failure, database will not be modifiedsaveSuccessInDatabase();
}
It also breaks code that was working in Spring 5, like methodWorkingInSpring5():
@TransactionalpublicvoidmethodWorkingInSpring5() {
asyncMethod().whenComplete((v, e) -> {
if (e != null) {
saveErrorReasonInDatabase(e);
}
});
}
Breaking change was introduced by issue #30018. Probably the same problem is with Vavr, but I'm not familiar with this library.
The text was updated successfully, but these errors were encountered:
This is a semantically complicated matter and admittedly not obvious.
@Transactional on CompletableFuture-returning methods is only meant to apply to the immediately executing code within the original thread, not to any asynchronous operations triggered by it. Such scenarios that seem to have worked before did not have well-defined semantics - and if they worked with any transactional impact at all, they arguably only worked by accident. The asynchronous thread would not see the transaction-managed state, and the completion of the transaction happens at the end of the original method invocation, not at the end of the asynchronous operation.
The change in #30018 aims for a differerent scenario: The use of Future types as a return value for completed operations. There may have been asynchronous steps involved but further steps have followed in the original method before returning the completed Future handle. Such a method could also be designed to entirely run synchronously and to return a Future only for compliance with a given method signature (like in the case of @Async methods), indicating a rollback not through throwing an exception but rather through returning a Future handle that just wraps a result object or an exception.
As a consequence, this behaves as designed. I'm turning this into a documentation issue for adding that clarification to the reference docs.
P.S.: I realize that such CompletableFuture.whenComplete scenarios within a wider transaction would have worked for you before. However, this was never intended on our side, in particular not with the ambiguous meaning of the nested @Transactional there.
In order to make your scenario work with 6.1, you could remove the @Transactional declaration from asyncMethod and exclusively rely on the wider transaction demarcation in methodDependingOnRollback/methodWorkingInSpring5. The nested method call will still implicitly participate in your outer transaction - but without a @Transactional declaration of its own, it is not going to make rollback decisions of its own (neither for exceptions thrown nor for exceptions returned in a CompletableFuture).
Alternatively, you could change the nested declaration to @Transactional(noRollbackFor=Exception.class) and suppress a local rollback decision that way.
Affects: 6.1.0+
Transactional method returning Future will sometimes be rollback, sometimes not, depending on time that Future is exceptionally completed. Here is example:
In case 1 transaction will be rolled back, in case 2 transaction will be committed, despite in both cases CompletableFuture is completed with error. From that reason method user cannot make any assumptions about transaction state after failure.
For example it could lead to unintended behaviour in method
methodDependingOnRollback()
:It also breaks code that was working in Spring 5, like
methodWorkingInSpring5()
:Breaking change was introduced by issue #30018. Probably the same problem is with Vavr, but I'm not familiar with this library.
The text was updated successfully, but these errors were encountered: