Eric D. Schabell: A custom jBPM exception framework for jBPM 3.x

Monday, December 7, 2009

A custom jBPM exception framework for jBPM 3.x

I have spent some time over the last few months (slowly, only one day a week) working on a jBPM exception handling framework. I wanted to present the use case and leave the exact details over to you as an exercise in Java coding.

The idea is that jBPM (here using 3.x supported versions from the Red Hat JBoss Customer Support Portal (CSP) provides the standard jBPM exception and that I want to also have the choice to apply a dynamically generated transition from the point of an exception that takes me to a custom exception flow. This custom exception flow is nothing more than a single decision that, based on the type of exception I have passed, will choose a path to one of the following:

  • task node (human task to evaluate the exception)
  • state node (retry using a timer to wait a bit before trying again)
  • any eventual node desired to process the exception

There are some fairly obvious benefits:

  • no longer just one handling for all your jBPM process exceptions
  • very extensible, just add new nodes into the exception handling process flow to provide new functionality
  • placing exception handling into an apart flow means you can deploy this once and use it with all of your deployed processes
  • provides for decisions, states and node exception handling. Transitions should not be doing anything exciting within my processes, if yours needs to you can extend this concept to these too.

The process flow diagram provided in this post shows the simple testing framework I used to enable this. All code snippets shown are simplified for this post. There is a global attribute that is checked for using our custom exception framework called 'useCustomExceptions'.

Decisions

For a decision node we need to implement the DecisionHandler from jBPM, so I do that in an AbstractDecisionHanlder that implements the decide() method and puts in an extra handleException() method as follows:

public String decide(ExecutionContext ctx) throws Exception {

  try {
         // pick transition from the decision handler.
         transitionName = chooseTransition(ctx); 
         getLogger().debug("Choosing transition " + transitionName);
  } catch (Exception ex) {
         if (useCustomExceptions) {
             getLogger().error("Handler threw exception.", ex);
             ctx.getNode().raiseException(ex, ctx);
         } else {
             getLogger().error("Handler threw exception.", ex);
             ctx.setException(ex);
             // use custom method to create dynamic transition.
             dynamicTransition = handleException(ctx);
         }
  }

  return transitionName;
}


// This method is provided in comment steps only, the rest
// is an exercise for you (I can not publish this code).
private String handleException(ExecutionContext ctx) {

  // Save the current node in the context to make a retry possible.
                
  // Make sure the transition to the Exception handling Node can be made.
                
  // Using handling node name for the transition name.
                
  return name_of_transition_leading_to_exception_handling;
}


Node

For a node we need to implement the ActionHandler from jBPM, so I do that in an AbstractActionHanlder that implements the execute() method. Furthermore I need to come up with a solution for the following issue that is unique to the jBPM 3.x node. When you enter a node, you have an on-node-enter event and when finished it will trigger the on-node-leave event which takes the transition that is assigned during the event on-node-enter event. This means using the Node.leave(some-transition) method will not work.

I got the reaction when I used this Node.leave(take-dynamic-transition-to-exception-handling) it worked fine until it entered the human-task wait state. At this point the originating node then continued onwards, taking the default transition (that was in the TransitionsList for the node) and merrily finishing the process flow. Even stranger, this was done on the same process id that was also assigned to some human task!

My solution is to provide a mechanism to do the following:

  • create the dynamic transition to custom exception handling for the node
  • add the dynamic transition to the TransistionsList for this node
  • put a copy of the TransistionList (without the new dynamic transition) into the jBPM Context
  • remove all transition entries from the TransistionsList except for the new dynamic one
  • use Node.leave() to follow the new dynamic transition
  • deal with exception handling
  • upon returning to the node after exception handling, pick up the transitions from the jBPM Context
  • fill the TransitionsList with those found in the jBPM Context
  • use Node.leave() to use the default

Some code to give you an idea with migrateTransitionsInNodeToContext which shows the transitions being migrated into the jBPM Context. I have left out the recovery of the transitions from the context, as it is trivial to reverse the process. To give you a bit of a hint, when you are in the exception handling process and decide it is time to go back to your originating node, you create a dynamic transition and fill the originating nodes TransitionsList from the jBPM Context. 

Here are some code snippets:

public final void execute(ExecutionContext ctx) throws Exception {

  try {
        if (ctx.getNode() instanceof State) {
           // process a state.
        } else {
   
           try {
  
              // do something in node.
           } catch (Exception ex) {
    
              if (useCustomExceptions) {
                   getLogger().error("Handler threw exception.", ex);
                   ctx.getNode().raiseException(ex, ctx);
              } else {
                   getLogger().error("Handler threw exception.", ex);
                   transitionName = handleException(ctx);

                   migrateTransitionsInNodeToContext(ctx);

                   // Move the process along.
                   if (!StringUtils.isBlank(transitionName) &&
ctx.getNode().hasLeavingTransition(transitionName)) {
                      ctx.getNode().leave(ctx, transitionName);
                   } else {
                       // Use the default transition.
                       ctx.getNode().leave(ctx);
                   }
              }
           }
        }
  } catch (Exception ex) {
      // do something
  }
}

/**
 * Use given context to migrate all transitions in the nodes list that
 * do not match the dynamic transition created for an exception.
 */
private void migrateTransitionsInNodeToContext(ExecutionContext ctx) {
  List existingTransitionsList = ctx.getNode().getLeavingTransitionsList();
  List removedExceptionTransitionList = new ArrayList();

  int i = 0;
  for (Transition transition : existingTransitionsList) {
    i++;
    if (transition.equals(transitionName)) {
      getLogger().debug("List item #" + i 
          + " is our dynamic tranition: " 
          + transition.toString());
    } else {
       getLogger().debug("List item #" + i + " tranition: " 
               + transition.toString() + ", migrating to context.");
       removedExceptionTransitionList.add(transition);
       getLogger().debug("List item #" + i + " tranition: " 
           + transition.toString() + ", removing from node transition list.");
       ctx.getNode().removeLeavingTransition(transition);
    }
  }

  if (!existingTransitionsList.isEmpty()) {
      // place existing transitions into context variable.
      String ctxStoredTranstionsName = 
           ctx.getNode().getName() + "_stored_transitions_list";
      ctx.setVariable(ctxStoredTranstionsName, removedExceptionTransitionList);
  }
}


State

For a state we work analogue to the decision node but within the extended abstract class that implements the jBPM ActionHandler. This is left as an exercise to the reader.

1 comment:

  1. There is a newer version of this available, see:

    http://www.schabell.org/2010/04/jbpm-v32-custom-exception-framework.html

    ReplyDelete

Note: Only a member of this blog may post a comment.