When using TransactionTemplate
in an Executor
thread, it appears unable to consistently find the current transaction, resulting in problems like this:
14:20:56.022 [pool-2-thread-1] DEBUG HibernateTransactionManager - Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_SERIALIZABLE
14:20:56.022 [pool-2-thread-1] DEBUG HibernateTransactionManager - Opened new Session [SessionImpl(1516598186<open>)] for Hibernate transaction
14:20:56.022 [pool-2-thread-1] DEBUG HibernateTransactionManager - Preparing JDBC Connection of Hibernate Session [SessionImpl(1516598186<open>)]
14:20:56.023 [pool-2-thread-1] DEBUG HibernateTransactionManager - Exposing Hibernate transaction as JDBC [org.springframework.orm.hibernate5.HibernateTransactionManager$$Lambda$1238/0x00000008009dfc40@5cc5f4]
14:20:56.026 [pool-2-thread-1] DEBUG HibernateTransactionManager - Found thread-bound Session [SessionImpl(1516598186<open>)] for Hibernate transaction
14:20:56.026 [pool-2-thread-1] DEBUG HibernateTransactionManager - Participating in existing transaction
14:20:56.052 [pool-2-thread-1] DEBUG SessionFactoryUtils - Flushing Hibernate Session on transaction synchronization
14:20:56.054 [pool-2-thread-1] DEBUG HibernateTransactionManager - Initiating transaction rollback after commit exception
javax.persistence.TransactionRequiredException: no transaction is in progress
at org.hibernate.internal.AbstractSharedSessionContract.checkTransactionNeededForUpdateOperation(AbstractSharedSessionContract.java:445)
at org.hibernate.internal.SessionImpl.checkTransactionNeededForUpdateOperation(SessionImpl.java:3478)
at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1394)
at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1389)
at org.springframework.orm.hibernate5.SessionFactoryUtils.flush(SessionFactoryUtils.java:113)
at org.springframework.orm.hibernate5.SpringSessionSynchronization.beforeCommit(SpringSessionSynchronization.java:95)
at org.springframework.transaction.support.TransactionSynchronizationUtils.triggerBeforeCommit(TransactionSynchronizationUtils.java:97)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.triggerBeforeCommit(AbstractPlatformTransactionManager.java:916)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:727)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711)
at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:152)
at org.springframework.transaction.support.TransactionOperations.executeWithoutResult(TransactionOperations.java:67)
at com.example.MyClass.doProcessing(MyClass.java:109)
at com.example.MyClass.lambda$scheduleJob$4(MyClass.java:93)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:829)
14:20:56.054 [pool-2-thread-1] DEBUG HibernateTransactionManager - Rolling back Hibernate transaction on Session [SessionImpl(1516598186<open>)]
14:20:56.056 [pool-2-thread-1] DEBUG HibernateTransactionManager - Closing Hibernate Session [SessionImpl(1516598186<open>)] after transaction
JPA is configured manually, because I have multiple DataSource
s in the application.
Relevant beans:
@Bean
@TelemetryDb
public LocalContainerEntityManagerFactoryBean telemetryEntityManagerFactory(
EntityManagerFactoryBuilder builder,
@TelemetryDb DataSource dataSource
) {
return builder.dataSource(dataSource)
.packages("com.example.model.telemetry")
.persistenceUnit("telemetry")
.properties(getVendorProperties(dataSource))
.jta(false)
.build();
}
@Bean
@TelemetryDb
@Order(Ordered.HIGHEST_PRECEDENCE)
public PlatformTransactionManager telemetryTransactionManager(@TelemetryDb EntityManagerFactory factory) {
return new HibernateTransactionManager((SessionFactory) factory);
}
@Configuration
@EnableJpaRepositories(
basePackageClasses = PackageMarker.class,
includeFilters = @ComponentScan.Filter(type = FilterType.CUSTOM, value = TelemetryFilter.class),
entityManagerFactoryRef = "telemetryEntityManagerFactory",
transactionManagerRef = "telemetryTransactionManager"
)
public static class Telemetry { }
The properties are:
hibernate.auto_quote_keyword=true
hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext
hibernate.dialect=com.example.PostgreSQL12Dialect
And some code that gives the above log:
@Service
@RequriedArgsConstructor
public class MyClass {
private final MyRepository repository;
private ExecutorService executor = Executors.newSingleThreadExecutor();
private SessionFactory sessionFactory;
private TransactionTemplate transactionTemplate;
@Autowired
public void setTransactionManager(@TelemetryDb PlatformTransactionManager manager) {
this.sessionFactory = ((HibernateTransactionManager) manager).getSessionFactory();
this.transactionTemplate = new TransactionTemplate(manager);
this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
}
public scheduleJob(long entityId) {
Boolean submit = transactionTemplate.execute(status -> {
MyEntity entity = repository.findById(entity).orElseThrow();
if (entity.isProcessing()) {
return false;
} else {
entity.setProcessing(true);
return true;
}
});
if (Boolean.TRUE.equals(submit)) {
executor.submit(() -> doProcessing(entityId));
}
}
protected void doProcessing(long entityId) {
transactionTemplate.executeWithoutResult(status -> {
MyEntity entity = repository.findById(entity).orElseThrow();
entity.setBlobField(sessionFactory.getCurrentSession().getLobHelper().createBlob(new byte[]{});
entity.setProcessing(false);
status.flush();
});
}
}
Note that the usage of TransactionTemplate
outside of the executor thread completes with no issues. Note also that the createBlob()
and flush()
do not complain about a missing transaction.
I assume I have missed something in the configuration, but I have been trying various permutations of it, and the doProcessing
method, but cannot get anything to work.
After further debugging, it appears that the getCurrentSession()
call causes SpringSessionContext
to look for, and then create, a session using a SessionFactoryImpl
as the key rather than the LocalContainerEntityManagerFactoryBean
proxy.
There doesn't seem to be any way to control the construction of the context within Hibernate. I think if I could obtain the raw SessionFactoryImpl
to pass to HibernateTransactionManager
then that would fix it.
I tried that (using EntityManagerFactory.unrwap
) but that broke a whole load of other stuff, because when Spring Data calls createQuery
it looks for the current session using the bean proxy.
It appears that SpringSessionContext
doesn't work as intended. Either there was a change in Hibernate and/or there wasn't sufficient integration testing.
There is a workaround. Ignore SessionFactory.getCurrentSession()
and use the TransactionSynchronizationManager
directly:
@PersistenceUnit(unitName = "telemetry")
private EntityManagerFactory entityManagerFactory;
private @NonNull Session getCurrentSession() {
Object resource = TransactionSynchronizationManager.getResource(entityManagerFactory);
if (resource == null) {
throw new IllegalStateException("No current transaction");
}
return ((SessionHolder) resource).getSession();
}
That only works within an existing session. More code would be required to create a new one to match the full behaviour.
Unsetting hibernate.current_session_context_class
ensures you will always get an exception when trying to use the incompatible interface, rather than getting surprise extra sessions.
Another (simpler) workaround:
@PersistenceUnit(unitName = "telemetry")
private EntityManager entityManager;
private @NonNull Session getCurrentSession() {
return entityManager.unwrap(Session.class);
}