In hibernate5 SpringSessionContext, 'currentSession' checks the existence of a transactionManager and a jtaSessionContext. If they exist, it then checks the transactionManager status to see if the transaction is active. Unfortunately, if the transactionManager status is not active, the code does not throw an exception but instead 'falls through' to using a SessionHolder based session.
This session in some cases gets leaked permanently into the thread local 'TransactionSynchronizationManager.resources' (for some reason, the clear() method of this class does not clean up the resources thread-local').
From this point on, the thread is corrupted, because this session is always pulled out of the thread-local preferentially in 'currentSession'. (and in our case this session object is marked-for-rollback permanently). So that thread is essentially dead.
A proposed fix is to have an 'else' statement on the transactionManager status check that throws an exception.
Comment From: jhoeller
That fallback arrangement is meant to address scenarios where some transactions are driven by JTA whereas others are resource-local, against the same SessionFactory
. Admittedly that's a rare case but I'm not sure we can ignore it completely. It might be more common to have a JTA setup (for whatever reason), autodetected into Hibernate, but not actually used there... so that despite an existing JTA setup, SpringSessionContext
would always go into the fallback path. In any case, I'm afraid we can't simply reject such a scenario through an exception at such a late point.
Do you have any indications for when the session would accidentally leak? It should always get cleaned up by the registered SpringSessionSynchronization
; not sure how that can be bypassed... Even if an active JTA transaction comes in during a pre-synchronized local session, those callbacks on the original SpringSessionSynchronization
should still get honored and perform the cleanup.
Comment From: mwgreen
In our case, within a transaction context, a RuntimeException is thrown which puts the transaction in rollback state. After that, during transaction commit and completion, event listeners are fired which attempt to do additional database modifications. A session is grabbed, which due to the above code (since the transaction is in marked-for-rollback state and not 'active') causes the code to 'fall through'. In previous versions, this would cause another exception (which is fine, the transaction is already in a rollback state). In this version, it will still get a session, and attempt to do the additional work. That new 'session' THEN sees the 'marked for rollback' state and after that is permanently in the thread-local resources variable.
I'll try to get a stack trace of where the session is grabbed.
Comment From: mwgreen
SpringSessionContext.currentSession() line: 130
SessionFactoryImpl.getCurrentSession() line: 496
MetadataCurrentAuditRevisionService.setMetaModfied() line: 42
MetadataCurrentAuditRevisionService$$FastClassBySpringCGLIB$$de66f993.invoke(int, Object, Object[]) line: not available
MethodProxy.invoke(Object, Object[]) line: 218
CglibAopProxy$CglibMethodInvocation.invokeJoinpoint() line: 749
CglibAopProxy$CglibMethodInvocation(ReflectiveMethodInvocation).proceed() line: 163
DelegatingIntroductionInterceptor.doProceed(MethodInvocation) line: 136
DelegatingIntroductionInterceptor.invoke(MethodInvocation) line: 124
CglibAopProxy$CglibMethodInvocation(ReflectiveMethodInvocation).proceed() line: 186
CglibAopProxy$DynamicAdvisedInterceptor.intercept(Object, Method, Object[], MethodProxy) line: 688
MetadataCurrentAuditRevisionService$$EnhancerBySpringCGLIB$$e89cc801.setMetaModfied() line: not available
MetadataAuditedEntityInterceptor(AuditedEntityInterceptor).setMetaModified(Object) line: 156
MetadataAuditedEntityInterceptor(AuditedEntityInterceptor).onSave(Object, Serializable, Object[], String[], Type[]) line: 115
DefaultSaveEventListener(AbstractSaveEventListener).substituteValuesIfNecessary(Object, Serializable, Object[], EntityPersister, SessionImplementor) line: 391
DefaultSaveEventListener(AbstractSaveEventListener).performSaveOrReplicate(Object, EntityKey, EntityPersister, boolean, Object, EventSource, boolean) line: 271
DefaultSaveEventListener(AbstractSaveEventListener).performSave(Object, Serializable, EntityPersister, boolean, Object, EventSource, boolean) line: 196
DefaultSaveEventListener(AbstractSaveEventListener).saveWithGeneratedId(Object, String, Object, EventSource, boolean) line: 139
DefaultSaveEventListener(DefaultSaveOrUpdateEventListener).saveWithGeneratedOrRequestedId(SaveOrUpdateEvent) line: 192
DefaultSaveEventListener.saveWithGeneratedOrRequestedId(SaveOrUpdateEvent) line: 38
DefaultSaveEventListener(DefaultSaveOrUpdateEventListener).entityIsTransient(SaveOrUpdateEvent) line: 177
DefaultSaveEventListener.performSaveOrUpdate(SaveOrUpdateEvent) line: 32
DefaultSaveEventListener(DefaultSaveOrUpdateEventListener).onSaveOrUpdate(SaveOrUpdateEvent) line: 73
SessionImpl.fireSave(SaveOrUpdateEvent) line: 713
SessionImpl.save(String, Object) line: 705
DefaultAuditStrategy(DefaultAuditStrategy).perform(Session, String, AuditEntitiesConfiguration, Serializable, Object, Object) line: 49
DefaultAuditStrategy(AuditStrategy).perform(Session, String, EnversService, Serializable, Object, Object) line: 46
AddWorkUnit(AbstractAuditWorkUnit).perform(Session, Object) line: 68
AuditProcess.executeInSession(Session) line: 125
AuditProcess.doBeforeTransactionCompletion(SessionImplementor) line: 174
AuditProcessManager$1.doBeforeTransactionCompletion(SessionImplementor) line: 47
ActionQueue$BeforeTransactionCompletionProcessQueue.beforeTransactionCompletion() line: 954
ActionQueue.beforeTransactionCompletion() line: 525
SessionImpl.beforeTransactionCompletion() line: 2527
JdbcCoordinatorImpl.beforeTransactionCompletion() line: 473
JtaTransactionCoordinatorImpl.beforeCompletion() line: 352
SynchronizationCallbackCoordinatorTrackingImpl(SynchronizationCallbackCoordinatorNonTrackingImpl).beforeCompletion() line: 47
RegisteredSynchronization.beforeCompletion() line: 37
BitronixTransaction.fireBeforeCompletionEvent() line: 543
BitronixTransaction.commit() line: 241
BitronixTransactionManager.commit() line: 183
JtaTransactionManager.doCommit(DefaultTransactionStatus) line: 1034
JtaTransactionManager(AbstractPlatformTransactionManager).processCommit(DefaultTransactionStatus) line: 746
JtaTransactionManager(AbstractPlatformTransactionManager).commit(TransactionStatus) line: 714
AnnotationTransactionAspect(TransactionAspectSupport).commitTransactionAfterReturning(TransactionAspectSupport$TransactionInfo) line: 533
AnnotationTransactionAspect(TransactionAspectSupport).invokeWithinTransaction(Method, Class<?>, InvocationCallback) line: 304
AnnotationTransactionAspect(AbstractTransactionAspect).ajc$around$org_springframework_transaction_aspectj_AbstractTransactionAspect$1$2a73e96c(Object, AroundClosure, JoinPoint$StaticPart) line: 70
Comment From: mwgreen
One thing, in the code
if (this.transactionManager != null && this.jtaSessionContext != null) {
try {
if (this.transactionManager.getStatus() == Status.STATUS_ACTIVE) {
In this case doesn't this mean that there IS a JTA transaction 'context' but it is not active? In this case shouldn't it NOT give you a session and instead throw an exception?
Comment From: snicoll
during transaction commit and completion, event listeners are fired which attempt to do additional database modifications.
@mwgreen at this stage, I think we need a small sample that showcases what you mean. I don't know if that's something we should reject upfront.
Comment From: mwgreen
I apologize, this was so long ago I don't have an example, and I've forgotten most of the issue. But thank you for responding
Comment From: snicoll
Alright, if it turns out you hit this again, please comment with the same and we can reopen.