19

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.

  • Is there a way to start a new session manually from within the session?
  • Or a way to tell the thread pool to do it for me?
  • What's the standard practice for doing this kind of work?

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.

naXa stands with Ukraine
  • 35,493
  • 19
  • 190
  • 259
Joel
  • 16,474
  • 17
  • 72
  • 93

3 Answers3

32

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
    }
}
Martin Frey
  • 10,025
  • 4
  • 25
  • 30
  • This technique has worked perfectly for my legacy Spring Roo 1.3.x application. I had to enhance the legacy app and it worked like a charm. Thank you!! – sunitkatkar Dec 07 '16 at 07:42
  • "To avoid this issue your async method should handle the complete work." this did the trick! – Joaquín L. Robles May 14 '18 at 14:26
  • This works, when the "heavy" work should be done in one transaction. How could we go about a workload that should be started async/scheduled, but should be split up into different transactions (so that, e.g., on an error in the second transaction, only the second transaction is rolled back)? – Dario Seidl Feb 26 '21 at 10:23
  • Thanks Mate! Once more, you solve a problem I was facing ;-) See you around – Camille Oct 06 '21 at 20:51
1

Method #1: JPA Entity Manager

In background thread: Inject entity manager or get it from Spring context or pass it as reference:

@PersistenceContext
private EntityManager entityManager;    

Then create a new entity manager, to avoid using a shared one:

EntityManager em = entityManager.getEntityManagerFactory().createEntityManager();

Now you can start transaction and use Spring DAO, Repository, JPA, etc

private void save(EntityManager em) {

    try
    {         
        em.getTransaction().begin();                

        <your database changes>

        em.getTransaction().commit();                        
    }
    catch(Throwable th) {
        em.getTransaction().rollback();
        throw th;
    }        
}

Method #2: JdbcTemplate

In case you need low-level changes or your task is simple enough, you can do it with JDBC and queries manually:

@Autowired
private JdbcTemplate jdbcTemplate;

and then somewhere in your method:

jdbcTemplate.update("update task set `status`=? where id = ?", task.getStatus(), task.getId());

Side note: I would recommend to stay away from @Transactional unless you use JTA or rely on JpaTransactionManager.

adlerer
  • 1,010
  • 11
  • 14
  • That's not correct! @Transactional does not need JTA. A transaction manager can also be a org.springframework.orm.jpa.JpaTransactionManager that uses the EntityManager to handle the transactions! – Simon Martinelli Feb 24 '18 at 14:34
  • Thank you for the feedback. I made the recommendation about @Transactional more specific now. It doesn't influence the content and correctness of the working recommendations on how to handle the problem in a background thread. – adlerer Feb 26 '18 at 08:35
  • 1
    I still do not agree because handling your own transaction can be cumbersome. In your example when calling em.getTransaction().rollback(); the transaction can be inactive. You have to check this! – Simon Martinelli Feb 26 '18 at 11:24
0

What happens is, probably, you have transaction on your DAO piece of code and Spring is closing the session on transaction close.

You should squeeze all your business logic into single transaction.

You can inject SessionFactory into your code and use SessionFactory.openSession() method.
The problem is, that you will have to manage your transactions.

naXa stands with Ukraine
  • 35,493
  • 19
  • 190
  • 259
alobodzk
  • 1,284
  • 2
  • 15
  • 27
  • Part of the problem is, I want to be able to update the status of the job while it's running. So if I wrap it all in a single transaction, the status doesn't update until the outer-most commit commits. Unless I'm missing something here... which is totally possible :) – Joel Jul 23 '14 at 17:06
  • Could you show your Runnable implementation class and maybe your ObjectRepository? I'm also kinda curious how and where do you intend to publish status updates. It's not that obvious with http based web apps. – alobodzk Jul 23 '14 at 17:17
  • Specifically to the updating the process status: use a nested transaction. Make sure your JPA provider supports them. – Virmundi Nov 23 '15 at 20:33