SeamのjBPMコンポーネントを使うと、例外発生時にjBPMのデータがロールバックされない

JBoss SeamにはjBPMの利用を支援するコンポーネントが同梱されていますが、アプリケーション側で例外が発生したり、jBPM側(プロセス定義中の各種ハンドラなど)で例外が発生した際に、jBPMのデータがコミットされてしまう不具合があります。

この不具合のバグレポートが挙がっていますが、まだ解決されていないようです。

未解決のバグレポート
https://jira.jboss.org/browse/JBSEAM-4512

上記のバグレポートによればSeam 2.2.0.GAで発生するようですが、手元の2.1.2でも同様の問題が確認できました。

原因

この不具合の原因は2つあります。

  1. 例外発生後もjBPMのエンティティがHibernate Session上に残ってしまっている。
  2. JbpmContextのxxxForUpdateメソッドを使っており、Hibernate Session上だけでなく、JbpmContext.autoSaveProcessInstancesにもエンティティが残っている為、クローズ時に保存されてしまう。

SeamではHTTPリクエスト1件を複数のトランザクションで処理しますが、ビジネス層のトランザクション(Restore ViewフェーズからInvoke Applicationフェーズまで)後もjBPMのエンティティがHibernate Sessionに残っており、プレゼンテーション層のトランザクション(Render Responseフェーズ)でコミットされてしまいます。

また、xxxForUpdateメソッドを使用しているため、例外発生時にjBPMのHibernate Sessionをクリアしても、JbpmContextのautoSaveProcessInstancesにエンティティが残っており、jBPMデータが登録されてしまいます。

ちなみに、アプリケーション側のエンティティも永続コンテキストに残っているのに、なぜコミットされないのか疑問に思い調べたところ、Render Responseフェーズ直前でエンティティマネージャのFlushModeをMANUALに切り替えている為のようです(SeamPhaseListener#beforeRenderResponseの最後のあたり)。

回避策1. SeamのjBPM関連コンポーネントを使わない

SeamのjBPM関連コンポーネントは使わず、アプリケーションロジックから直接jBPM APIを叩くことで、この問題を回避…、というか無視できます。

jBPMの永続化には、Seam管理のHibernate Sessionを使用します。

Seam管理のHibernate Session - components.xml
...
<persistence:hibernate-session-factory name="hibernateSessionFactory"/>
<persistence:managed-hibernate-session name="hibernateSession"
                                auto-create="true"
                  session-factory-jndi-name="java:/hibernateSessionFactory"/>
...

Hibernate SessionをJbpmContextインスタンスに注入することで、アプリケーションと同じトランザクション境界で処理できるようになります。

...
public void open() {
  Session hibernateSession = (Session) Component.getInstance("hibernateSession");

  JbpmContext jbpmContext = JbpmConfiguration.getInstance().createJbpmContext();
  jbpmContext.setSession(hibernateSession);

  return jbpmContext;
}
...

回避策2. SeamのjBPM関連コンポーネントを置き換える

SeamのjBPM関連コンポーネントを使う場合は、上記の不具合を回避するよう修正したコンポーネントを、Seam同梱のコンポーネントと置き換えます。

public class MyManagedJbpmContext extends ManagedJbpmContext {
  @Override
  public void beforeCompletion() {
    if (BusinessProcess.instance().hasActiveProcess()) {
      //JbpmContextのxxxForUpdateメソッドを使わず、明示的にエンティティを保存する。
      instance().save(ProcessInstance.instance());
    }
    super.beforeCompletion();
  }

  @Override
  public void afterCompletion(int status) {
    if (status == Status.STATUS_ROLLEDBACK) {
      instance().getSession().clear();
    }

    super.afterCompletion(status);
  }
}

ManagedJbpmContextのほかに、JbpmContextのxxxForUpdateメソッドを叩いているコンポーネント(BusinessProcess、PooledTask、ProcessInstance、TaskInstance)をオーバーライドします。

例) BusinessProcess
public class MyBusinessProcess extends BusinessProcess {
  @Override
  public void createProcess(String processDefinitionName, boolean shouldSignalProcess) {
    createProcess(processDefinitionName, null, shouldSignalProcess);
  }

  @Override
  public void createProcess(String processDefinitionName, String businessKey) {
    createProcess(processDefinitionName, businessKey, true);
  }

  protected void createProcess(String processDefinitionName, String businessKey, boolean shouldSignalProcess) {
    //xxxForUpdateメソッドは使わない。
    ProcessInstance process = ManagedJbpmContext.instance().newProcessInstance(processDefinitionName);
    if (!Strings.isEmpty(businessKey)) process.setKey(businessKey);
    afterCreateProcess(processDefinitionName, process, shouldSignalProcess);
  }

  private void afterCreateProcess(String processDefinitionName, ProcessInstance process, boolean shouldSignalProcess) {
    /*
     * オリジナルのBusinessProcessからコピーする。
     */
    ...
  }
}

最後に、Seam同梱のコンポーネントを修正したコンポーネントで置き換えます。

components.xml
 ...
 <component
  class="com.natswell.jbpm.MyManagedJbpmContext"
  name="org.jboss.seam.bpm.jbpmContext" scope="event"/>
 <component class="com.natswell.jbpm.MyBusinessProcess"
  name="org.jboss.seam.bpm.businessProcess" scope="conversation"/>
 ...

コメントを残す

メールアドレスが公開されることはありません。