jBPM persistence

jBPM implements a persistence layer based on JPA and Hibernate.

jBPM allows the persistent storage of certain information. This chapter describes these different types of persistence, and how to configure them. An example of the information stored is the process runtime state. Storing the process runtime state is necessary in order to be able to continue execution of a process instance at any point, if something goes wrong. -- 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 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 underlaying 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.

SimpleSessionCommandService
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();
    }

SessionStore

Persisting the SessionInfo is handled by the JcrSessionStore implementation in SystemContextSessionStore where all operations are performed in Magnolia's SystemContext.

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.

Marshalling

The marshalling of work items happens in the update method of the WorkItemInfo object and the binary data is stored under the bytes property.

WorkItemStore

Persisting the WorkItemInfo is handled by the JcrWorkItemStore implementation in SystemContextWorkitemStore where all operations are performed in Magnolia's SystemContext.

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 state of a process instance is stored at so-called "safe points" during the execution of the process engine. Whenever a process instance is executing (for example when it started or continuing from a previous wait state, the engine executes the process instance until no more actions can be performed (meaning that the process instance either has completed (or was aborted), or that it has reached a wait state in all of its parallel paths). At that point, the engine has reached the next safe state, and the state of the process instance (and all other process instances that might have been affected) is stored persistently. – jBPM: Safe Points

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.  

JCR persistence diagram

This diagram shows the most important classes used for persisting sessions, workitems and processes to JCR.

#trackbackRdf ($trackbackUtils.getContentIdentifier($page) $page.title $trackbackUtils.getPingUrl($page))