Reputation: 876
The amount of grey hair has dramatically increased in last couple of days while trying to resolve the following problem. I'm using Spring Data JPA repositories in custom event listeners that utilises simple Spring 3.2 eventing mechanism. The problem I'm having is that if ListenerA
creates an entity and calls assetRepository.save(entity)
or assetRepository.saveAndFlash(entity)
the subsequent calls to retrieve this same entity from another listener fails. The cause seems to be that the ListenerB
can not find the original entity in the database, it seem to be still in Hibernate's cache.
The trigger for ListenerB to lock up the entity is an event fired as a result of a runnable task execution from a thread pool.
Here is my configuration:
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="persistenceUnitName" value="spring-jpa" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="generateDdl" value="false" />
<property name="database" value="#{appProps.database}" />
</bean>
</property>
<property name="jpaProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.Oracle10gDialect</prop>
<prop key="hibernate.hbm2ddl.auto">#{appProps['hibernate.hbm2ddl.auto']}</prop>
<prop key="hibernate.show_sql">#{appProps['hibernate.show_sql']}</prop>
<prop key="hibernate.format_sql">#{appProps['hibernate.format_sql']}</prop>
<prop key="hibernate.search.default.directory_provider">org.hibernate.search.store.impl.FSDirectoryProvider</prop>
<prop key="hibernate.search.default.indexBase">#{appProps.indexLocation}</prop>
<prop key="hibernate.search.lucene_version">#{appProps['hibernate.search.lucene_version']}</prop>
</props>
</property>
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
<property name="jpaDialect">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect" />
</property>
</bean>
I'm omitting the dataSource
configuration which is an instance of ComboPooledDataSource
that defines connection to Oracle database. As a side note, component scanning is used and the project is Spring MVC.
Now Java classes.
ListenerA
@Sevice
public class ListenerA implements ApplicationListener<FileUploadedEvent> {
@Autowired
private AssetRepository assetRepository;
@Autowired
private ExecutorService executor; // Triggers runnable task on a Job in Spring's TaskExecutor
@Override
@Transactional
public void onApplicationEvent(FileUploadedEvent event) {
Asset target = event.getTarget();
Job job = new Job(target);
assetRepository.save(job);
executor.execute(job);
}
ListenerB
@Sevice
public class ListenerB implements ApplicationListener<JobStartedEvent> {
@Autowired
private AssetRepository assetRepository;
@Override
@Transactional
public void onApplicationEvent(JobStartedEvent event) {
String id = event.getJobId();
Job job = assetRepository.findOne(id); // at this point we can not find the job, returns null
job.setStartTime(new DateTime());
job.setStatus(Status.PROCESSING);
assetRepository.save(job);
}
JobStartedEvent
is fired from a runnable task within TaskExecutor
.
What I'm doing wrong here? I have tried to use custom event publisher that is transaction aware, but that doesn't seem to solve the problem. I have also tried to wire appropriate service instead of data repository and remove @Transactional
annotations from listeners, which also have failed. Any reasonable suggestions of how to solve the problem would be welcome.
Upvotes: 3
Views: 4133
Reputation: 2049
While this is slightly old, I ran in to this same problem but now with Spring 4.1.1.RELEASE, Spring Data JPA 1.7.0 and Hibernate 4.3.5.Final
My scenario occurred during testing with some tests failing. During testing, our problems were caused by H2 in single connection mode, broadcasting asynchronous events and event transactionality.
Solutions
First problem was due to transaction timeout and was solved by adding MVCC=true
to the H2 URL string. See: https://stackoverflow.com/a/6357183/941187
Asynchronous Events were causing issues during tests since they executed on different threads. In the event configuration, a task executor and thread pool were used. To fix, just provided an overridden configuration bean using the SyncTaskExecutor
as the task executor. This will cause all events to occur synchronously.
The event transactionality was tricky. In our framework, event get broadcast from within a transaction (@Transactional
). The event was then handled on another thread outside of the transaction context. This introduced a race condition since the handler often depended on the object from the transaction to have had been committed. We didn't notice the problem on our Windows development machines but it became apparent when deployed to production on Linux. The solution uses TransactionSynchronizationManager.registerSynchronization()
with an implementation of TransactionSynchronization.afterCommit()
to broadcast the event after committing. See http://www.javacodegeeks.com/2013/04/synchronizing-transactions-with-asynchronous-events-in-spring.html for more info and examples.
Related to #3, we had to add @Transactional(propagation = REQUIRES_NEW)
for some of the service methods called from some of the event handlers.
Hopefully this helps some late comers.
Upvotes: 1
Reputation: 876
I have managed to resolve the problem thanks to the hint from @Kresimir Nesek. So the solution was to replace Spring Data repositories with appropriate services. Here are modified classes.
Listener A
@Sevice
public class ListenerA implements ApplicationListener<FileUploadedEvent> {
@Autowired
private JobService service;
@Autowired
private ExecutorService executor; // Triggers runnable task on a Job in Spring's TaskExecutor
@Override
public void onApplicationEvent(FileUploadedEvent event) {
Job job = service.initJobForExecution(event.getTarget());
executor.execute(job);
}
}
In the JobService
method initJobForExecution(Asset target)
had to be annotated with @Transactional(propagation=Propagation.REQUIRES_NEW)
for everything to work.
Listener B
@Sevice
public class ListenerB implements ApplicationListener<JobStartedEvent> {
@Autowired
private JobService service;
@Override
public void onApplicationEvent(JobStartedEvent event) {
service.updateStatus(event.getJobId(), Status.PROCESSING);
}
}
Upvotes: 2