Usually, a process definition weaves the trajectory of workitems among participants.
<process-definition name="def0" revision="0.1">
<sequence>
<participant ref="accounting" />
<!-- send invoice -->
<participant ref="logistics" />
<!-- deliver items -->
</sequence>
</process-definition>
As seen, participants are invoked with the ‘participant’ expression but you can write :
<process-definition name="def0" revision="0.1">
<sequence>
<accounting />
<logistics />
</sequence>
</process-definition>
Provided that your participant names don’t conflict with XML.
In OpenWFEru you can define processes as Ruby classes :
class Def0 < OpenWFE::ProcessDefinition
sequence do
accounting
logistics
#
# sometimes you'll prefer the more explicit :
#
# participant :ref => "accounting"
# participant :ref => "logistics"
end
end
Participants are registered in the engine in this way :
require 'openwfe/engine/engine'
require 'openwfe/worklist/storeparticipants'
(...)
engine = Engine.new
hpAlice = HashParticipant.new
hpBob = HashParticipant.new
engine.register_participant(:alice, hpAlice)
engine.register_participant(:bob, hpBob)
(...)
To remove participants from the engine :
engine.unregister_participant(:alice)
engine.unregister_participant("bob")
Participants can be categorized in two flavours : push and pull. Push participants like for example a MailParticipant or the BlockParticipant react immediately upon receiving a workitem (sending an email, executing some code). Pull participants like store participants, simply keep the workitems for later consumption by external services.
Here are the participant implementations that ship with OpenWFEru :
Their descriptions :
ActiveParticipant(this participant is part of the “openwferu-extras” gem)
It’s too bad this participant comes first in this [alphabetical] list. It’s a bit advanced.
The active page is dedicated to the ActiveRecord items in OpenWFEru.
ActiveStoreParticipant(this participant is part of the “openwferu-extras” gem)
It’s a refinement of the ActiveParticipant. This is the participant used by ‘densha’ for its workitem stores.
AliasParticipantGiven a list of ‘user participant’ :
engine.register_participant :alice do |workitem|
workitem.whatever = "oh and by the way"
end
engine.register_participant :bob do |workitem|
workitem.decision = "there is a coordination problem, let's decide later"
end
engine.register_participant :charly do |workitem|
workitem.time_estimate = "Carlito says : cuando la vaca vuela"
end
Some aliases may get helpful :
engine.register_participant :boss, AliasParticipant.new("bob")
engine.register_participant :hr_head, AliasParticipant.new("alice")
engine.register_participant "engineering_.*", AliasParticipant.new("charly")
AtomFeedParticipantMore at atom :: AtomFeedParticipant
AtomPubParticipantMore at atom :: AtomPubParticipant
BlockParticipantThe participant with the most Ruby-feel.
The idea is to directly map a Ruby block as a participant to a workflow.
engine.register_participant(:vat) do |workitem|
price = Integer(workitem.price)
price = price + price * $vat
workitem.net_price = price
end
The reply to the engine is implicit, it has not to be stated in the block.
This participant is used in one of the examples in the examples/ folder of OpenWFEru. In that example, it’s barely used to dump the incoming workitem to the standard output (stdout).
You can pass a block that accepts no arguments, one (workitem) or two (flow expression and workitem) to this participant.
The last value in the block (its return value) is set as the value of the field ‘result’ of the outgoing workitem. Beware : do not set this value to something not serializable (like a reference to a complex object or something like that).
engine.register_participant(:my_participant) do |flow_expression, workitem|
workitem.attributes["parent"] = flow_expression.parent_id.to_s
#
# just storing the parent expression of the participant expression
# that sent the workitem, as a string in the workitem
#
# you could do more complicated and dangerous things with
# handle on the flow expression (but that's an advanced topic)
end
Note that if the block’s execution could take a certain amount of time, it’d be better to provide error handling and CancelItem handling inside of it. A short and hypothetical example :
engine.register_participant "sluggy" do |workitem|
if workitem.is_a?(OpenWFE::CancelItem)
#
# cancel job
job_id = jobs[workitem.fei]
external_sluggy_system.cancel_job(job_id)
else
#
# launch job
job_id = external_sluggy_system.execute_job(
workitem.param0, workitem.param1)
jobs[workitem.fei] = job_id
end
end
Maybe in such a case, a dedicated participant class could be better, leaving BlockParticipant for tiny chunk of ruby code.
BlogParticipantMore at atom :: BlogParticipant
CsvParticipant(this participant is part of the “openwferu-extras” gem)
CsvParticipants are called “decision tables” or “decision participants” in Java OpenWFE.
Here is a blog entry about the CSV tables behind decision participants.
Note that since OpenWFEru 0.9.13, the CSVParticipant has been moved to the OpenWFE::Extras namespace.
rdoc (CSV table)
FileParticipantThis participant just dumps workitems to files. By default, it dumps the workitem as a YAML string, but you can override the encode_workitem() method.
require 'openwfe/participants/participants'
engine.register_participant("Fedor", OpenWFE::FileParticipant)
In this example, workitems for participant ‘Fedor’ will get dispatched (dumped) in the directory ./work/out/
The FileParticipant has a counterpart : the FileListener which polls directory for incoming ‘file’ workitems.
The FileParticipant alone is interesting as well for debugging a business process, it can be placed at special places to “dump” the workitem as it reaches those points.
HashParticipantOpenWFEja features a worklist component. OpenWFEru (0.9.3) not yet, but this ‘HashParticipant’ is the first step towards a worklist.
It directly maps a participant with a hash for storing workitems waiting for treatment.
This participant is used in one of the examples in the examples/ folder of OpenWFEru.
But the better rdoc to browse is the one of the StoreParticipantMixin module HashParticipant implements.
MailParticipantThis participant sends a notification per email. The text of the email is generated by filling a template with the data found in the workitem payload.
rdoc (a former version of the MailParticipant)
NoOperationParticipantImmediately replies to the engine, doesn’t touch the workitem (no operation).
NullParticipantDiscards the workitem (no operation), but doesn’t reply to the engine. A bit like a BPM /dev/null
ProcessParticipantThe classical way to call a participant is via the subprocess :
<sequence>
<participant ref="alice" />
<subprocess ref="review" />
<participant ref="bob" />
<subprocess ref="review" />
<subprocess ref="http://process.server.company.org/processes/proc0.xml" />
</sequence>
<process-definition name="review">
<loop>
<participant ref="boss1" />
<participant ref="boss2" />
<break if="${f:boss2_decision} == ok" />
</loop>
</process-definition>
With a Ruby process definition it would look like :
sequence do
alice
review
bob
review
subprocess :ref => "http://process.server.company.org/processes/proc0.xml"
end
process_definition :name => "review" do
_loop do
boss1
boss2
_break :if => "${f:boss2_decision} == ok"
end
end
Note that the participant and the subprocess are simply referred directly via their names, except for the subprocess call using an URL. (Note too that expressions whose names are Ruby keywords like ‘loop’ and ‘break’ are escaped with an underscore as a prefix.
A ProcessParticipant can solve this URL problem :
require 'openwfe/participants/participants'
engine.register_participant(
"proc0",
ProcessParticipant.new("http://process.server.company.org/processes/proc0.xml"))
The process definition body would then look like :
sequence do
alice
review
bob
review
proc0
end
rdoc for more information on how to register a participant (as a URL, as a Ruby process class, ...)
SoapParticipantWraps a call to a webservice within a participant. The ‘result’ field of the workitem receives then the result of the call.
This is a minimal implementation, but it’s Ruby, the extension possibilities are huge.
This participant is used in one of the examples in the examples/ folder of OpenWFEru.
SocketParticipantA quite straightforward participant, it dumps the workitems over a TCP connection.
require 'openwfe/participants/socketparticipants'
sp = OpenWFE::SocketParticipant.new("localhost", 7007)
engine.register_participant(:stuart, sp)
An extension : XmlSocketParticipant which dispatches workitems encoded as XML (as OpenWFEja understands them).
Of course, there is a SocketListener available.
SqsParticipant(this participant and its listener is part of the “openwferu-extras” gem)
SQS is an Amazon web service : ‘Amazon Simple Queue Service (Amazon SQS) offers a reliable, highly scalable hosted queue for storing messages as they travel between computers’, learn more at http://aws.amazon.com
The SqsParticipant deposits workitems on a SQS queue where they can be read by other applications.
require 'openwfe/extras/participants/sqsparticipants'
engine.register_participant(:queue, OpenWFE::Extras::SqsParticipant.new("workqueue0"))
By default, this participant does a YAML dump of the workitem in its ‘hash version’ and then encodes it as Base64 before putting it in the queue. You can very easily change that by overriding encode_workitem(), for example :
require 'openwfe/extras/participants/sqsparticipants'
sp = OpenWFE::Extras::SqsParticipant.new("workqueue0")
class << sp
#
# instead of extending SqsParticipant, we simply modify the
# method implementation just for that sp instance
#
def encode_workitem (wi)
msg = ""
msg << wi.fei.to_s
msg << "\n"
msg << wi.attributes["task_description"]
msg << "\n"
msg
end
end
engine.register_participant(:queue, sp)
There is a counterpart to the SqsParticipant : an SqsListener
require 'openwfe/extras/listeners/sqslisteners'
engine.add_workitem_listener(
OpenWFE::Extras::SqsListener.new(:wiqueue3, engine.application_context),
"2m15s")
#
# will poll our SQS queue named "wiqueue3" for workitems
# every 2 minutes and 15 seconds
A listener allows InFlowWorkItems to resume back in their originating process instance. They also accept (if allowed) LaunchItems and launch a brand new process instance according to the info found in the LaunchItem.
The SqsListener expects by default that workitems (InFlowWorkItem and LaunchItem instances) are encoded as hashes, then dumped via YAML, then encoded with Base64 (like SqsParticipant does).
TwitterParticipantThe TwitterParticipant was first documented in this blog post on the tech blog.
Twitter notification could be a lightweight substitute for email notification.
require 'openwfe/extras/participants/twitterparticipants'
tp = OpenWFE::Extras::TwitterParticipant.new(
twitter_account, twitter_password, :no_ssl => true)
#
# remove this :no_ssl param if the Participant should use SSL
# (as sometimes Twitter doesn't listen on SSL)
engine.register_participant("twitter_notification", tp)
The TwitterParticipant, out of the box supports status and direct messages. The message itself is the value of the “twitter_message” workitem field. To send a direct message, set the “twitter_target” workitem field to a twitter login name and the twitter user will receive the message via email, thanks to Twitter (email notification anyway).
The way the message and the target are determined can be changed by overriding (or monkey patching) the extract_message() method. Here is an example :
require 'openwfe/extras/participants/twitterparticipants'
class MyTwitterParticipant < OpenWFE::Extras
#
# This method is meant to return a pair login name / message.
#
def extract_message (workitem)
message = ""
message << "customer '#{workitem.customer_name}'"
message << " replied '#{workitem.customer_reply}'"
message << " (#{Time.now.to_s})"
[ nil, message ]
#
# always returning nil as the twitter target, the message
# will thus always be a status message, not a direct message
end
end
YamlParticipantThis participant is an extension of the HashParticipant built on top of a YAML file persistence.
By default, the workitems are stored under ”./work/participants/name_of_the_participant/”
This participant is used in the sample code of the ManoTracker
As for the HashParticipant, it’s interesting to browse the rdoc of the StoreParticipantMixin module that this participant implements.