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.