Skip to content
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

Closed
wants to merge 4 commits into from

Conversation

EnricSala
Copy link
Contributor

@EnricSala EnricSala commented Oct 18, 2021

A failure to commit a reactive transaction will complete the transaction and clean up resources. Executing a rollback at that point is invalid, which causes an IllegalTransactionStateException that masks the cause of the commit failure.

This change restructures TransactionalOperatorImpl and ReactiveTransactionSupport to avoid executing a rollback after a failed commit. While there, the Mono transaction handling in TransactionalOperator is simplified by moving it to a default method on the interface.

See gh-27523

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Oct 18, 2021
Copy link
Contributor Author

@EnricSala EnricSala left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a couple of comments to explain some changes.

Comment on lines +73 to +74
default <T> Mono<T> transactional(Mono<T> mono) {
return execute(it -> mono).singleOrEmpty();
Copy link
Contributor Author

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 the Mono.

Please let me know if this exceeds the scope of the issue and I would drop it from the PR.

Comment on lines -93 to -99
// 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),
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 rollbackOnException has been moved to inside the usingWhen.

Relates to this comment: https://github.com/spring-projects/spring-framework/pull/23562/files#r319925489

Copy link
Member

Choose a reason for hiding this comment

The 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 commit because rollback happens upon cancellation) dropped into rollbackOnException which performs another round of cleanup and that is the actual bug.

To solve the issue, we need to call rollbackOnException in the transaction cleanup instead of onErrorResume(…) and (tx, ex) -> Mono.empty(). That would align the flow with the imperative TransactionTemplate.

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 TransactionAspectSupport.ReactiveTransactionSupport.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 asyncComplete or asyncError wraps the exception like this:

java.lang.RuntimeException: Async resource cleanup failed after onComplete|onError

Maybe that's ok? But that's why I thought that the .onErrorResume(rollback) + .concatWith(commit) combination may achieve something similar and avoid the exception refining. Do you think it would be preferable to go with the asyncComplete&asyncError + exception cleanup approach?

Note that the same issue exists in TransactionAspectSupport.ReactiveTransactionSupport.

Good point! Shall I apply these changes also over there, or do you think separate PR?

Copy link
Member

Choose a reason for hiding this comment

The 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 RuntimeException seems fine. Maybe @jhoeller can provide a bit more guidance.

Shall I apply these changes also over there, or do you think separate PR?

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 reactor.core.Exceptions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Paging @simonbasle, maybe Simon can give further insights.

Copy link
Contributor

Choose a reason for hiding this comment

The 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 RuntimeException with:

  1. the cause of the rollback failure as the getCause()
  2. the original exception in the usingWhen which triggered the rollback in the first place (the rollbackCause) as a suppressed exception

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the reply! Agree, the exception propagated by usingWhen meets the criteria so leaving it untouched would be an option 👍

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 :)

Copy link
Member

Choose a reason for hiding this comment

The 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 RuntimeException.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also have a preference for this option because the RuntimeException feels a bit synthetic, it's only there due to the usingWhen implementation, and it doesn't align with the behavior of non-reactive transactions.

I have applied the change, it should be ready for review :)

@rstoyanchev rstoyanchev added the in: data Issues in data modules (jdbc, orm, oxm, tx) label Nov 10, 2021
@sdeleuze sdeleuze self-assigned this Sep 23, 2022
@sdeleuze sdeleuze added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Sep 23, 2022
@sdeleuze sdeleuze added this to the 5.3.24 milestone Sep 23, 2022
@sdeleuze
Copy link
Contributor

@mp911de Could you please review the latest version of this PR and confirm (or not) you are ok for merging it (I will also have a deeper look after your confirmation)?

@sdeleuze sdeleuze added this to the 6.0.6 milestone Mar 1, 2023
@sdeleuze
Copy link
Contributor

sdeleuze commented Mar 1, 2023

@EnricSala I have rebased the branch on top of main in https://github.com/sdeleuze/spring-framework/tree/gh-27523, but I see an error in CoroutinesTransactionInterceptorTests, could you please have a look and maybe rebased this PR on top of main potentially using my branch as a basis?

@sdeleuze sdeleuze modified the milestones: 6.0.6, 6.0.7 Mar 1, 2023
@EnricSala
Copy link
Contributor Author

@sdeleuze I have rebased this PR on top of main and added a commit to fix CoroutinesTransactionInterceptorTests. Please check if this small fix makes sense :)

@sdeleuze sdeleuze closed this in edf0ae7 Mar 6, 2023
@sdeleuze
Copy link
Contributor

sdeleuze commented Mar 6, 2023

Merged via edf0ae7 after @simonbasle and @mp911de green light. Thanks for contributing this and for your patience @EnricSala. Please test snapshots to check everything looks fine for your use cases.

sbrannen added a commit that referenced this pull request Jun 23, 2023
This commit fixes a regression introduced in conjunction with gh-27572.

See gh-30597
Closes gh-30729
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: data Issues in data modules (jdbc, orm, oxm, tx) type: bug A general bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants