Chapter 10. Asynchronous Continuations
10.1. The Concept
jBPM is based on Graph-Oriented Programming (GOP). Basically, GOP specifies a simple-state machine that can handle concurrent paths of execution but, in the specified execution algorithm, all state transitions are undertaken in a single thread client operation. By default, it is a good approach to perform state transitions in the thread of the client because it fits naturally with server-side transactions. The process execution moves from one "wait" state to another in the space of one transaction.
In some situations, a developer might want to fine-tune the transaction demarcation in the process definition. In jPDL, it is possible to specify that the process execution should continue asynchronously with the attribute
async="true"
. async="true"
is supported only when it is triggered in an event but can be specified on all node types and all action types.
10.2. Example
Normally, a node is always executed after a token has entered it. Hence, the node is executed in the client's thread. One will explore asynchronous continuations by looking at two examples. The first example is part of a process with three nodes. Node 'a' is a wait state, node 'b' is an automated step and node 'c' is, again, a wait state. This process does not contain any asynchronous behavior and it is represented in the diagram below.
The first frame shows the starting situation. The token points to node 'a', meaning that the path of execution is waiting for an external trigger. That trigger must be given by sending a signal to the token. When the signal arrives, the token will be passed from node 'a' over the transition to node 'b'. After the token arrived in node 'b', node 'b' is executed. Recall that node 'b' is an automated step that does not behave as a wait state (e.g. sending an email). So the second frame is a snapshot taken when node 'b' is being executed. Since node 'b' is an automated step in the process, the execute of node 'b' will include the propagation of the token over the transition to node 'c'. Node 'c' is a wait state so the third frame shows the final situation after the signal method returns.
Figure 10.1. Example One: Process without Asynchronous Continuation
Whilst "persistence" is not mandatory in jBPM, most commonly a signal will be called within a transaction. Look at the updates of that transaction. Initially, the token is updated to point to node 'c'. These updates are generated by Hibernate as a result of the
GraphSession.saveProcessInstance
on a JDBC connection. Secondly, in case the automated action accesses and updates some transactional resources, such updates should be combined or made part of the same transaction.
The second example is a variant of the first and introduces an asynchronous continuation in node 'b'. Nodes 'a' and 'c' behave the same as in the first example, namely they behave as wait states. In jPDL a node is marked as asynchronous by setting the attribute
async="true"
.
The result of adding
async="true"
to node 'b' is that the process execution will be split into two parts. The first of these will execute the process up to the point at which node 'b' is to be executed. The second part will execute node 'b.' That execution will stop in wait state 'c'.
The transaction will hence be split into two separate transactions, one for each part. While it requires an external trigger (the invocation of the
Token.signal
method) to leave node 'a' in the first transaction, jBPM will automatically trigger and perform the second transaction.
Figure 10.2. A Process with Asynchronous Continuations
For actions, the principle is similar. Actions that are marked with the attribute
async="true"
are executed outside of the thread that executes the process. If persistence is configured (it is by default), the actions will be executed in a separate transaction.
In jBPM, asynchronous continuations are realized by using an asynchronous messaging system. When the process execution arrives at a point that should be executed asynchronously, jBPM will suspend the execution, produces a command message and send it to the command executor. The command executor is a separate component that, upon receipt of a message, will resume the execution of the process where it got suspended.
jBPM can be configured to use a JMS provider or its built-in asynchronous messaging system. The built-in messaging system is quite limited in functionality, but allows this feature to be supported on environments where JMS is unavailable.
10.3. The Job Executor
The job executor is the component that resumes process executions asynchronously. It waits for job messages to arrive over an asynchronous messaging system and executes them. The two job messages used for asynchronous continuations are
ExecuteNodeJob
and ExecuteActionJob
.
These job messages are produced by the process execution. During process execution, for each node or action that has to be executed asynchronously, a
Job
(Plain Old Java Object) will be dispatched to the MessageService
. The message service is associated with the JbpmContext
and it just collects all the messages that have to be sent.
The messages will be sent as part of
JbpmContext.close()
. That method cascades the close()
invocation to all of the associated services. The actual services can be configured in jbpm.cfg.xml
. One of the services, JmsMessageService
, is configured by default and will notify the job executor that new job messages are available.
The graph execution mechanism uses the interfaces
MessageServiceFactory
and MessageService
to send messages. This is to make the asynchronous messaging service configurable (also in jbpm.cfg.xml
). In Java EE environments, the DbMessageService
can be replaced with the JmsMessageService
to leverage the application server's capabilities.
The following is a brief summary of the way in which the job executor works.
"Jobs" are records in the database. Furthermore, they are objects and can be executed. Both timers and asynchronous messages are jobs. For asynchronous messages, the dueDate is simply set to the current time when they are inserted. The job executor must execute the jobs. This is done in two phases.
- The dispatcher thread must acquire a job
- An executor thread must execute the job
Acquiring a job and executing the job are done in 2 separate transactions. The dispatcher thread acquires jobs from the database on behalf of all the executor threads on this node. When the executor thread takes the job, it adds its name into the owner field of the job. Each thread has a unique name based on IP address and sequence number.
A thread could fail between acquisition and execution of a job. To clean-up after those situations, there is one lock-monitor thread per job executor that checks the lock times. The lock monitor thread will unlock any jobs that have been locked for more than 10 minutes, so that they can be executed by another job executor thread.
The isolation level must be set to
REPEATABLE_READ
for Hibernate's optimistic locking to work correctly. REPEATABLE_READ
guarantees that this query will only update one row in exactly one of the competing transactions.
update JBPM_JOB job set job.version = 2 job.lockOwner = '192.168.1.3:2' where job.version = 1
Non-Repeatable Reads can lead to the following anomaly. A transaction re-reads data it has previously read and finds that data has been modified by another transaction, one that has been committed since the transaction's previous read.
Non-Repeatable reads are a problem for optimistic locking and therefore, isolation level
READ_COMMITTED
is not enough because it allows for Non-Repeatable reads to occur. So REPEATABLE_READ
is required if you configure more than one job executor thread.
Configuration properties related to the job executor are:
- jbpmConfiguration
- The bean from which configuration is retrieved.
- name
- The name of this executor.
Important
This name should be unique for each node, when more than one jBPM instance is started on a single machine. - nbrOfThreads
- The number of executor threads that are started.
- idleInterval
- The interval that the dispatcher thread will wait before checking the job queue, if there are no jobs pending.
Note
The dispatcher thread is automatically notifed when jobs are added to the queue. - retryInterval
- The interval that a job will wait between retries, if it fails during execution. The default value for this is 3 times.
Note
The maximum number of retries is configured by jbpm.job.retries. - maxIdleInterval
- The maximum period for idleInterval.
- historyMaxSize
- This property is deprecated and has no effect.
- maxLockTime
- The maximum time that a job can be locked before the lock-monitor thread will unlock it.
- lockMonitorInterval
- The period for which the lock-monitor thread will sleep between checking for locked jobs.
- lockBufferTime
- This property is deprecated, and has no affect.
10.4. jBPM's built-in asynchronous messaging
When using jBPM's built-in asynchronous messaging, job messages will be sent by persisting them to the database. This message persisting can be done in the same transaction or JDBC connection as the jBPM process updates.
The job messages will be stored in the
JBPM_JOB
table.
The POJO command executor (
org.jbpm.msg.command.CommandExecutor
) will read the messages from the database table and execute them. The typical transaction of the POJO command executor looks like this:
- Read next command message
- Execute command message
- Delete command message
If execution of a command message fails, the transaction will be rolled back. After that, a new transaction will be started that adds the error message to the message in the database. The command executor filters out all messages that contain an exception.
Figure 10.3. POJO command executor transactions
If the transaction that adds the exception to the command message fails, it is rolled back. The message will remain in the queue without an exception and will be retried later.
Important
jBPM's built-in asynchronous messaging system does not support multi-node locking. You cannot deploy the POJO command executor multiple times and have them configured to use the same database.