Tuesday, April 23, 2013

Activiti Overall Exception Handling

In our framework we use Activiti to handle workflows.

Because of a deadlock problem I need to change a certain callActivity to activiti:async="true".
This leads to another problem, since the process runs asynchronous the user does not get notified of any exception in case something went wrong.

Synch
  • user starts process or claims task, using a service call
  • serviceTask or callActivity starts synchronous
  • serviceTask or callActivity completes or fails
  • service call returns
Asynch
  • user starts process or claims task, using a service call
  • serviceTask or callActivity starts asynchronous
  • service call returns
  • serviceTask or callActivity completes or fails
After the service call returns the user goes on working, e.g. closing workflow dialog and opening something else.
Now if the task failed, the exception is logged on the server but the user thinks everything is fine.

The solution to catch such exceptions is the org.activiti.engine.impl.jobexecutor.FailedJobCommandFactory, you can declare your own command which is executed in case the task fails.

Spring Context

<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
 <property name="dataSource" ref="dataSource" />
 <property name="transactionManager" ref="transactionManager" />
 <property name="databaseSchemaUpdate" value="true" />
 <property name="jobExecutorActivate" value="true" />
 <property name="history" value="full" />
 <property name="failedJobCommandFactory">
  <bean class="com.softmodeler.workflow.command.SoftmodelerFailedJobCommandFactory" />
 </property>
</bean>

Command Factory

/*******************************************************************************
 * $URL: $
 *
 * Copyright (c) 2007 henzler informatik gmbh, CH-4106 Therwil
 *******************************************************************************/
package com.softmodeler.workflow.command;

import org.activiti.engine.impl.interceptor.Command;
import org.activiti.engine.impl.jobexecutor.FailedJobCommandFactory;

/**
 * @author created by Author: fdo, last update by $Author: $
 * @version $Revision: $, $Date: $
 */
public class SoftmodelerFailedJobCommandFactory implements FailedJobCommandFactory {

 @Override
 public Command<Object> getCommand(String jobId, Throwable exception) {
  return new SoftmodelerFailedJobCommand(jobId, exception);
 }

}

org.activiti.engine.impl.cmd.DecrementJobRetriesCmd is the default command declared, a normal job can have x retries (default 3).
This command reduces the retries by one.
Here is a little example, notice "job.setRetries(0);". Since I'm sending an error email I don't want the job to execute again, otherwise I would get three mails instead of only one.

Command

/*******************************************************************************
 * $URL: $
 *
 * Copyright (c) 2007 henzler informatik gmbh, CH-4106 Therwil
 *******************************************************************************/
package com.softmodeler.workflow.command;

import java.io.PrintWriter;
import java.io.StringWriter;

import org.activiti.engine.impl.cfg.TransactionContext;
import org.activiti.engine.impl.cfg.TransactionState;
import org.activiti.engine.impl.context.Context;
import org.activiti.engine.impl.interceptor.Command;
import org.activiti.engine.impl.interceptor.CommandContext;
import org.activiti.engine.impl.jobexecutor.JobExecutor;
import org.activiti.engine.impl.jobexecutor.MessageAddedNotification;
import org.activiti.engine.impl.persistence.entity.JobEntity;

/**
 * @author created by Author: fdo, last update by $Author: $
 * @version $Revision: $, $Date: $
 */
public class SoftmodelerFailedJobCommand implements Command<Object> {

 private String jobId;
 private Throwable exception;

 /**
  * constructor
  * 
  * @param jobId
  * @param exception
  */
 public SoftmodelerFailedJobCommand(String jobId, Throwable exception) {
  this.jobId = jobId;
  this.exception = exception;
 }

 @Override
 public Object execute(CommandContext commandContext) {
  try {
   JobEntity job = Context.getCommandContext().getJobManager().findJobById(jobId);
   job.setRetries(0);
   job.setLockOwner(null);
   job.setLockExpirationTime(null);

   if (exception != null) {
    job.setExceptionMessage(exception.getMessage());
    job.setExceptionStacktrace(getExceptionStacktrace());

    // Send mail to process initiator here
   }

   JobExecutor jobExecutor = Context.getProcessEngineConfiguration().getJobExecutor();
   MessageAddedNotification messageAddedNotification = new MessageAddedNotification(jobExecutor);
   TransactionContext transactionContext = commandContext.getTransactionContext();
   transactionContext.addTransactionListener(TransactionState.COMMITTED, messageAddedNotification);

   return null;
  } catch (Throwable e) {
   throw new RuntimeException(e);
  }
 }

 private String getExceptionStacktrace() {
  StringWriter stringWriter = new StringWriter();
  exception.printStackTrace(new PrintWriter(stringWriter));
  return stringWriter.toString();
 }
}