Search code examples
javadatabaseejbcluster-computingbean-managed-transactions

Start/end transaction in separate EJB methods


I developed a typical enterprise application that is responsible for provisioning customer to a 3rd party system. This system has a limitation, that only one thread can work on a certain customer. So we added a simple locking mechanism that consists of @Singleton which contains a Set of customerIds currently in progress. Whenever a new request comes for provisioning, it first checks this Set. If cusotomerId is present, it waits otherwise it adds it to the Set and goes into processing.

Recently it was decided, that this application will be deployed in cluster which means that this locking approach is no longer valid. We came up with a solution to use DB for locking. We created a table with single column that will contain customerIds (it also has a unique constraint). When a new provisioning request comes we start a transaction and try and lock the row with customerId with SELECT FOR UPDATE (if customerId does not exist yet, we insert it). After that we start provisioning customer and when finished, we commit transaction. Concept works but I have problems with transactions. Currently we have a class CustomerLock with add() and remove() methods that take care of adding and removing customerIds from Set. I wanted to convert this class to a stateless EJB that has bean-managed transactions. add() method would start a transaction and lock the row while remove() method would commit transaction and thus unlocked the row. But it seems that start and end of transaction has to happen in the same method. Is there a way to use the approach I described or do I have to modify the logic so the transaction starts and ends in the same method?

CustomerLock class:

@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
public class CustomerLock {

    @Resource
    private UserTransaction tx;

    public void add(String customerId) throws Exception {
        try {
            tx.begin();
            dsApi.lock()
        } catch (Exception e) {
            throw e;
        }
    }

    public void remove(String customerId) throws Exception {
        try {
            tx.commit();
        } catch (Exception e) {
            throw e
        }
    }
}

CustomerProvisioner class excerpt:

public abstract class CustomerProvisioner {

    ...

    public void execute(String customerId) {
        try {
            customerLock.add(customerId);

            processing....

            customerLock.remove(customerId);
        } catch (Exception e) {
            logger.error("Error", e);
        }
    }

    ...

}

StandardCustomerProvisioner class:

@Stateless
public class StandardCustomerProvisioner extends CustomerProvisioner {

    ...

    public void provision(String customerId) {
        // do some business logic
        super.execute(customerId);
    }
}

Solution

  • As @Gimby noted, you should not mix container-managed and bean-managed transactions. Since your StandardCustomerProvisioner has no annotation like "@TransactionManagement(TransactionManagementType.BEAN)" - it uses container-managed transactions, and REQUIRED by default.

    You have 2 options to make it work:

    1) To remove "@TransactionManagement(TransactionManagementType.BEAN)" with UserTransaction calls and run CMT

    2) Add this annotation ("@TransactionManagement(TransactionManagementType.BEAN)") to StandardCustomerProvisioner and use transaction markup calls from this method, so all the invoked methods use the same transactional context. Markup calls from CustomerLock should be removed anyway.