-
Notifications
You must be signed in to change notification settings - Fork 38.3k
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
Avoid rollback after a commit failure in TransactionalOperator
#27572
Changes from all commits
0881f09
69fd5d5
e7f9cb8
5f5d68f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,6 +35,7 @@ | |
* | ||
* @author Mark Paluch | ||
* @author Juergen Hoeller | ||
* @author Enric Sala | ||
* @since 5.2 | ||
* @see #execute | ||
* @see ReactiveTransactionManager | ||
|
@@ -70,40 +71,16 @@ public ReactiveTransactionManager getTransactionManager() { | |
return this.transactionManager; | ||
} | ||
|
||
@Override | ||
public <T> Mono<T> transactional(Mono<T> mono) { | ||
return TransactionContextManager.currentContext().flatMap(context -> { | ||
Mono<ReactiveTransaction> status = this.transactionManager.getReactiveTransaction(this.transactionDefinition); | ||
// This is an around advice: Invoke the next interceptor in the chain. | ||
// This will normally result in a target object being invoked. | ||
// Need re-wrapping of ReactiveTransaction until we get hold of the exception | ||
// through usingWhen. | ||
return status.flatMap(it -> Mono.usingWhen(Mono.just(it), ignore -> mono, | ||
this.transactionManager::commit, (res, err) -> Mono.empty(), this.transactionManager::rollback) | ||
.onErrorResume(ex -> rollbackOnException(it, ex).then(Mono.error(ex)))); | ||
}) | ||
.contextWrite(TransactionContextManager.getOrCreateContext()) | ||
.contextWrite(TransactionContextManager.getOrCreateContextHolder()); | ||
} | ||
|
||
@Override | ||
public <T> Flux<T> execute(TransactionCallback<T> action) throws TransactionException { | ||
return TransactionContextManager.currentContext().flatMapMany(context -> { | ||
Mono<ReactiveTransaction> status = this.transactionManager.getReactiveTransaction(this.transactionDefinition); | ||
// This is an around advice: Invoke the next interceptor in the chain. | ||
// This will normally result in a target object being invoked. | ||
// Need re-wrapping of ReactiveTransaction until we get hold of the exception | ||
// through usingWhen. | ||
return status.flatMapMany(it -> Flux | ||
.usingWhen( | ||
Mono.just(it), | ||
Comment on lines
-93
to
-99
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was hesitant about filing a PR because I wasn't sure I was fully understanding this part. IIUC we could now simplify this because the Relates to this comment: https://github.com/spring-projects/spring-framework/pull/23562/files#r319925489 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The previous code commits the transaction depending on successful/exceptional completion/cancellation. Any failure in the async cleanup methods (specifically To solve the issue, we need to call It is possible that we need to refine the actual exception if it is emitted by the async clean up afterward. Note that the same issue exists in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the feedback @mp911de :) Do you mean something like this? Flux.usingWhen(
this.transactionManager.getReactiveTransaction(this.transactionDefinition),
action::doInTransaction,
this.transactionManager::commit,
this::rollbackOnException,
this.transactionManager::rollback)) Was indeed going for something like this initially but as you mentioned it would require refining the exception, because a failure on either
Maybe that's ok? But that's why I thought that the
Good point! Shall I apply these changes also over there, or do you think separate PR? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That bit looks decent. The more operators we use the more performance impact we can generate. Regarding exception mapping, I'm not sure which top-level exception should be propagated downstream. Looking into the transaction manager, a
The issue is the same and it makes sense to fix the same problem in multiple places at once as we can keep the context within one commit from your side. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pushed the changes. Went ahead with a message-based approach to the unwrapping, maybe there is a utility for this? 🤔 Didn't find anything in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Paging @simonbasle, maybe Simon can give further insights. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there isn't such an unwrapping utility currently, not for the sake of detecting that particular kind of exception. @EnricSala note that in the case of a rollback which fails, we have a
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the reply! Agree, the exception propagated by I squashed the changes on the branch leaving only two commits: the first one shows the implementation with exception unwrapping and the second commit shows what it would look like if we drop the unwrapping. Both options resolve my problem, so I think at this point it may be a matter of preference or consistency with the rest of the framework. Please let me know which option would be preferable :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It makes sense to unwrap the exception as callers typically do not expect a generic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would also have a preference for this option because the I have applied the change, it should be ready for review :) |
||
action::doInTransaction, | ||
this.transactionManager::commit, | ||
(tx, ex) -> Mono.empty(), | ||
this.transactionManager::rollback) | ||
.onErrorResume(ex -> | ||
rollbackOnException(it, ex).then(Mono.error(ex)))); | ||
}) | ||
return TransactionContextManager.currentContext().flatMapMany(context -> | ||
Flux.usingWhen( | ||
this.transactionManager.getReactiveTransaction(this.transactionDefinition), | ||
action::doInTransaction, | ||
this.transactionManager::commit, | ||
this::rollbackOnException, | ||
this.transactionManager::rollback) | ||
.onErrorMap(this::unwrapIfResourceCleanupFailure)) | ||
.contextWrite(TransactionContextManager.getOrCreateContext()) | ||
.contextWrite(TransactionContextManager.getOrCreateContextHolder()); | ||
} | ||
|
@@ -121,11 +98,28 @@ private Mono<Void> rollbackOnException(ReactiveTransaction status, Throwable ex) | |
if (ex2 instanceof TransactionSystemException tse) { | ||
tse.initApplicationException(ex); | ||
} | ||
else { | ||
ex2.addSuppressed(ex); | ||
} | ||
return ex2; | ||
} | ||
); | ||
} | ||
|
||
/** | ||
* Unwrap the cause of a throwable, if produced by a failure | ||
* during the async resource cleanup in {@link Flux#usingWhen}. | ||
* @param ex the throwable to try to unwrap | ||
*/ | ||
private Throwable unwrapIfResourceCleanupFailure(Throwable ex) { | ||
if (ex instanceof RuntimeException && | ||
ex.getCause() != null && | ||
ex.getMessage().startsWith("Async resource cleanup failed")) { | ||
return ex.getCause(); | ||
} | ||
return ex; | ||
} | ||
|
||
|
||
@Override | ||
public boolean equals(@Nullable Object other) { | ||
|
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.
A similar default implementation existed in the past but using
.next()
instead of.singleOrEmpty()
. This was changed in gh-23562 because.next()
triggers the cancellation of the source.I believe we could use
.singleOrEmpty()
which IIUC should not cancel the source before receiving the completion signal from theMono
.Please let me know if this exceeds the scope of the issue and I would drop it from the PR.