15

I'm using Spring with JPA. I have @EnableAsync and @EnableTransactionManagement turned on. In my user registration service method, I have a few other service methods I call that are annotated @Async. These methods do various things like sending a welcome email and registering the newly minted user with our third party payment system.

Everything works well until I want to verify that the third party payment system successfully created the user. At that point, the @Async method attempts to create a UserAccount (that references the newly minted User) and errors out with a javax.persistence.EntityNotFoundException: Unable to find com.dk.st.model.User with id 2017

The register call looks like this:

private User registerUser(User newUser, Boolean waitForAccount) {
    String username = newUser.getUsername();
    String email = newUser.getEmail();

    // ... Verify the user doesn't already exist

    // I have tried all manner of flushing and committing right here, nothing works
    newUser = userDAO.merge(newUser);

    // Here is where we register the new user with the payment system.
    // The User we just merged is not /actually/ in the DB
    Future<Customer> newCustomer = paymentService.initializeForNewUser(newUser);
    // Here is where I occasionally (in test methods) pause this thread to wait
    // for the successful account creation.
    if (waitForAccount) {
        try {
            newCustomer.get();
        } catch (Exception e) {
            logger.error("Exception while creating user account!", e);
        }
    }

    // Do some other things that may or may not be @Aysnc

    return newUser;
}

The payment service calls out to do its work of registering the user and looks like this:

@Async
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Future<Customer> initializeForNewUser(User newUser) {
    // ... Set up customerParams

    Customer newCustomer = null;
    try {
        newCustomer = Customer.create(customerParams);

        UserAccount newAccount = new UserAccount();
        newAccount.setUser(newUser);
        newAccount.setCustomerId(newCustomer.getId());
        newAccount.setStatus(AccountStatus.PRE_TRIAL);

        // When merging, JPA cannot find the newUser object in the DB and complains
        userAccountDAO.merge(newAccount);
    } catch (Exception e) {
        logger.error("Error while creating UserAccount!", e);
        throw e;
    }

    return new AsyncResult<Customer>(newCustomer);
}

The StackOverflow answer listed here suggests that I set a REQUIRES_NEW propagation, which I have done, but with no such luck.

Can anyone point me in the right direction? I really don't want to have to call the paymentService directly from my controller method. I feel that it should be a service level call for sure.

Thanks for any help!

Community
  • 1
  • 1
Michael Ressler
  • 851
  • 1
  • 9
  • 17
  • 1
    At first read i would say the newly created user is not flush before the payment service is executed – Vyncent Mar 25 '15 at 14:52
  • 1
    I thought the same thing. I added a flush() call to the userDAO.merge() call so that it made the user immediately available, but no such luck. It seems like the `@Async` `@Transactional` method is running in a different DB session entirely. – Michael Ressler Mar 25 '15 at 20:01
  • 1
    FYI: that is *exactly* what's happening. An @Async methodcall is handled on a different thread (that is what async means), which by definition means a different transaction. – Buurman Aug 03 '18 at 06:59

2 Answers2

12

With Vyncent's help, here is the solution that I arrived at. I created a new class called UserCreationService and put all of the method that handled User creation in that class. Here is an example:

@Override
public User registerUserWithProfileData(User newUser, String password, Boolean waitForAccount) {
    newUser.setPassword(password);
    newUser.encodePassword();
    newUser.setJoinDate(Calendar.getInstance(TimeZone.getTimeZone("UTC")).getTime());

    User registered = userService.createUser(newUser);
    registered = userService.processNewRegistration(registered, waitForAccount);

    return userService.setProfileInformation(registered);
}

You'll notice that there is NO @Transactional annotation on this method. This is on purpose. The corresponding createUser and processNewRegistration definitions look like this:

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public User createUser(User newUser) {
    String username = newUser.getUsername();
    String email = newUser.getEmail();

    if ((username != null) && (userDAO.getUserByUsername(username) != null)) {
        throw new EntityAlreadyExistsException("User already registered: " + username);
    }

    if (userDAO.getUserByUsername(newUser.getEmail()) != null) {
        throw new EntityAlreadyExistsException("User already registered: " + email);
    }

    return userDAO.merge(newUser);
}

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public User processNewRegistration(
        User newUser,
        Boolean waitForAccount) 
{
    Future<UserAccount> customer = paymentService.initializeForNewUser(newUser);
    if (waitForAccount) {
        try {
            customer.get();
        } catch (Exception e) {
            logger.error("Error while creating Customer object!", e);
        }
    }

    // Do some other maintenance type things...

    return newUser;
}

Vyncent was spot on that transaction management was the issue. Creating the other service allowed me to have better control over when those transactions committed. While I was hesitant to take this approach initially, that's the tradeoff with Spring managed transactions and proxies.

I hope this helps someone else save some time later.

Michael Ressler
  • 851
  • 1
  • 9
  • 17
4

Make a try by creating a new UserService class to manage user check, like so

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public User createOrUpdateUser(User newUser) {
    String username = newUser.getUsername();
    String email = newUser.getEmail();

    // ... Verify the user doesn't already exist

    // I have tried all manner of flushing and committing right here, nothing works
    newUser = userDAO.merge(newUser);
    return newUser;
}

then in the actual class, change

private User registerUser(User newUser, Boolean waitForAccount) {
    String username = newUser.getUsername();
    String email = newUser.getEmail();

    // ... Verify the user doesn't already exist

    // I have tried all manner of flushing and committing right here, nothing works
    newUser = userDAO.merge(newUser);

by

private User registerUser(User newUser, Boolean waitForAccount) {
    newUser = userService.createOrUpdateUser(newUser);

The new userService with @Transactional REQUIRES_NEW should force the commit and solve the issue.

Vyncent
  • 1,185
  • 7
  • 17
  • This actually led me down a path (and a deeper understanding - thank you!) that I believe means this could never work as intended. Since the transaction **REQUIRES_NEW**, an entirely separate transaction, fully isolated from the first, is created. Any subsequent `merge` called on the `User` object in the calling transaction will attempt to insert a **new** `User` (because it's isolated from the other transaction) and fail unique constraints. Womp. – Michael Ressler Mar 27 '15 at 06:13
  • It may work as you want if you wrote thé user update in a doInTransaction code block. As son as i cet acces to a laptop i pût an example – Vyncent Mar 27 '15 at 06:31
  • On further consideration, I think it could work if the calling function does very little and instead delegates a lot of the work to the **new** UserService (that I'm calling UserCreationService). I'll try this tomorrow and report back. Thanks for the help. – Michael Ressler Mar 27 '15 at 06:39
  • I guess an other trouble is the calling method have a transaction open – Vyncent Mar 27 '15 at 06:58