spiff-arena/doc/bpmn/events.rst

261 lines
9.5 KiB
ReStructuredText

Events
======
BPMN Model
----------
We'll be using the following files from `spiff-example-cli <https://github.com/sartography/spiff-example-cli>`_.
- `transaction <https://github.com/sartography/spiff-example-cli/blob/master/bpmn/transaction.bpmn>`_ workflow
- `signal_event <https://github.com/sartography/spiff-example-cli/blob/master/bpmn/signal_event.bpmn>`_ workflow
- `events <https://github.com/sartography/spiff-example-cli/blob/master/bpmn/events.bpmn>`_ workflow
- `call activity <https://github.com/sartography/spiff-example-cli/blob/master/bpmn/call_activity.bpmn>`_ workflow
- `product_prices <https://github.com/sartography/spiff-example-cli/blob/master/bpmn/product_prices.dmn>`_ DMN table
- `shipping_costs <https://github.com/sartography/spiff-example-cli/blob/master/bpmn/shipping_costs.dmn>`_ DMN table
A general overview of events in BPMN can be found in the :doc:`/intro`
section of the documentation.
SpiffWorkflow supports the following Event Definitions:
- `Cancel Events`_
- `Signal Events`_
- `Terminate Events`_
- `Error Events`_
- `Escalation Events`_
- `Timer Events`_
- `Message Events`_
We'll include examples of all of these types in this section.
Transactions
^^^^^^^^^^^^
We also need to introduce the concept of a Transaction, bceause certain events
can only be used in that context. A Transaction is essentially a subprocess, but
it must fully complete before it affects its outer workflow.
We'll make our customer's ordering process through the point they review their order
into a Transaction. If they do not complete their order, then product selections and
customizations will be discarded; if they place the order, the workflow will proceed
as before.
We'll also introduce our first event type, the Cancel Event. Cancel Events can
only be used in Transactions.
Cancel Events
^^^^^^^^^^^^^
.. figure:: figures/transaction.png
:scale: 30%
:align: center
Workflow with a transaction and Cancel Event
We changed our 'Review Order' Task to be a User Task and have added a form, so
that we can give the customer the option of cancelling the order. If the customer
answers 'Y', then the workflow ends normally and we proceed to collecting
payment information.
However, if the user elects to cancel their order, we use a 'Cancel End Event'
instead, which generates a Cancel Event. We can then attach a 'Cancel Boundary
Event' to the Transaction, and execute that path if the event occurs. Instead of
asking the customer for their payment info, we'll direct them to a form and ask
them why they cancelled their order.
If the order is placed, the workflow will contain the order data; if it is
cancelled, it will contain the reason for cancellation instead.
To run this workflow
.. code-block:: console
./run.py -p order_product \
-d bpmn/product_prices.dmn bpmn/shipping_costs.dmn \
-b bpmn/transaction.bpmn bpmn/call_activity.bpmn
Signal Events
^^^^^^^^^^^^^
.. figure:: figures/signal_event.png
:scale: 30%
:align: center
Workflow with Signal Events
Suppose we also want to give our customer the ability to cancel their order at
any time up until they are charged. We need to throw an event after the charge
is placed and catch this event before the user completes the 'Cancel Order' task.
Once the charge is placed, the task that provides the option to cancel will
itself be cancelled when the charge event is received.
We'll also need to detect the case that the customer cancels their order and
cancel the charge task if it occurs; we'll use a separate signal for that.
Multiple tasks can catch the same signal event. Suppose we add a Manager role
to our workflow, and allow the Employee to refer unsuccessful charges to the
Manager for resolution. The Manager's task will also need to catch the 'Order
Cancelled' signal event.
Signals are referred to by name.
.. figure:: figures/throw_signal_event.png
:scale: 30%
:align: center
Signal Event configuration
.. Terminate Events:
Terminate Events
^^^^^^^^^^^^^^^^
We also added a Terminate Event to the Manager Workflow. A regular End Event
simply marks the end of a path. A Terminate Event will indicate that the
entire workflow is complete and any remaining tasks should be cancelled. Our
customer cannot cancel an order that has already been cancelled, and we won't ask
them for feedback about it (we know it wasn't completed), so we do not want to
execute either of those tasks.
We'll now modify our workflow to add an example of each of the other types of
events that SpiffWorkflow Supports.
To run this workflow
.. code-block:: console
./run.py -p order_product \
-d bpmn/product_prices.dmn bpmn/shipping_costs.dmn \
-b bpmn/signal_event.bpmn bpmn/call_activity.bpmn
Error Events
^^^^^^^^^^^^
Let's turn to our order fulfillment subprocess. Either of these steps could
potentially fail, and we may want to handle each case differently.
.. figure:: figures/events.png
:scale: 30%
:align: center
Workflow with multiple event types
One potential failure is that our product is unavailable. This actually might be
a temporary problem, but we'll assume that it is a show stopper for the sake of
this tutorial.
We ask the Employee to verify that they were able to retrieve the product; if they
were unable to do so, then we generate an Error End Event, which we will handle
with an Interrupting Error Boundary Event (Error events are *always* Interrupting).
If the product is unavailable, our Manager will notify the customer, issue a refund,
and cancel the order.
Escalation Events
^^^^^^^^^^^^^^^^^
Escalation events are a lot like Error Events and as far as I can tell, which one
to use comes down to preference, with the caveat that if you want to use an Intermediate
Event, you'll have to use Escalation, because BPMN does not allow Intermediate Error Events,
and that Error Events cannot be Non-Interrupting.
In our example, we'll assume that if we failed to ship the product, we can try again later,
so we will not end the Subprocess (Escalation events can be either Interrupting or
Non-Interrupting).
However, we still want to notify our customer of a delay, so we use a Non-Interrupting
Escalation Boundary Event.
Both Error and Escalation Events can be optionally associated with a code. Here is
Throw Event for our `product_not_shipped` Escalation.
.. figure:: figures/throw_escalation_event.png
:scale: 30%
:align: center
Throw Escalation Event configuration
Error Event configuration is similar.
If no code is provided in a Catch event, any event of the corresponding type will catch
the event.
Timer Events
^^^^^^^^^^^^
In the previous section, we mentioned that that we would try again later if we were unable
to ship the order. We can use a Duration Timer Event to force our workflow to wait a certain
amount of time before continuing. We can use this as a regular Intermediate Event (in
'Try Again Later') or a Boundary Event. Timer Boundary Events can be Interrupting, but in
this case, we simply want to notify the customer of the delay while continuing to process
their order, so we use a Non-Interrupting Event.
.. figure:: figures/timer_event.png
:scale: 30%
:align: center
Duration Timer Event configuration
We express the duration as a Python :code:`timedelta`. We show the configuration for the Boundary
Event.
It is also possible to use a static datetime to trigger an event. It will need to be parseable
as a date by Python.
Timer events can only be caught, that is waited on. The timer begins implicitly when we
reach the event.
Message Events
^^^^^^^^^^^^^^
.. sidebar:: QA Lane
Ideally, this lane would be a process independent from the ordering process (we don't want
it to be cancelled just because an order eventually completes). However, limitations of how
SpiffWorkflow handles processes precludes multiple top-level processes.
In BPMN, Messages are used to communicate across processes and cannot be used within a
workflow, but SpiffWorkflow allows message communication between lanes as well as between
parent and child workflows. We'll use the first scenario in our example.
We've added a QA lane to out ordering process, whose job is investigating order order delays
and recommending improvements. This portion of our process will only be started when an
appropriate message is received.
Messages are similar to signals, in that they are referenced by name, but they have the
additional property that they may contain a payload.
.. note::
We currently depend on some Camunda-specific features in our implementation, but we
intend to replace this with our own.
.. figure:: figures/throw_message_event.png
:scale: 30%
:align: center
Throw Message Event configuration
The Throw Message Event Implementation should be 'Expression' and the Expression should
be a Python statement that can be evaluated. In this example, we'll just send the contents
of the :code:`reason_delayed` variable, which contains the response from the 'Investigate Delay'
Task.
We can provide a name for the result variable, but I have not done that here, as it does not
make sense to me for the generator of the event to tell the handler what to call the value.
If you *do* specify a result variable, the message payload (the expression evaluated in the
context of the Throwing task) will be added to the handling task's data in a variable of that
name; if you leave it blank, SpiffWorkflow will create a variable of the form <Handling
Task Name>_Response.
Running The Model
^^^^^^^^^^^^^^^^^
.. code-block:: console
./run.py -p order_product \
-d bpmn/product_prices.dmn bpmn/shipping_costs.dmn \
-b bpmn/events.bpmn bpmn/call_activity.bpmn