JCR persistence
jBPM persistence
jBPM implements a persistence layer based on JPA and Hibernate.
The jBPM engine supports persistent storage of the runtime state of running process instances. Because it stores the runtime states, it can continue execution of a process instance if the jBPM engine stopped or encountered a problem at any point. – jBPM: Persistence and Transactions
In short the persistence allows shutting down the system and restoring the state of all running processes upon restart.
In order to not have to set up a separate storage mechanism in Magnolia for the jBPM engine, Magnolia provides its own JCR persistence layer for jBPM.
JCR persistence
Magnolia stores all runtime data from the jBPM engine in the
workflow
workspace. By default, workflow is only enabled on the author
instance.
jBPM stores most of the data used for its execution as binary data using marshalling mechanisms. This makes the underlying scheme rather simple.
Session
As we are using the
singleton
strategy for our runtime engine we are only dealing with one session.
This session is stored under the sessions
node with 0
as static
identifier and is never removed. All data is marshalled into the binary
property bytes
.
The session ID is not using the key generator Magnolia uses for storing processes and workItems. One reason is that by using the singleton strategy this is not necessary at the moment at least. Another reason ist that the interfaces and implementation classes used inside the jBPM persistence package is restricted to use integers as IDs, compared to processInstances and workItems using a long.
Marshalling
The marshalling is done inside the SessionInfo’s update method, which is an object used and created by the CommandService.
protected void initNewKnowledgeSession(KieBase kbase, KieSessionConfiguration conf) {
this.sessionInfo = new SessionInfo();
...
this.marshallingHelper = new SessionMarshallingHelper( this.ksession, conf );
this.sessionInfo.setJPASessionMashallingHelper( this.marshallingHelper );
...
this.commandService = new TransactionInterceptor(kContext);
}
SessionInfo
public void update() {
this.rulesByteArray = this.helper.getSnapshot();
}
ProcessInstance
Processes are stored under the processInstances
node inside the
workflow
workspace. As this data is used during the actual execution
of processes, the data is removed when the process terminates.
Magnolia’s current implementation does not support logging completed
processes for further auditing. For more information how this could be
implemented, see the documentation of
jBPM
Audit data model.
Key generator
For creating the IDs for process instances we use the
ProcessInstanceIdGenerator
which creates a Long from the current
system time. The processes are further stored hierarchical based on
year, month of year and day of month.
Marshalling
Similar to the sessions, the processes use a marshalling mechanism and
most of the runtime data is stored as a binary under the bytes property.
The marshalling is performed inside ProcessInstanceInfo’s update method.
The info object is created by JcrProcessInstanceManager
.
ProcessInstanceStore
Persisting the ProcessInstanceInfo is handled by the JcrProcessStore
implementation in SystemContextProcessStore
where all operations are
performed in Magnolia’s SystemContext
. Because we do not need the
correlation key
used for mapping sessions to processInstances due to
the singleton strategy
, those methods are currently not implemented.
Workitem
Key generator
Workitems are using the same hierarchical structure and keys as the
processInstances. The key generator used is implemented in
WorkItemIdGenerator
.
Safe points
Contrary to the simple storage scheme finding the right spots to persist the current state of a process is a bit more tricky. These spots are called safe points:
The jBPM engine saves the state of a process instance to persistent storage at safe points during the execution of the process.
When a process instance is started or resumes execution from a previous wait state, the jBPM engine continues the execution until no more actions can be performed. If no more actions can be performed, it means that the process has completed or else has reached a wait state. If the process contains several parallel paths, all the paths must reach a wait state.
This point in the execution of the process is considered a safe point. At this point, the jBPM engine stores the state of the process instance, and of any other process instances that were affected by the execution, to persistent storage.
These safe points are reached by different classes. Some of of the logic
is taken care of by the ProcessInstanceManager
and WorkItemManager
where the state is persisted when the execution starts or is completed.
As this is not sufficient for keeping the persisted state updated at all
times we hook into the internal execution of a process with the
CommandService
.
CommandService
To persist the processes at safe points, Magnolia uses a CommandService which allows intercepting internally used Commands.
When loading or creating a KieSession
by the JcrSessionFactory
it
delegates the creation to JcrKieStoreServices
which in turn creates a
CommandBasedStatefulKnowledgeSession
, an implementation of the
KieSession
creating Commands for each step of the process.
As an example this is how the CommandBasedStatefulKnowledgeSession
starts a process by creating a StartProcessCommand
containing the
processId and the parameters. You also see how the actual execution of
the command is delegated to the commandService
.
...
public ProcessInstance startProcess(String processId, Map<String, Object> parameters) {
StartProcessCommand command = new StartProcessCommand();
command.setProcessId( processId );
command.setParameters( parameters );
return commandService.execute( command );
}
...
When not using the CommandBasedStatefulKnowledgeSession
this would
create a ProcessInstance
and directly start it.
Interceptors
The interceptors used for persisting the state are registered by the
JcrSessionFactory
to the commandService
.
The concept behind these interceptors is rather simple. They act as
CommandExecutors for commands and allow adding custom logic before and
after executing the command. As an example let’s take a look at the
JcrPersistProcessInterceptor
which takes care of persisting
ProcessInstances.
JcrPersistProcessInterceptor
public JcrPersistProcessInterceptor(SimpleSessionCommandService interceptedService) {
this.interceptedService = interceptedService;
}
@Override
public <T> T execute(Command<T> command) {
T result = null;
try {
result = executeNext(command);
}
...
if (isValidCommand(command)) {
executeNext(new PersistProcessCommand(jpm, ksession));
}
...
}
protected boolean isValidCommand(Command<?> command) {
return (command instanceof StartProcessCommand) ||
(command instanceof CreateProcessInstanceCommand) ||
...
(command instanceof FireAllRulesCommand);
}
Note how the interceptor creates and executes a PersistProcessCommand
in case the Command met the criteria of isValidCommand(command)
.
Persisting the ProcessInstance is then taken care of by the
PersistProcessCommand.
PersistProcessCommand
public PersistProcessCommand(Object jpm, KieSession ksession) {
this.persistenceContext = ((ProcessPersistenceContextManager) jpm).getProcessPersistenceContext();
this.ksession = ksession;
}
@Override
public Void execute(Context context) {
...
ProcessInstanceInfo info = new ProcessInstanceInfo(instance, ksession.getEnvironment());
info.setId(instance.getId());
info.update();
persistenceContext.persist(info);
...
}
A similar interceptor is used for persisting the session state.