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

Doc: Nested declarative transaction management leads to UnexpectedRollbackException in case of a silent inner setRollbackOnly call [SPR-3452] #8135

Closed
spring-issuemaster opened this Issue May 4, 2007 · 9 comments

Comments

Projects
None yet
1 participant
@spring-issuemaster
Copy link
Collaborator

spring-issuemaster commented May 4, 2007

Thomas Hoppe opened SPR-3452 and commented

Hi,

First off: I'm not sure whether this is really a bug!

As I could not get a hint in the forum, nor the documentation, on the
topic discussed here:

http://forum.springframework.org/showthread.php?t=36298

I'm filing this as a bug.

A similar problem is described here:
http://forum.springframework.org/showthread.php?t=26574

The author describes his problem with an UnexpectedRollbackException
and Jürgen Höller responds that ...in the case of nested transactions with JTA, you have to roll back in the outer most transaction...

I think I'm doing this by defining propagation="REQUIRED" for nested method calls but rolling back only works when I roll back in the outer most method.
But why? With propagation="REQUIRED" all nested method invocations should pick up
the transaction of the outer most method (I confirmed this by looking at the debug output)


Affects: 2.0.4

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented May 4, 2007

Juergen Hoeller commented

As I've mentioned in those forum discussions, this is not a bug. In particular, note that rollbacks are processed correctly - the UnexpectedRollbackException is just thrown at the very end of the process, to inform the outer transaction's caller than a rollback has been caused, which the caller would otherwise not be aware of.

So you can of course mark a transaction as rollback-only at the outer or the inner level; it will always be correctly propagated. The difference is just that a caller of a transaction should never be misled to assume that a commit was performed when it really wasn't. So if an inner transaction (that the outer caller is not aware of) silently marks a transaction as rollback-only, the outer caller would still innocently call commit - and needs to receive an UnexpectedRollbackException to clearly indicate that a rollback was performed instead.

I hope that explanation makes some sense... This is a general problem with transaction participation semantics, and unfortunately we're not aware of any other proper way to solve this for the general case.

