jpaasynchronousplayframeworkasync-awaitplayframework-1.x

playframework 1.2.x: await / async and JPA transactions


I have a PUT request that is too long to run. I'd like to make it async, using continuations (await/promise feature). I create a job (LongJobThatUpdatesThePassedEntity) that modifies my entity

public static void myLongPut(@required Long id, String someData) {
       MyJpaModel myJpaModel = MyJpaModel.findById(id);

       //straightforward modifications
       updateMyJpaModel(someData);
       myJpaModel.save(); 

       //long processing modifications to entity, involving WS calls
       Promise<String> delayedResult = new LongJobThatUpdatesThePassedEntity(id).now();

       await(delayedResult);

       render(myJpaModel.refresh());

    } 

How are the DB transactions managed?

is there a commit before the job's call?

the job has it's own DB transaction?

if there is an issue in the LongJobThatUpdatesThePassedEntity that rollsback, the modifications done in updateMyJpaModel are persisted?

can I do render(myJpaModel.refresh()) at the end? will it contain the straighforward modifications and the long ones?

thank's


Solution

  • I can answer most of your question for Play 1.4.3, which is the version I'm currently using. I don't expect that much has changed since Play 1.2.

    How are the DB transactions managed?

    Play! handles the transactions for jobs and controller actions using an "invocation", which is a Play-specific concept. In short, for any invocation, each plugin gets a chance to do some setup and cleanup before and after the invoked method runs. For database access, the JPAPlugin.withinFilter method starts and closes the transaction using the JPA class's helper methods.

    is there a commit before the job's call?

    When you call await(Future<T>), it has the effect of closing the current transaction and starting a new one. The specific mechanism is that it throws a "Suspend" exception, which bubbles up to PlayHandler$NettyInvocation and causes the afterInvocation callbacks to be called. This causes JPAPlugin.afterInvocation to call JPA.closeTx() which either commits or rollsback the transaction, as appropriate.

    When the Job exits and the await() continuation is resumed. This is also handled as an invocation, so the transaction is started in the same way as before, using JPAPlugin.withinFilter(). However, unlike before, the controller action is not the target of the invocation, but instead ActionInvoker.invoke() calls invokeWithContinuation, which restores the saved continuation state and resumes execution by returning from await().

    JPA.withTransaction looks like it has some special logic to retain the same entity manager across the continuation suspend/resume. I think without this, you wouldn't be able to call refresh().

    In your code, I think there's a race condition between when await() closes the transaction and the Job starts its transaction. That is, it's possible that the Job's transaction begins before the controller commits the "before await" transaction. To avoid this, you can explicitly call JPA.closeTx() before calling Job.now().

    Based on code inspection, it looks like the way Play! is implemented, it so happens that the Job will exit and the Job's transaction will be closed before the "after await()" transaction is opened. I don't know if any documentation that says this is an intended part of the await() contract, so if this is essential for your appliaction, you can avoid using undocumented behavior by committing the transaction just before your Job.doJobWithResult() method returns.

    the job has it's own DB transaction?

    Yes, unless its annotated to not have a transaction.

    if there is an issue in the LongJobThatUpdatesThePassedEntity that rollsback, the modifications done in updateMyJpaModel are persisted?

    Based on the explanation above, each of the three transactions are independent. If one is rolled back, I don't see how it would affect the others.