Sunday, April 06, 2008

Workflow Reloaded : Moving to OSWorkflow

I have been trying to model a workflow for one of my applications. I started with Bossa, but after getting stuck using parts of its API, I was well on my way to designing my own workflow engine using Bossa's Petri Net abstraction, until a colleague pointed out that I was trying to re-invent something for which solid, mature implementations already existed in the open-source world, and that, down the road, other developers would curse me for having stuck them with having to maintain this component.

This week, I describe my experience implementing a solution to my workflow problem using OSWorkflow, a popular and mature workflow engine developed by the OpenSymphony group. Actually, I was quite pleasantly surprised to see Hani Suleiman's name as the author of some of the classes. For those of you who don't know, he is also the author of the Bileblog. Although he doesn't write it anymore, its worth a read simply because of his ability to call bullshit on any number of things, and his ability to call a spade a...er...several very unmentionable things. I don't agree with a lot of the stuff he said, but it was a very entertaining (and sometimes informative) read nevertheless. Anyway, like I said, it was nice to see his name on the @author tags.

My initial take on OSWorkflow is that it is less "pure" in its abstraction, but probably closer to the way people think of a workflow in the real world. I show my workflow redrawn the OSWorkflow way below. Notice how similar it is to the picture of the back-of-the-envelope version of the workflow.

However, in trying to make OSWorkflow "user-friendly", the concept of Step (Place in Bossa) has been overloaded to include Action (Transition in Bossa). Because of this, there are extra constructs for Split and Join (indicated above by pink and blue), Initial Action to kick off a workflow, and Pre- and Post-Functions to "attach" to a Step (instead of being the input and output Transition to a Place as in Bossa). What's more, Splits have to be modeled as real Steps, while Joins don't. So for me there was some amount of unlearning to do. Another problem is that the documentation is rather sparse and a lot of what I did was by trial and error.

Also, my needs were slightly different from the standard usage. The workflow I was trying to build is a network of batch processes, which have dependencies on each other. The batch jobs will be run using JMS (more on that probably next week), so the application needs to be event-driven. That is, each of the Actions would call back the main Workflow Runner once it is done, so the runner can move the workflow forward.

I am using Spring for the rest of the application, so I planned on using Spring's event handling framework for this. OSWorkflow has built-in integration with Spring, which I used, but that does not expose the actual post-functions as Spring objects. Fortunately, I was able to find a solution in this springmodules page, which works even without having the osworkflow springmodules module in your classpath.

Configuration

Configuration is using Spring and a couple of XML files that are used to define the workflow to OSWorkflow. The Spring applicationContext.xml file is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?xml version="1.0" encoding="UTF-8"?>
<beans ...>

  <bean id="workflowStore" 
    class="com.opensymphony.workflow.spi.memory.MemoryWorkflowStore"/>
  
  <bean id="workflowFactory" 
      class="com.opensymphony.workflow.spi.hibernate.SpringWorkflowFactory" 
      init-method="init">
    <property name="resource" value="workflow-defs.xml"/>
    <property name="reload" value="true"/>    
  </bean>

  <bean id="workflowConfiguration" 
      class="com.opensymphony.workflow.config.SpringConfiguration">
    <property name="factory" ref="workflowFactory"/>
    <property name="store" ref="workflowStore"/>
  </bean>
  
  <bean id="workflowTypeResolver" 
      class="com.opensymphony.workflow.util.SpringTypeResolver">
    <property name="functions">
      <map>
        <entry key="mockAction"><ref bean="mockAction"/></entry>
      </map>
    </property>
  </bean>

  <bean id="workflow" 
      class="com.opensymphony.workflow.basic.BasicWorkflow" scope="prototype">
    <constructor-arg><value>testuser</value></constructor-arg>
    <property name="configuration" ref="workflowConfiguration"/>
    <property name="resolver" ref="workflowTypeResolver"/>
  </bean>
    
  <bean id="mockAction" class="com.mycompany.myapp.workflow.MockAction" scope="prototype"/>
  
  <bean id="workflowRunner" 
      class="com.mycompany.myapp.workflow.WorkflowRunner" scope="prototype">
    <property name="workflow" ref="workflow"/>
  </bean>
</beans>