As for your particular scenario: Consider throwing an exception at the inner transaction level, then catching that exception at the outer level and translating it into a silent setRollbackOnly call there. This will work fine, with the transaction completing without an UnexpectedRollbackException then, since the setRollbackOnly call happened at the outermost level (where the outer caller is aware of it, so doesn't need to be notified through an exception).

Juergen

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented May 6, 2007

Thomas Hoppe commented

Hello Juergen,

I completely agree with your explanation given but my scenario is slightly different:

I would expect the behaviour like described by you if I had nested transactions!
But what I have is a BO layer with declarative transactions which are bound to any BO
method call BUT with propagation="REQUIRED". My understanding is that with this setting,
a nested call in my BO layer wont raise a new transaction because no matter
how deeply I nest, the transaction of the outer most call is used.

Therefore, I expected, that I can roll back in any nested call because it will always
roll back the one-and-only existing transaction.
But obviously, a transaction is created for every nested call.

I hope you have my point, maybe I misunderstood propagation="REQUIRED".

many thanks

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented May 7, 2007

Juergen Hoeller commented

I guess we need to differentiate between 'logical' transaction scopes and 'physical' transactions here...

What PROPAGATION_REQUIRED creates is a logical transaction scope for each method that it gets applied to. Each such logical transaction scope can individually decide on rollback-only status, with an outer transaction scope being logically independent from the inner transaction scope. Of course, in case of standard PROPAGATION_REQUIRED behavior, they will be mapped to the same physical transaction. So a rollback-only marker set in the inner transaction scope does affect the outer transaction's chance to actually commit. However, since the outer transaction scope did not decide on a rollback itself, the rollback (silently triggered by the inner transaction scope) comes unexpected at that level - which is why an UnexpectedRollbackException gets thrown.

PROPAGATION_REQUIRES_NEW, in contrast, uses a completely independent transaction for each affected transaction scope. In that case, the underlying physical transactions will be different and hence can commit or rollback independently, with an outer transaction not affected by an inner transaction's rollback status.

PROPAGATION_NESTED is different again in that it uses a single physical transaction with multiple savepoints that it can roll back to. Such partial rollbacks allow an inner transaction scope to trigger a rollback for its scope, with the outer transaction being able to continue the physical transaction despite some operations having been rolled back. This is typically mapped onto JDBC savepoints, so will only work with JDBC resource transactions (Spring's DataSourceTransactionManager).

To complete the discussion: UnexpectedRollbackException may also be thrown without the application ever having set a rollback-only marker itself. Instead, the transaction infrastructure may have decided that the only possible outcome is a rollback, due to constraints in the current transaction state. This is particularly relevant with XA transactions.

As I suggested above, throwing an exception at the inner transaction scope, then catching that exception at the outer scope and translating it into a silent setRollbackOnly call there should work for your scenario. A caller of the outer transaction will never see an exception then. Since you only worry about such silent rollbacks because of special requirements imposed by a caller, I would even argue that the correct architectural solution is to use exceptions within the service layer, and to translate those exceptions into silent rollbacks at the service facade level (right before returning to that special caller).

Since your problem is possibly not only about rollback exceptions, but rather about any exceptions thrown from your service layer, you could even use standard exception-driven rollbacks all the way throughout you service layer, and then catch and log such exceptions once the transaction has already completed, in some adapting service facade that translates your service layer's exceptions into UI-specific error states.

Juergen

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented May 7, 2007

Paul Benedict commented

What a novel. If anything, I'd like to see this exact explanation added to the Reference Documentation and not lost to JIRA. This is very good to know.

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented May 7, 2007

Juergen Hoeller commented

Good point, this is probably not explicitly documented anywhere... Let me take the occasion to turn this into a documentation issue for Rick.

FWIW, unless you're using silent setRollbackOnly calls (which is uncommon in general), there is not much need to worry about this differentiation for PROPAGATION_REQUIRED's behavior at all, since a thrown exception will implicitly trigger a rollback for the inner transaction scope as well as the outer transaction scope. It is of course good to know about the exact differences to PROPAGATION_REQUIRES_NEW and PROPAGATION_NESTED in any case.

Juergen

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented May 7, 2007

Thomas Hoppe commented

Hi All,

ok, this answers all my questions. I totally agree with Paul, this should definately go into the docs because I cannot remember, that I've read it somewhere.

As for my problem: I think I'm going to throw an application specific exception because I've found a way now
to catch exceptions on a very high level in JSF, there I can transform the exception in some user friendly outcome.

MANY THANKS

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented Jun 19, 2007

Rick Evans commented

Added a section discussing propagation semantics to the Transaction chapter of the Spring reference documentation.

Cheers
Rick

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented Oct 21, 2012

Mitsu Hadeishi commented

I have a comment about this case, and a question, if someone can address it. It seems to me that there are many reasons why you might want to be able to specify a policy which says "if there is an existing transaction with compatible semantics, do not create a new logical transaction." For instance, if your application has a design in which the service layer facade has many entry points, and you would like code which calls through the facade to initiate a transaction at that point, but not create new transactional contexts when the service layer calls another method "inside itself". It seems to me that this may not be the ideal application architecture, but it isn't a totally unreasonable one, either.

For instance, it may well be that an application would like to call some lower-level service methods but is OK with those service methods dying, for whatever cause, even a RuntimeException of some kind, and would like to catch and swallow that exception (log, recover in some way, etc.) without killing the overall transactional context. It seems to me this is a potentially valid thing to do. With this design, however, if you call into a service method which happens to be decorated with a PROPAGATION_REQUIRED annotation, it will create a new logical transaction even though that's not necessarily what you intend. The smaller service layer isn't really a new transactional scope, it's just a sub-process in a larger scope. One solution is to change the rollback rules for that method so it doesn't roll back on a RuntimeException, but you might want to leave that decision to the calling scope --- for instance, suppose that same interface is called by different code which does want the whole transaction to fail on a RuntimeException? I.e., it might get called directly from, say, a controller, and if it fails you do want the transaction rolled back, but a more complex service method might "know" it is OK if the whole transaction isn't rolled back.

I actually would argue it should be possible to have yet another option besides PROPAGATION_REQUIRED which supports this policy.

However, on doing some Googling on this issue I discovered this setting: setGlobalRollbackOnParticipationFailure() which appears to allow you to set this policy globally. Am I correct in assuming this? I understand that some conditions might still cause a global transaction rollback anyway, but would changing this to false allow, say, some random RuntimeException that wasn't necessarily fatal to the database transaction (i.e., which might be unrelated to the DB) be caught by the outer "logical" transaction and swallowed or logged?

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented Oct 22, 2012

Mitsu Hadeishi commented

Okay I verified that setting does do what I thought it would do. I've opened a ticket suggesting that you guys document this method more clearly in the Transaction section --- it took me quite a while to discover this, and I think there are strong reasons why a developer might want to set this to false.

https://jira.springsource.org/browse/SPR-9907

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.