By now, we have seen the two main components of a jBPM6 runtime, the KIE session and the Human task service. These components are provided as the basic components needed to run process interactions between humans and systems. As the responsibilities of the process runtime grows, connectors to external systems should be added to transform the process engine into a highly configurable enterprise integration platform, where coordination between different systems can be easily managed in a business-friendly way.
The entire configuration involved in creating a runtime for the process engine, the Human task component, and all the external systems connectors could become cumbersome if not managed in a single place. In this chapter, we will learn about the runtime manager component, which is designed to create a bridge between all the components and configurations involved in a process execution. This component can be used to create a full runtime for the process engine. For this chapter, our goals will be as follows:
The runtime manager's main role in an application is to provide an application-wide point to access process runtimes (all the necessary interconnected services to execute a process execution within a specific domain). The nature of those runtimes and how many different runtimes can exist in a particular domain is a decision that each of the runtime manager implementations available has to make. By default, a runtime will be composed of two main components: the KIE session where the process is executed and the Human task component where human tasks will be handled. Both are grouped and returned from the runtime manager through the RuntimeEngine
interface:
public interface RuntimeManager { RuntimeEngine getRuntimeEngine(Context<?> context); void disposeRuntimeEngine(RuntimeEngine engine); String getIdentifier(); void close(); } public interface RuntimeEngine { KieSession getKieSession(); TaskService getTaskService(); }
Runtime managers will return a specific number of runtime engines, depending on their specific nature. There is a singleton implementation that will return a single session for all required runtimes in an application, no matter how many you ask. On the other hand, there is a runtime manager for each process instance that will provide a separate session for each process instance started in the runtime. The following diagram shows how the classes involved in the runtime manager component interact with each other:
Determining what type of runtime manager we should use for our environment is a complex decision that involves an equilibrium between synchronization of operations and the number of concurrent calls that can be performed by a BPM environment at the same time. We will try to explain the advantages and disadvantages of each type of runtime manager as we explain them. Regarding the architectural decisions on which runtime manager to use, we will see a few considerations to help us in Chapter 10, Integrating KIE Workbench with External Systems.
The nature of the KIE sessions will impact the amount of information shared between process instances and how recoverable that information will be by other threads later on. The KIE sessions have a memory group called working memory, which groups a set of objects that will be evaluated by business rules. If you feed information to this working memory from the process instance or decide to fire rules during the process execution, depending on the level of isolation of the KIE session, the working memory might contain objects from a single process instance or many.
Rules evaluate this working memory to find patterns between different objects. Depending on what a business rule states, you might want to match different objects that come from different process instances, or isolate them on the instance level/request level of each process. For these cases, the runtime manager provides different alternatives to access a process instance in different session environments.
Runtime managers provide you with a strategy to access different levels of isolation for your runtime, but it depends on another component to define all the configurations that allow the runtime to run in a specific way. The component that allows you to define these configurations is called the runtime environment, which is a grouping of all the different elements required for the process runtime. In the runtime environment, you can configure several things:
The following code section defines the RuntimeEnvironment
interface:
public interface RuntimeEnvironment { KieBase getKieBase(); Environment getEnvironment(); KieSessionConfiguration getConfiguration(); boolean usePersistence(); RegisterableItemsFactory getRegisterableItemsFactory(); UserGroupCallback getUserGroupCallback(); ClassLoader getClassLoader(); void close(); }
As you can see, there are methods to provide all the different components involved in providing the necessary information to the runtime to run correctly, including the KIE session configuration properties, class loaders, and runtime connectors, which are defined inside the registerable items factory. This is explained in the next section.
The runtime environment has a particular subcomponent that takes care of registering all the different connectors to external systems. This subcomponent is called RegisterableItemsFactory
, and it provides a contract for the process engine to populate its work item handlers and different listener types for process and rule executions:
public interface RegisterableItemsFactory { Map<String, WorkItemHandler> getWorkItemHandlers( RuntimeEngine runtime); List<ProcessEventListener> getProcessEventListeners( RuntimeEngine runtime); List<AgendaEventListener> getAgendaEventListeners( RuntimeEngine runtime); List<WorkingMemoryEventListener> getWorkingMemoryEventListeners( RuntimeEngine runtime); }
As you might have noticed already, external connectors can be implemented in a bidirectional way—both to expose the internal functionality of the engine to the rest of the application components and to configure signaling methods from external components back into the engine. It is because of this reason that listeners and work item handlers are provided with a reference to the runtime engine, so you can pass it on to your own connectors whenever they might need them.
Another thing to notice is that all these interfaces (except for RuntimeManager
) define just getters for different components that the runtime will need at specific instances of rules and processes execution paths. You can implement your own to return the different implementations you wish as you see fit. However, the jbpm-runtime-manager
project provides two implementations: SimpleRuntimeEnvironment
and DefaultRuntimeEnvironment
.
The SimpleRuntimeEnvironment
implementation provides setters as well as getters for all the configuration components, or enough setters to create the configuration components based on simpler parameters. The DefaultRuntimeEnvironment
instance extends the first implementation to add the most usual configurations as a default template. This default configuration will provide a persistent environment for sessions to be created, with history logs and the Human task component preconfigured.
Since persistence will be covered in detail in Chapter 8, Implementing Persistence and Transactions, we will save it for later use and just focus on working with SimpleRuntimeEnvironment
for now. However, in order to simplify the code a little, we will not use it directly, but through a builder class called RuntimeEnvironmentBuilder
. In the following code snippet, we can see how this builder class will let us initialize a RuntimeEnvironment
instance really fast:
SimpleRegisterableItemsFactory factory = new SimpleRegisterableItemsFactory(); factory.addWorkItemHandler("Human Task", MyWIHandler.class); RuntimeEnvironment environment = RuntimeEnvironmentBuilder .Factory.get().newEmptyBuilder() .userGroupCallback(userGroupCallback) .knowledgeBase(kbase) .registerableItemsFactory(factory) .get();
As you can see in the previous code, we can provide our own implementations of different components to a RuntimeEnvironment
instance really fast without creating multiple lines of code to invoke each of the setters provided by a specific implementation.
You might have also noticed the SimpleRegisterableItemsFactory
implementation that we used to register a work item handler type (by a class name) to its correspondent key. While the API provides two different implementations of the RuntimeEnvironment
interface, it also provides several different implementations or the RegisterableItemsFactory
implementation that we can use in different situations. The different implementations available are:
SimpleRegisterableItemsFactory
: This implementation defines type setters for work item handlers and listeners. When a runtime engine asks for the implementations, it will try to build the different handlers. It will create each of the specified types looking for a constructor with a KieSession
parameter, a TaskService
parameter, a RuntimeEngine
parameter, or no parameter at all.DefaultRegisterableItemsFactory
: This implementation extends the previous one to provide default listeners and work item handlers. Particularly, it will create a Human task component work item handler and listeners to trigger rules from processes and keep history logs in a persistent environment.KModuleRegisterableItemsFactory
: This implementation extends the previous one to add any configuration defined in the kmodule.xml
file of a specific KieModule
object. It is constructed with a KieContainer
reference and a session name from which to obtain the correspondent mappings.InjectableRegisterableItemsFactory
: This implementation extends the default implementation to discover the different configuration components from CDI injection.Now that we understand how the environment for a runtime manager is configured, and before we start working with the actual RuntimeEngine
implementations, we need to understand how the lifecycle of the runtime is expected to be executed.
We have already reviewed the idea behind the runtime manager and the runtime engine to understand their purpose. The runtime engine is a grouping of all the running configurations connected together that are provided by the runtime manager. We now need to understand how these components reside in the server and at what times they must be created or discarded. First of all, let's discuss the runtime manager's lifecycle.
The runtime manager usually behaves like a singleton in an application; we just need one instance of it. From that one instance, we will construct one or many runtime engines. So, it is important that even if we create many different runtime managers, they all behave in the same manner; otherwise, we might have different behaviors depending on how many runtime managers we have.
In the next section, we will see how different implementations of the runtime manager interface provide solutions for this problem. Whichever implementation we choose, the behavior against the obtained RuntimeEngine
instance is the same as explained in the following sequence diagram:
The RuntimeManager
implementation will return a specific runtime engine, depending on the RuntimeManager
type and the Context
parameter we pass to it:
RuntimeManager manager = ...; RuntimeEngine engine = manager.getRuntimeEngine( EmtpyContext.get());
Once we receive a RuntimeEngine
instance, we will be able to interact with its subcomponents using the following options:
engine.getKieSession().startProcess("procId"); engine.getKieSession().fireAllRules(); engine.getKieSession().signalEvent("signalX", null);
List<TaskSummary> tasks = engine.getTaskService(). getTasksOwned("john", "en-UK"); Long taskId = tasks.iterator().next().getId() engine.getTaskService().start(taskId, "john");
The RuntimeEngine
interface could also be extended to add other interactions if necessary for a particular domain.
When we're done using RuntimeEngine
in the current transaction or thread, we let RuntimeManager
know whether it can try to free resources by calling the disposeRuntimeEngine
method:
manager.disposeRuntimeEngine(engine);
Once we do that, the engine
instance will be rendered unusable. If we want to interact with the processes or tasks, we will need to get another instance of RuntimeEngine
.
Now that we understand the lifecycle of both the RuntimeManager
and RuntimeEngine
instances, we can discuss the different types of runtime managers available for use in the jBPM6 code base.