Monday, October 14, 2013

Check for postChanges without commit or rollback in ADF BC

It is not uncommon for an ADF application to invoke DBTransactionImpl::postChanges() to perform database DML without invoking commit (or rollback). This is typically done in environments where we also want to invoke PL/SQL logic in the database that already needs to see these pending changes.

The developer should make sure a commit or rollback is executed in the same JSF request. If this is not done, the database session has pending changes and as we know there is no guarantee a subsequent request by the same user will get the same Application Module and JDBC Connection. It is considered bad practice to have these pending changes survive a single request and today we had a situation where this accidentally happened with all sorts of nasty side-effects. One of the things that happens is that the pending changes also could mean long-lived database locks. We added some code to our ADF BC base classes to detect this and throw an exception so we at least know what is going on and to alert a developer he/she is implementing this bad practice.

First thing we need is to configure our application module to use a custom database transaction class. This is described in the ADF documentation. First we need to create our own DatabaseTransactionFactory so we can use our own subclass of DBTransactionImpl2:
import oracle.jbo.server.DBTransactionImpl2;
import oracle.jbo.server.DatabaseTransactionFactory;

public class MyDatabaseTransactionFactory
  extends DatabaseTransactionFactory
{
  @Override
  public DBTransactionImpl2 create()
  {
    return new MyDBTransactionImpl2();
  }
}

Then we need to make sure this factory class is used instead of the default by setting the TransactionFactory property of the ADF Application Module configuration to the full class name of our own transaction factory:
Set TransactionFactory property of AM Configuration

Next, we can create our own subclass of DBTransactionImpl2 that keeps track of pending postChanges. It sets a flag when postChanges is executed and clears this flag when performing a commit or rollback. We are going to check this flag later on when releasing an Application Module at the end of a request:
import oracle.jbo.server.DBTransactionImpl2;
import oracle.jbo.server.TransactionEvent;

public class MyDBTransactionImpl2
  extends DBTransactionImpl2
{
  private boolean postedChanges = false;

  @Override
  protected void postChanges(TransactionEvent te)
  {
    super.postChanges(te);
    setPostedChanges(true);
  }

  @Override
  protected void doCommit()
  {
    setPostedChanges(false);
    super.doCommit();
  }

  @Override
  protected void doRollback()
  {
    setPostedChanges(false);
    super.doRollback();
  }

  private void setPostedChanges(boolean postedChanges)
  {
    this.postedChanges = postedChanges;
  }

  public boolean isPostedChanges()
  {
    return postedChanges;
  }

}

Final step is to check for pending postChanges at the end of each request. We can do this from our Application Module. This would typically be code you add to your own ADF BC base classes:
import oracle.jbo.ApplicationPoolSvcMsgContext;
import oracle.jbo.JboException;
import oracle.jbo.server.ApplicationModuleImpl;
import oracle.jbo.server.DBTransaction;

public class MyBaseAppModuleImpl
  extends ApplicationModuleImpl
{
  /**
   * This is invoked by the Application Module Pool whenever an 
   * application module is being used, released, removed or recycled
   * from the pool.
   */
  @Override
  public ApplicationPoolSvcMsgContext doPoolMessage(ApplicationPoolSvcMsgContext ctx)
  {
    if (ctx.getMessageType() == 
        ApplicationPoolSvcMsgContext.MESSAGE_TYPE_RELEASING)
    {
      DBTransaction txn = getDBTransaction();
      if (txn instanceof MyDBTransactionImpl2 && 
          ((MyDBTransactionImpl2) txn).isPostedChanges())
      {
        // throwing exception will mark AM as dead and close tainted
        // DBTransaction and JDBC connection
        throw new JboException("Application Module released to the pool with pending posted changes");
      }
    }
    return super.doPoolMessage(ctx);
  }

}

ApplicationModule::doPoolMessage is invoked whenever the pool uses, releases, removes or recycles an application module instance. This can be seen by inspecting ApplicationPoolSvcMsgContext::getMessageType() and comparing to any of the MESSAGE_* constants in ApplicationPoolSvcMsgContext. We check for uncommitted postChanges whenever the application module instance is released to the pool (aka at the end of each request). If these exist we simply throw a JboException. This will not be visible to the end-user as the JSF response has already been completed by then. It will show up in the log for administrators and developers. Because we are throwing an exception during pool management the AM instance will be destroyed together with its JDBC connection. This is actually good as we don't want the AM instance with pending changes, and potential database locks, to linger around.

Update 2 jan: We changed the code to no longer throw an exception thus destroying the applicationModuke, but only log a warning message. Due to an ADF bug we have quite a few AM instances with posted changes at the end of their request. When they get destroyed all ViewObjects loose their where clause and other (or all) records are fetched on the next page request. So until that bug is we fixed we reverted to only logging a warning.