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.

Workflow workspace

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.

image

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.

image

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

image

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 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.

JCR persistence diagram

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

image

Feedback