I'm building a simple Tomcat webapp that's using Spring Data and Hibernate. There's one end point that does a lot of work, so I want to offload the work to a background thread so that the web request doesn't hang for 10+ minutes while the work is being done. So I wrote a new Service in a component-scan'd package:
@Service
public class BackgroundJobService {
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
public void startJob(Runnable runnable) {
threadPoolTaskExecutor.execute(runnable);
}
}
Then have the ThreadPoolTaskExecutor
configured in Spring:
<bean id="threadPoolTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="5" />
<property name="maxPoolSize" value="10" />
<property name="queueCapacity" value="25" />
</bean>
This is all working great. However, the problem comes from Hibernate. Inside my runnable, queries only half work. I can do:
MyObject myObject = myObjectRepository.findOne()
myObject.setSomething("something");
myObjectRepository.save(myObject);
But if I have lazy loaded fields, it fails:
MyObject myObject = myObjectRepository.findOne()
List<Lazy> lazies = myObject.getLazies();
for(Lazy lazy : lazies) { // Exception
...
}
I get the following error:
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.stackoverflow.MyObject.lazies, could not initialize proxy - no Session
So it looks like to me (Hibernate newbie) that the new thread doesn't have a session on these home-made threads, but Spring Data is automatically creating new sessions for HTTP Request threads.
I've been able to work around it a little by doing everything from inside a @Transactional
method, but I'm quickly learning that's not a very good solution, as that doesn't let me use methods that work just fine for web requests.
Thanks.
You cant actually use both of them in the same application.
With Spring Data, you may use Hibernate, EclipseLink, or any other JPA provider.
You can read the input stream(i.e br. readLine() ) in a thread. That way, it's always running in the background. ReaderThread should continue reading the output of the process you have started, as long as it lasts.
With Spring you don't need your own executor. A simple annotation @Async
will do the work for you. Just annotate your heavyMethod
in your service with it and return void or a Future
object and you will get a background thread. I would avoid using the async annotation on the controller level, as this will create an asynchronous thread in the request pool executor and you might run out of 'request acceptors'.
The problem with your lazy exception comes as you suspected from the new thread which does not have a session. To avoid this issue your async method should handle the complete work. Don't provide previously loaded entities as parameters. The service can use an EntityManager and can also be transactional.
I for myself dont merge @Async
and @Transactional
so i can run the service in either way. I just create async wrapper around the service and use this one instead if needed. (This simplifies testing for example)
@Service
public class AsyncService {
@Autowired
private Service service;
@Async
public void doAsync(int entityId) {
service.doHeavy(entityId);
}
}
@Service
public class Service {
@PersistenceContext
private EntityManager em;
@Transactional
public void doHeavy(int entityId) {
// some long running work
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With