Here all except the last two beans are needed by OSWorkflow to define a Store and Factory, which in turn defines the configuration for the workflow. The workflowTypeResolver specifies that certain classes are going to be recognized as Spring functions by the Workflow. The mockAction is our Action class which we use for tests. In the real application, there will be specialized Action classes that will be injected into the appropriate workflow Steps.

The WorkflowFactory is built using the osworkflow file, workflow-defs.xml, which include a list of other workflow files, one for each workflow defined in the system. Both these files need to be in the application classpath.

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<workflows>
  <workflow name="workflow-def-1" type="resource" location="workflow-def-1.xml"/>
</workflows>
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE workflow PUBLIC 
  "-//OpenSymphony Group//DTD OSWorkflow 2.8//EN"
  "http://www.opensymphony.com/osworkflow/workflow_2_8.dtd">
<workflow>
  <initial-actions>
    <action id="0" name="start">
      <pre-functions>
        <function type="class">
          <arg name="class.name">com.opensymphony.workflow.util.Caller</arg>
        </function>
        <function type="spring">
          <arg name="bean.name">mockAction</arg>
          <arg name="action.name">t0</arg>
        </function>
      </pre-functions>
      <results>
        <unconditional-result old-status="Finished" status="Queued" 
          owner="${caller}" step="1"/>
      </results>
    </action>
  </initial-actions>
  <steps>
    <step id="1" name="p1">
      <actions>
        <action id="1" name="t1">
          <results>
            <unconditional-result old-status="Finished" status="Queued" 
              owner="${caller}" step="26"/>
          </results>
          <post-functions>
            <function type="spring">
              <arg name="bean.name">mockAction</arg>
              <arg name="action.name">t1</arg>
            </function>
          </post-functions>
        </action>
      </actions>
    </step>
    <step id="26" name="s26">
      <actions>
        <action id="26" name="split26">
          <results>
            <unconditional-result old-status="Finished" owner="${caller}" split="26"/>
          </results>
          <post-functions>
            <function type="spring">
              <arg name="bean.name">mockAction</arg>
              <arg name="action.name">split26</arg>
            </function>
          </post-functions>
        </action>
      </actions>
    </step>
    <step id="2" name="p2">
      <actions>
        <action id="2" name="t2">
          <results>
            <unconditional-result old-status="Finished" status="Queued" 
              owner="${caller}" step="34"/>
          </results>
          <post-functions>
            <function type="spring">
              <arg name="bean.name">mockAction</arg>
              <arg name="action.name">t2</arg>
            </function>
          </post-functions>
        </action>
      </actions>
    </step>
    <step id="34" name="s34">
      <actions>
        <action id="34" name="split34">
          <results>
            <unconditional-result old-status="Finished" owner="${caller}" split="34"/>
          </results>
          <post-functions>
            <function type="spring">
              <arg name="bean.name">mockAction</arg>
              <arg name="action.name">split34</arg>
            </function>
          </post-functions>
        </action>
      </actions>
    </step>
    <step id="3" name="p3">
      <actions>
        <action id="3" name="t3">
          <results>
            <unconditional-result old-status="Finished" owner="${caller}" join="43"/>
          </results>
          <post-functions>
            <function type="spring">
              <arg name="bean.name">mockAction</arg>
              <arg name="action.name">t3</arg>
            </function>
          </post-functions>
        </action>
      </actions>
    </step>
    <step id="4" name="p4">
      <actions>
        <action id="4" name="t4">
          <results>
            <unconditional-result old-status="Finished" owner="${caller}" join="43"/>
          </results>
          <post-functions>
            <function type="spring">
              <arg name="bean.name">mockAction</arg>
              <arg name="action.name">t4</arg>
            </function>
          </post-functions>
        </action>
      </actions>
    </step>
    <step id="5" name="p5">
      <actions>
        <action id="5" name="t5">
          <results>
            <unconditional-result old-status="Finished" owner="${caller}" join="75"/>
          </results>
          <post-functions>
            <function type="spring">
              <arg name="bean.name">mockAction</arg>
              <arg name="action.name">t5</arg>
            </function>
          </post-functions>
        </action>
      </actions>
    </step>
    <step id="6" name="p6">
      <actions>
        <action id="6" name="t6">
          <results>
            <unconditional-result old-status="Finished" status="Queued" 
              owner="${caller}" step="7"/>
          </results>
          <post-functions>
            <function type="spring">
              <arg name="bean.name">mockAction</arg>
              <arg name="action.name">t6</arg>
            </function>
          </post-functions>
        </action>
      </actions>
    </step>
    <step id="7" name="p7">
      <actions>
        <action id="7" name="t7">
          <results>
            <unconditional-result old-status="Finished" owner="${caller}" join="75"/>
          </results>
          <post-functions>
            <function type="spring">
              <arg name="bean.name">mockAction</arg>
              <arg name="action.name">t7</arg>
            </function>
          </post-functions>
        </action>
      </actions>
    </step>
    <step id="8" name="p8">
      <actions>
        <action id="8" name="t8">
          <results>
            <unconditional-result old-status="Finished" status="Queued" 
              owner="${caller}" step="9"/>
          </results>
          <post-functions>
            <function type="spring">
              <arg name="bean.name">mockAction</arg>
              <arg name="action.name">t8</arg>
            </function>
          </post-functions>
        </action>
      </actions>
    </step>
    <step id="9" name="stop">
      <actions>
        <action id="9" name="t9" finish="true">
          <results>
            <unconditional-result old-status="Finished" 
              status="Complete" owner="${caller}"/>
          </results>
        </action>
      </actions>
    </step>
  </steps>
  <splits>
    <split id="26">
      <unconditional-result old-status="Finished" status="Queued" 
        owner="${caller}" step="2"/>
      <unconditional-result old-status="Finished" status="Queued" 
        owner="${caller}" step="6"/>
    </split>
    <split id="34">
      <unconditional-result old-status="Finished" status="Queued" 
        owner="${caller}" step="3"/>
      <unconditional-result old-status="Finished" status="Queued" 
        owner="${caller}" step="4"/>
    </split>
  </splits>
  <joins>
    <join id="43">
      <conditions type="AND">
        <condition type="beanshell">
          <arg name="script"><![CDATA[
            "Finished".equals(jn.getStep(3).getStatus()) &&
            "Finished".equals(jn.getStep(4).getStatus())
          ]]>
          </arg>
        </condition>
      </conditions>
      <unconditional-result old-status="Finished" status="Queued" 
        owner="${caller}" step="5"/>
    </join>
    <join id="75">
      <conditions type="AND">
        <condition type="beanshell">
          <arg name="script"><![CDATA[
          "Finished".equals(jn.getStep(5).getStatus()) &&
          "Finished".equals(jn.getStep(7).getStatus())
          ]]>
          </arg>
        </condition>
      </conditions>
      <unconditional-result old-status="Finished" status="Queued" 
        owner="${caller}" step="8"/>
    </join>
  </joins>
