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
(POJO) 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, DbMessageService
, 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.
Here's how the job executor works in a nutshell:
Jobs are records in the database. Jobs are objects and can be executed, too. Both timers and async messages are jobs. For async 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 2 phases: 1) a job executor thread must acquire a job and 2) the thread that acquired the job must execute it.
Acquiring a job and executing the job are done in 2 separate transactions. A thread
acquires a job by putting its name into the owner field of the job. Each thread has a unique
name based on ip-address and sequence number. Hibernate's optimistic locking is enabled on
Job
-objects. So if 2 threads try to acquire a job concurrently, one of
them will get a StaleObjectException and rollback. Only the first one will succeed. The
thread that succeeds in acquiring a job is now responsible for executing it in a separate
transaction.
A thread could die 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. Jobs that are locked for more then 30 mins (by default) will be unlocked so that they can be executed by another job.
The required isolation level should be set to REPEATABLE_READ for hibernate's optimistic locking to work correctly. That isolation level will guarantee that
update JBPM_JOB job set job.version = 2 job.lockOwner = '192.168.1.3:2' where job.version = 1
will only result in 1 row updated in exactly 1 of the competing transactions.
Non-Repeatable Reads means that the following anomaly can happen: 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 cause it allows for Non-Repeatable reads to occur. So REPEATABLE_READ is required if you configure more than one job executor thread.