workflow control patterns
The Workflow Patterns are a catalog of various building blocks for workflow execution.
Described here are ways to implement each of those patterns with ruote. Some of them are not directly realizable with ruote, approximations are proposed. This is a self-evaluation, for an authoritative voice, the workflow patterns website and its mailing list are here.
Each pattern is illustrated with a Ruby DSL implementation (or approximation). XML implementations are easily derivable from their Ruby counterparts. There is also a link to the original pattern explanation and its flash animation.
Participant expressions have been supplemented with an :activity or :task attribute to give a better feel about an hypothetical context for the application of the pattern.
- New Control Flow Patterns
- coming soon
Basic Control Flow Patterns
sequence
Chaining activities in sequence. Uses the sequence expression. The cursor expression might also be used.
sequence do participant :ref => 'alpha', :activity => 'write' participant :ref => 'bravo', :activity => 'fix typos' end
original pattern explanation | top
parallel split
The concurrence expression is the main tool for ‘parallel splits’.
concurrence do participant :ref => 'alpha', :activity => 'write introduction' participant :ref => 'bravo', :activity => 'write postface' end
original pattern explanation | top
synchronization
Synchronization is supported implicitely by the concurrence expression. Note that this expression can be tuned via its attributes for behaviours different than the vanilla “wait before all child expressions have replied” one.
original pattern explanation | top
exclusive choice
Exclusive ‘routing’ within the process : the flow will go one way or the other, but not both.
The if is usually in charge when implementing this pattern (the underscore ‘_’ prefixing the if prevents collision with the ‘if’ Ruby keyword).
_if '${f:decision} == accepted' do participant :ref => 'alpha', :activity => 'request further info' participant :ref => 'alpha', :activity => 'send refusal note' end
(this if example forwards the flow to the same ‘alpha’ participant, but with different activities).
One could rewrite this as :
sequence do participant 'alpha', :activity => 'request further info', :if => '${f:decision} == accepted' participant 'alpha', :activity => 'send refusal note', :if => '${f:decision} == refused' end
But this is not strictly equivalent
With a bit of imagination, exclusive choices may be found beyond ifs :
Ruote.process_definition :name => 'request processing' do sequence do # ... participant :ref => 'editor' # decision : accepted or refused subprocess :ref => 'request_${f:decision}' # ... end define 'request_accepted' do participant :ref => 'alpha', :activity => 'request further info' end define 'request_refused' do participant :ref => 'alpha', :activity => 'send refusal note' end end
The name of the subprocess is extrapolated at runtime and the flow is routed accordingly.
original pattern explanation | top
simple merge
A simple merge occur when two (or more) exclusive branch converge. As seen in exclusive choice this pattern is implicitely supported. It simply occurs when the ‘then’ or the ‘else’ clause of an ‘if’ terminates and the flow resumes.
original pattern explanation | top
Advanced Branching and Synchronization Patterns
multi choice
Combining the concurrence expression and :if attributes makes for an easy implementation of this pattern :
concurrence do participant 'regular service', :if => '${f:price} < 50' participant 'premium service', :if => '${f:price} > 40' participant 'extra service', :if => '${f:extra_ordered}' end
In this example, if the price is between 40 and 50, regular service and premium service are triggered, extra could be triggerd as well if the field ‘extra_ordered’ has the value ‘true’.
original pattern explanation | top
structured synchronizing merge
The multi choice pattern implementation implicitely support this ‘structured synchronizing merge’. The concurrence expression waits for all its children expression to reply before resuming the flow.
original pattern explanation | top
multi merge
The convergence of two or more branches into a single subsequent branch such that each enablement of an incoming branch results in the thread of control being passed to the subsequent branch.
The flash animation for this pattern is worth a look.
Basically, what comes after the multi-merge will be executed once for each incoming replies.
Certainly, ruote doesn’t support that out of the box, but there are approximations to it :
Ruote.process_definition :name => 'multi merge' do concurrence do sequence do team_a after end sequence do team_b after end sequence :if => '${f:more_capacity_needed}' do extra_team after end end define 'after' do participant 'supervisor', :activity => 'assess work' end end
Here, a subprocess (define ‘after’) is used after each concurrent branch. Before replying to its parent (the concurrence), each branch calls it. (team_a, team_b and extra_team could be participant or subprocess names).
Leveraging the listen expression is another option :
Ruote.process_definition :name => 'multi merge' do concurrence :count => 3, :remaining => :forget do team_a team_b team_extra :if => '${f:more_capacity_needed}' listen :to => /^team\_.+/, :upon => :reply do participant 'supervisor', :activity => 'assess work' end end end
With this process definition, each time a team participant replies, the participant ‘supervisor’ is sent a workitem. Please note that this works only with participants. In the above subprocess example, ‘team_a’ could point either to a participant, either to a subprocess.
original pattern explanation | top
structured discriminator
As soon as one of two concurrent branches replies, the flow resumes and the other branch is ‘forgotten’ (continues its execution without the parent process caring for its outcome).
sequence do concurrence :count => 1, :remaining => :forget do test_batch_a test_batch_b end final_test_batch end
As soon as an initial test batch is completed, the final test batch is triggered. The remaining test batch continues, unhindered (but forgotten).
The attributes :count and :remaining of the concurrence are used. Note that those attributes are understood by the concurrent_iterator expression as well :
sequence do concurrent_iterator :on => 'a, b, c, d', :count => 1, :remaining => :forget do subprocess :ref => 'test_batch_${v:i}' # batches a, b, c and d will get triggered end final_test_batch end
original pattern explanation | top
Structural Patterns
arbitrary cycles
The ability to represent cycles in a process model that have more than one entry or exit point.
Thanks to the cursor expression, the process represented as flash animation on the Workflow Patterns site may be implemented as :
cursor do participant 'a' participant 'b' participant 'c' jump :to => 'b', :if => '${f:back_to_b}' participant 'd' jump :to => 'c', :if => '${f:back_to_c}' participant 'e' end
The jump expression understand participant names, subprocess names and :tag names in its :to attribute, making it easy to specify jump points.
original pattern explanation | top
implicit termination
Replicating the flash animation of the pattern, we obtain :
sequence do participant 'alpha' concurrence do sequence do participant 'bravo' participant 'delta' end sequence do participant 'charly' participant 'echo' end end end
There is not much to say about this pattern. When there is no more work, the process ends.
original pattern explanation | top
Multiple Instance Patterns
multiple instances without synchronization
within a given process instance, multiple instances of an activity can be created. These instances are independent of each other and run concurrently. There is no requirement to synchronize them upon completion.
concurrent_iterator :times => 10 do activity :forget => true end
‘activity’ here could be a participant or a subprocess named ‘activity’. :forget is set to true (no requirement to synchronize). The process will resume immediately after this concurrent_iterator (since all its children are forgotten).
The flash animation could be translated to something like :
sequence do participant 'a' concurrent_iterator :times => '${f:count}' do participant 'b', :forget => true end participant 'c' end
The number of ‘b’ instances to fire is expected to be found in the workitem field named ‘count’ (value is perhaps chosen by participant ‘a’).
original pattern explanation | top
multiple instances with a priori design time knowledge
the required number of instances is known at design time. These instances are independent of each other and run concurrently. It is necessary to synchronize the activity instances at completion before any subsequent activities can be triggered.
The concurrence or the concurrent_iterator expressions can be used.
concurrence do participant 'alfred' participant 'bertha' participant 'charly' end
which can be rewritten as :
concurrent_iterator :on => 'alfred, bertha, charly', :to_field => 'p' do participant :ref => '${f:p}' end
original pattern explanation | top
multiple instances with a priori run time knowledge
Within a given process instance, multiple instances of an activity can be created. The required number of instances may depend on a number of runtime factors, including state data, resource availability and inter-process communications, but is known before the activity instances must be created. Once initiated, these instances are independent of each other and run concurrently. It is necessary to synchronize the instances at completion before any subsequent activities can be triggered.
It’s again a job for the concurrent_iterator expression.
concurrent_iterator :on => '${f:reviewer_list}', :to_field => 'p' do participant :ref => '${f:p}' end
Here, the list of participant that will ‘perform’ concurrently is help in a workitem field named ‘reviewer_list’ (a comma-separated list of participant names).
original pattern explanation | top
multiple instances without a priori run time knowledge
The number of activity is known at runtime and may change at any point during the execution of these activities.
The expression add_branches which works in conjunction with the concurrent_iterator expression is necessary for this pattern implementation.
concurrence :count => 1 do concurrent_iterator :on => '${f:reviewer_list}', :tag => 'review' do participant :ref => '${v:i}' end repeat do participant 'supervisor', :q => 'add more reviewers ?' add_branches '${f:new_supervisors}', :ref => 'review' end end
The review work is performed in the concurrent_iterator tagged ‘review’. Concurrently a participant supervisor is asked if more reviewers are needed. He has the opportunity to add more reviewers as long as the concurrent_iterator is not over. Since the wrapping concurrence has a :count of 1, as soon as the iterator is done, the concurrence will be over (the supervisor branch getting cancelled).
The add_branches expression knows it has to add the supervisor to the concurrent iterator via the :ref => ‘review’ declaration.
original pattern explanation | top
State-based Patterns
deferred choice
This pattern is difficult for ruote to implement. It requires some control over the state of the workitem as a task, and since ruote delegates task handling to participants…
A set of participants is presented with a task, as soon as one of them starts working on it, the other instances of the task are withdrawn from the other participants.
With a bit of help from the participant implementations, this could be a realization :
sequence do # presenting the task concurrence :count => 1 do participant 'a', :task => 'start work' participant 'b', :task => 'start work' end # performing the task participant '${a_or_b}', :task => 'perform work' end
As soon as ‘a’ or ‘b’ replies (concurrence :count => 1), it is presented with the task again. The other participant receives a cancel notification.
Of course, the workitem/task presented is not strictly equivalent to the task to be performed…
Perhaps the better way to implement this pattern is within the participant itself. The ruote engine could dispatch the task to participant ‘x’, a worklist handler managing access rights, escalation and so and so independently.
original pattern explanation | top
interleaved parallel routing
TODO
original pattern explanation | top
milestone
A task is only enabled when the process is in a specific state.
Here is a ruote attempt at implementing this pattern :
Ruote.define do concurrence do sequence do participant 'a' participant 'b', :tag => 'milestone' participant 'c' end concurrence :count => 1 do # :count => 1 means the concurrence terminates as soon as 1 branch # replies. The other branch is cancelled. sequence do listen :to => 'milestone', :upon => 'entering', :wfid => true # blocks until milestone is reached participant 'd' end listen :to => 'milestone', :upon => 'leaving', :wfid => true # blocks until milestone is left end end end
In this implementation, the participant ‘d’ receives a workitem as soon as the milestone is reached (participant ‘b’ receives a workitem). d’s task is over as soon as d terminates it or b terminates his task.
original pattern explanation | top
Cancellation Patterns
New Control Flow Patterns
coming soon.