</workflow>

The file above is the XML version of the workflow diagram above. It has approximately eight real steps, a start and stop step (which are pseudo steps in the sense that they don't do anything, but need to be modeled as regular steps here), two splits (each of which need to be modeled as steps) and two joins.

The reason the actual mockAction beans have been built as post-actions is because of the event based nature of the actions. Building them as pre-actions will result in an infinite loop because the workflow will never leave the first step. Since post-actions are called after the step is left, this is an ideal position to do this.

Event Subscriber and Publisher

The Event Subscriber is the WorkflowRunner class, which listens on events generated by the Action classes. The event contains the workflowId for the current Workflow instance, which is used to query the workflow to find the next available actions to execute. It implements Spring's ApplicationListener and exposes an onApplicationEvent which is triggered by events from the Actions. The code is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// WorkflowRunner.java
package com.mycompany.myapp.workflow;

import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;

import com.opensymphony.workflow.Workflow;
import com.opensymphony.workflow.WorkflowException;
import com.opensymphony.workflow.spi.Step;

public class WorkflowRunner implements ApplicationListener {

  private final Log log = LogFactory.getLog(getClass());
  
  private String workflowName;
  private Workflow workflow;
  private Map<String,Object> inputs;
  
  private Set<Integer> alreadyExecuted = new HashSet<Integer>();
  
  @Required
  public void setWorkflow(Workflow workflow) {
    this.workflow = workflow;
  }
  
  public void setWorkflowName(String workflowName) {
    this.workflowName = workflowName;
  }
  
  public void setInputs(Map<String,Object> inputs) {
    this.inputs = inputs;
  }
  
  public long init(int initialActionId) throws Exception {
    long workflowId = workflow.initialize(workflowName, initialActionId, inputs);
    return workflowId;
  }
  
  @SuppressWarnings({"unused","unchecked"})
  public void process(long workflowId) {
    List<Step> currentSteps = workflow.getCurrentSteps(workflowId);
    for (Step currentStep : currentSteps) {
      int[] availableActions = workflow.getAvailableActions(workflowId, inputs);
      for (int availableAction : availableActions) {
        if (alreadyExecuted.contains(availableAction)) {
          continue;
        }
        try {
          System.out.println("Executing action.id=" + availableAction);
          workflow.doAction(workflowId, availableAction, inputs);
          alreadyExecuted.add(availableAction);
        } catch (WorkflowException e) {
          log.error("Exception in (workflow,action.id)=(" + workflowName + "," + 
            availableAction + "). Workflow stopped", e);
        }
      }
    }
  }
  
  public void onApplicationEvent(ApplicationEvent event) {
    if (event.getSource() instanceof Long) {
      process((Long) event.getSource());
    }
  }
}

The events are published into the ApplicationContext from our Action classes. The MockAction is only an example which prints the name of the action being executed. It implements FunctionProvider (so it can be called from within OSWorkflow as a function) and ApplicationContextAware (so it can publish events into Spring's ApplicationContext).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// MockAction.java
package com.mycompany.myapp.workflow;

import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;

import com.opensymphony.module.propertyset.PropertySet;
import com.opensymphony.workflow.FunctionProvider;
import com.opensymphony.workflow.WorkflowException;
import com.opensymphony.workflow.spi.WorkflowEntry;

public class MockAction implements FunctionProvider, ApplicationContextAware {

  private final Log log = LogFactory.getLog(getClass());
  
  private ApplicationContext applicationContext;

  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    this.applicationContext = applicationContext;
  }

  /**
   * @see com.opensymphony.workflow.FunctionProvider#execute(
   *   java.util.Map, java.util.Map, com.opensymphony.module.propertyset.PropertySet)
   */
  @SuppressWarnings({"serial","unchecked"})
  public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException {
    String actionName = (String) args.get("action.name");
    WorkflowEntry workflowEntry = (WorkflowEntry) transientVars.get("entry");
    long workflowId = workflowEntry.getId();
    applicationContext.publishEvent(new ApplicationEvent(workflowId) {});
  }
}

Example Client Code

Finally, here is how I plan to call the workflow. The workflow is kickstarted by a user request, however, any further movement in the workflow is done by the events, which call the WorkflowRunner.process method internally. Here is the JUnit test I used to test this out.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// WorkflowRunnerTest.java
package com.mycompany.myapp.workflow;

import java.util.HashMap;
import java.util.Map;

import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.opensymphony.workflow.Workflow;

public class WorkflowRunnerTest {

  private WorkflowRunner runner;
  private Map<String,Object> inputs = new HashMap<String,Object>();
  private long workflowId;
  
  @Before
  public void setUp() throws Exception {
    ApplicationContext context = new ClassPathXmlApplicationContext(new String[] {
      "classpath:applicationContext.xml"
    }); 
    runner = (WorkflowRunner) context.getBean("workflowRunner");
    runner.setWorkflowName("workflow-def-1");
    runner.setInputs(inputs);
    workflowId = runner.init(0);
  }

  @Test
  public void testRunner() throws Exception {
    runner.process(workflowId);
  }
}

The output looks like this, which seems reasonable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Executing action.id=1
Executing action.id=26
Executing action.id=2
Executing action.id=6
Executing action.id=34
Executing action.id=7
Executing action.id=3
Executing action.id=4
Executing action.id=5
Executing action.id=8
Executing action.id=9

Conclusion

OSWorkflow seems to have quite a steep learning curve, but it is quite powerful once you understand how it all works. I don't know everything about it (not even close), and I am probably only going to explore more unless I have to. Like any other workflow engine, the hardest work is to design your workflow. During my searches for things OSWorkflow related, I did come across OSWorkflow: A guide for Java developers and architects, a book by Diego Naya, who also seems to be quite active on the OSWorkflow mailing list. I haven't read it, but it may be worth buying at some point in the future. OSWorkflow also comes with a GUI designer, which I did try in desperation at one point, but I could not do much with it, so I cannot comment on how good it is.