Search code examples
jsfasynchronousejbcdi

How to correctly use CDI in @Asynchronous method?


I have a JSF 2 application (running on top of JBoss AS 7.1) that has to start a long process when the user clicks a button in a page. The idea is to have a not blocking interaction, so the user can wait and see the results or simply close the page and get back later to see how it is going or the results, if the process is concluded already.

The process itself is coded in a following (simplified) class:

@Stateless
@LocalBean
@ApplicationScoped
public class MyProcessManager {
    @Inject
    private ProcessHelper processHelper;

    @Asynchronous
    public void start(final ProcessParameters parameters) {
        // the process...
    }
}

Such a class is marked as @ApplicationScoped because all the processes running (for all the users) are kept by it. So, when the button is clicked, the backing bean sets some parameters and calls the asynchronous method start().

Everything goes ok until the moment the process tries to use processHelper, which runs a number of Hibernate queries in order to proceed with the persistence part of the process. When the first method of processHelper is called I get the follwing exception:

WELD-001303: No active contexts for scope type javax.enterprise.context.RequestScoped

As an additional info, a breakpoint inside of such a method is never hit.

What is happening and how to fix it?


Solution

  • The exception suggests that ProcessHelper is @RequestScoped.

    When @Asynchronous is invoked, a brand new and separate thread is spawned which is not controlled by the HTTP servlet container. In the context of that thread, there's thus no means of a HTTP request or HTTP session anywhere. You can only use @ApplicationScoped, not @RequestScoped, let alone @SessionScoped.

    As to the ProcessManager itself, the combination @Stateless @ApplicationScoped doesn't make sense. You most probably actually want a @javax.ejb.Singleton. Additional bonus is that it's stateful, so you could just keep the process results there as an instance variable.

    You mentioned that ProcessHelper in turn runs some DB queries. Which means that it should run in transaction. In that case you should make it a fullworthy EJB instead of a CDI managed bean. So make ProcessHelper a @Stateless too, or just move all the DB interaction job into the ProcessManager EJB. That's also possible.

    So, all in all, this should do it:

    <h:form>
        <h:commandButton value="Start" action="#{processBacking.start}" />
    </h:form>
    <p>
        Result (manually refresh page to check): #{processBacking.result}
    </p>
    

    @Named
    @RequestScoped
    public class ProcessBacking {
    
        @Inject
        private ProcessManager processManager;
    
        public void start() {
            // ...
            processManager.start(parameters);
        }
    
        public ProcessResult getResult() {
            return processManager.getResult();
        }
    
        // ...
    }
    

    @Singleton
    @ConcurrencyManagement(ConcurrencyManagementType.BEAN)
    public class ProcessManager {
    
        private ProcessResult result;
    
        @Inject
        private ProcessHelper helper;
    
        @Asynchronous
        @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
        public void start(ProcessParameters parameters) {
            ProcessResult result = runSomeLongRunningNonTransactionalProcess(parameters);
            this.result = helper.persist(result);
        }
    
        public ProcessResult getResult() {
            return result;
        }
    
    }
    

    @Stateless
    public class ProcessHelper {
    
        @PersistenceContext
        private EntityManager entityManager;
    
        @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
        public ProcessResult persist(ProcessResult result) {
            entityManager.persist(result);
            return result;
        }
    
    }
    

    Do note that a @Singleton is by default read/write locked. So you can't invoke getResult() until the start() is finished. Hence the ConcurrencyManagementType.BEAN, which means that it's unlocked and thus essentially the caller is itself responsible for concurrency management. This allows you to keep refreshing the page as long as the process still runs.

    See also: