/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jbpm.runtime.manager.impl;
import java.util.Objects;
import org.drools.core.command.SingleSessionCommandService;
import org.drools.core.command.impl.CommandBasedStatefulKnowledgeSession;
import org.drools.core.command.impl.ExecutableCommand;
import org.drools.core.command.impl.RegistryContext;
import org.drools.core.command.runtime.BatchExecutionCommandImpl;
import org.drools.core.common.InternalKnowledgeRuntime;
import org.drools.core.event.AbstractEventSupport;
import org.drools.persistence.api.OrderedTransactionSynchronization;
import org.drools.persistence.api.TransactionManager;
import org.drools.persistence.api.TransactionManagerHelper;
import org.jbpm.runtime.manager.impl.error.ExecutionErrorManagerImpl;
import org.jbpm.runtime.manager.impl.factory.LocalTaskServiceFactory;
import org.jbpm.runtime.manager.impl.mapper.EnvironmentAwareProcessInstanceContext;
import org.jbpm.runtime.manager.impl.mapper.InMemoryMapper;
import org.jbpm.runtime.manager.impl.mapper.InternalMapper;
import org.jbpm.runtime.manager.impl.mapper.JPAMapper;
import org.jbpm.runtime.manager.impl.tx.DisposeSessionTransactionSynchronization;
import org.jbpm.services.task.impl.TaskContentRegistry;
import org.kie.api.command.BatchExecutionCommand;
import org.kie.api.event.process.DefaultProcessEventListener;
import org.kie.api.event.process.ProcessCompletedEvent;
import org.kie.api.event.process.ProcessStartedEvent;
import org.kie.api.runtime.EnvironmentName;
import org.kie.api.runtime.ExecutableRunner;
import org.kie.api.runtime.KieSession;
import org.kie.api.runtime.manager.Context;
import org.kie.api.runtime.manager.RuntimeEngine;
import org.kie.api.runtime.manager.RuntimeEnvironment;
import org.kie.api.task.TaskService;
import org.kie.internal.runtime.manager.Disposable;
import org.kie.internal.runtime.manager.InternalRuntimeManager;
import org.kie.internal.runtime.manager.Mapper;
import org.kie.internal.runtime.manager.SessionFactory;
import org.kie.internal.runtime.manager.TaskServiceFactory;
import org.kie.internal.runtime.manager.context.CaseContext;
import org.kie.internal.runtime.manager.context.ProcessInstanceIdContext;
import org.kie.internal.task.api.ContentMarshallerContext;
import org.kie.internal.task.api.InternalTaskService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EventListener;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A RuntimeManager implementation that is backed by the "Per Case" strategy. This means that every
* process instance that belongs to same case will be bound to a single (case scoped) ksession for it's entire life time.
* Once started, whenever other operations are invoked,this manager will ensure that the correct ksession will be provided.
*
* <br/>
* This implementation supports the following <code>Context</code> implementations:
* <ul>
* <li>ProcessInstanceIdContext</li>
* <li>CaseContext</li>
* </ul>
*/
public class PerCaseRuntimeManager extends AbstractRuntimeManager {
private static final Logger logger = LoggerFactory.getLogger(PerCaseRuntimeManager.class);
private boolean useLocking = Boolean.parseBoolean(System.getProperty("org.jbpm.runtime.manager.pc.lock", "true"));
private SessionFactory factory;
private TaskServiceFactory taskServiceFactory;
private static ThreadLocal<Map<Object, RuntimeEngine>> local = new ThreadLocal<Map<Object, RuntimeEngine>>();
private Mapper mapper;
private AbstractEventSupport<? extends EventListener> caseEventSupport;
public PerCaseRuntimeManager(RuntimeEnvironment environment, SessionFactory factory, TaskServiceFactory taskServiceFactory, String identifier) {
super(environment, identifier);
this.factory = factory;
this.taskServiceFactory = taskServiceFactory;
this.mapper = ((org.kie.internal.runtime.manager.RuntimeEnvironment) environment).getMapper();
this.registry.register(this);
}
@SuppressWarnings("unchecked")
@Override
public RuntimeEngine getRuntimeEngine(Context<?> context) {
if (isClosed()) {
throw new IllegalStateException("Runtime manager " + identifier + " is already closed");
}
checkPermission();
RuntimeEngine runtime = null;
Object contextId = context.getContextId();
if (!(context instanceof ProcessInstanceIdContext || context instanceof CaseContext)) {
logger.warn("ProcessInstanceIdContext or CaseContext shall be used when interacting with PerCase runtime manager");
}
if (engineInitEager) {
KieSession ksession = null;
Long ksessionId = null;
RuntimeEngine localRuntime = findLocalRuntime(contextId);
if (localRuntime != null) {
return localRuntime;
}
synchronized (this) {
ksessionId = mapper.findMapping(context, this.identifier);
if (ksessionId == null) {
ksession = factory.newKieSession();
ksessionId = ksession.getIdentifier();
if (context instanceof CaseContext) {
ksession.execute(new SaveMappingCommand(mapper, context, ksessionId, getIdentifier()));
}
} else {
ksession = factory.findKieSessionById(ksessionId);
}
}
InternalTaskService internalTaskService = (InternalTaskService) taskServiceFactory.newTaskService();
runtime = new RuntimeEngineImpl(ksession, internalTaskService);
((RuntimeEngineImpl) runtime).setManager(this);
((RuntimeEngineImpl) runtime).setContext(context);
configureRuntimeOnTaskService(internalTaskService, runtime);
registerDisposeCallback(runtime, new DisposeSessionTransactionSynchronization(this, runtime));
registerItems(runtime);
attachManager(runtime);
ksession.addEventListener(new MaintainMappingListener(ksessionId, runtime, this.identifier, (String) contextId));
if (context instanceof CaseContext) {
ksession.getEnvironment().set("CaseId", context.getContextId());
} else {
Object contexts = mapper.findContextId(ksession.getIdentifier(), this.identifier);
if (contexts instanceof Collection) {
RuntimeEngine finalRuntimeEngnie = runtime;
KieSession finalKieSession = ksession;
((Collection<Object>) contexts).forEach(o -> {
try {
saveLocalRuntime(null, Long.parseLong(o.toString()), finalRuntimeEngnie);
} catch (NumberFormatException e) {
saveLocalRuntime(o.toString(), null, finalRuntimeEngnie);
finalKieSession.getEnvironment().set("CaseId", o.toString());
}
});
}
}
} else {
RuntimeEngine localRuntime = findLocalRuntime(contextId);
if (localRuntime != null) {
return localRuntime;
}
// lazy initialization of ksession and task service
runtime = new RuntimeEngineImpl(context, new PerCaseInitializer());
((RuntimeEngineImpl) runtime).setManager(this);
}
String caseId = null;
Long processInstanceId = null;
if (context instanceof CaseContext) {
caseId = (String) contextId;
} else if (context instanceof ProcessInstanceIdContext) {
processInstanceId = (Long) contextId;
}
Long ksessionId = mapper.findMapping(context, this.identifier);
createLockOnGetEngine(ksessionId, runtime);
saveLocalRuntime(caseId, processInstanceId, runtime);
((ExecutionErrorManagerImpl)executionErrorManager).createHandler();
return runtime;
}
@Override
public void signalEvent(String type, Object event) {
// first signal with new context in case there are start event with signal
RuntimeEngine runtimeEngine = getRuntimeEngine(ProcessInstanceIdContext.get());
runtimeEngine.getKieSession().signalEvent(type, event);
if (canDispose(runtimeEngine)) {
disposeRuntimeEngine(runtimeEngine);
}
// next find out all instances waiting for given event type
List<String> processInstances = ((InternalMapper) mapper).findContextIdForEvent(type, getIdentifier());
for (String piId : processInstances) {
runtimeEngine = getRuntimeEngine(ProcessInstanceIdContext.get(Long.parseLong(piId)));
runtimeEngine.getKieSession().signalEvent(type, event);
if (canDispose(runtimeEngine)) {
disposeRuntimeEngine(runtimeEngine);
}
}
// process currently active runtime engines
Map<Object, RuntimeEngine> currentlyActive = local.get();
if (currentlyActive != null && !currentlyActive.isEmpty()) {
RuntimeEngine[] activeEngines = currentlyActive.values().toArray(new RuntimeEngine[currentlyActive.size()]);
for (RuntimeEngine engine : activeEngines) {
Context<?> context = ((RuntimeEngineImpl) engine).getContext();
if (context != null && context instanceof ProcessInstanceIdContext && ((ProcessInstanceIdContext) context).getContextId() != null) {
engine.getKieSession().signalEvent(type, event, ((ProcessInstanceIdContext) context).getContextId());
}
}
}
}
@Override
public void validate(KieSession ksession, Context<?> context) throws IllegalStateException {
if (isClosed()) {
throw new IllegalStateException("Runtime manager " + identifier + " is already closed");
}
if (context == null || context.getContextId() == null) {
return;
}
Long ksessionId = mapper.findMapping(context, this.identifier);
if (ksessionId != null && ksession.getIdentifier() != ksessionId) {
throw new IllegalStateException("Invalid session was used for this context " + context);
}
}
@Override
public void disposeRuntimeEngine(RuntimeEngine runtime) {
if (isClosed()) {
throw new IllegalStateException("Runtime manager " + identifier + " is already closed");
}
try {
if (canDispose(runtime)) {
removeLocalRuntime(runtime);
((ExecutionErrorManagerImpl)executionErrorManager).closeHandler();
releaseAndCleanLock(((RuntimeEngineImpl)runtime).getKieSessionId(), runtime);
if (runtime instanceof Disposable) {
// special handling for in memory to not allow to dispose if there is any context in the mapper
if (mapper instanceof InMemoryMapper && ((InMemoryMapper) mapper).hasContext(runtime.getKieSession().getIdentifier())) {
return;
}
((Disposable) runtime).dispose();
}
}
} catch (Exception e) {
releaseAndCleanLock(runtime);
removeLocalRuntime(runtime);
((ExecutionErrorManagerImpl)executionErrorManager).closeHandler();
throw new RuntimeException(e);
}
}
@Override
public void softDispose(RuntimeEngine runtimeEngine) {
super.softDispose(runtimeEngine);
removeLocalRuntime(runtimeEngine);
}
@Override
public void close() {
try {
if (!(taskServiceFactory instanceof LocalTaskServiceFactory)) {
// if it's CDI based (meaning single application scoped bean) we need to unregister context
removeRuntimeFromTaskService();
}
} catch (Exception e) {
// do nothing
}
super.close();
factory.close();
}
public boolean validate(Long ksessionId, Long processInstanceId) {
Long mapped = this.mapper.findMapping(ProcessInstanceIdContext.get(processInstanceId), this.identifier);
if (Objects.equals(mapped, ksessionId)) {
return true;
}
return false;
}
private class MaintainMappingListener extends DefaultProcessEventListener {
private Long ksessionId;
private RuntimeEngine runtime;
private String managerId;
private String caseId;
MaintainMappingListener(Long ksessionId, RuntimeEngine runtime, String managerId, String caseId) {
this.ksessionId = ksessionId;
this.runtime = runtime;
this.managerId = managerId;
this.caseId = caseId;
}
@Override
public void afterProcessCompleted(ProcessCompletedEvent event) {
mapper.removeMapping(new EnvironmentAwareProcessInstanceContext(event.getKieRuntime().getEnvironment(), event.getProcessInstance().getId()), managerId);
removeLocalRuntime(runtime, event.getProcessInstance().getId());
}
@Override
public void beforeProcessStarted(ProcessStartedEvent event) {
mapper.saveMapping(new EnvironmentAwareProcessInstanceContext(event.getKieRuntime().getEnvironment(), event.getProcessInstance().getId()), ksessionId, managerId);
saveLocalRuntime(caseId, event.getProcessInstance().getId(), runtime);
((RuntimeEngineImpl) runtime).setContext(ProcessInstanceIdContext.get(event.getProcessInstance().getId()));
}
}
public SessionFactory getFactory() {
return factory;
}
public void setFactory(SessionFactory factory) {
this.factory = factory;
}
public TaskServiceFactory getTaskServiceFactory() {
return taskServiceFactory;
}
public void setTaskServiceFactory(TaskServiceFactory taskServiceFactory) {
this.taskServiceFactory = taskServiceFactory;
}
public Mapper getMapper() {
return mapper;
}
public void setMapper(Mapper mapper) {
this.mapper = mapper;
}
protected RuntimeEngine findLocalRuntime(Object caseId) {
if (caseId == null) {
return null;
}
Map<Object, RuntimeEngine> map = local.get();
if (map == null) {
return null;
} else {
RuntimeEngine engine = map.get(caseId);
// check if engine is not already disposed as afterCompletion might be issued from another thread
if (engine != null && ((RuntimeEngineImpl) engine).isDisposed()) {
map.remove(caseId);
return null;
}
return engine;
}
}
protected void saveLocalRuntime(Object caseId, Object processInstanceId, RuntimeEngine runtime) {
Map<Object, RuntimeEngine> map = local.get();
if (map == null) {
map = new HashMap<Object, RuntimeEngine>();
local.set(map);
}
if (caseId != null) {
map.put(caseId, runtime);
}
if (processInstanceId != null) {
map.put(processInstanceId, runtime);
}
}
protected void removeLocalRuntime(RuntimeEngine runtime) {
Map<Object, RuntimeEngine> map = local.get();
List<Object> keyToRemoves = new ArrayList<Object>();
if (map != null) {
for (Map.Entry<Object, RuntimeEngine> entry : map.entrySet()) {
if (runtime.equals(entry.getValue())) {
keyToRemoves.add(entry.getKey());
}
}
for (Object keyToRemove : keyToRemoves) {
map.remove(keyToRemove);
}
}
}
protected void removeLocalRuntime(RuntimeEngine runtime, Long processInstanceId) {
Map<Object, RuntimeEngine> map = local.get();
List<Object> keyToRemoves = new ArrayList<Object>();
if (map != null) {
for (Map.Entry<Object, RuntimeEngine> entry : map.entrySet()) {
if (processInstanceId.equals(entry.getKey())) {
keyToRemoves.add(entry.getKey());
}
}
for (Object keyToRemove : keyToRemoves) {
map.remove(keyToRemove);
}
}
}
@Override
public void init() {
TaskContentRegistry.get().addMarshallerContext(getIdentifier(), new ContentMarshallerContext(environment.getEnvironment(), environment.getClassLoader()));
boolean owner = false;
TransactionManager tm = null;
if (environment.usePersistence()){
tm = getTransactionManagerInternal(environment.getEnvironment());
owner = tm.begin();
}
try {
// need to init one session to bootstrap all case - such as start timers
KieSession initialKsession = factory.newKieSession();
// there is a need to call getProcessRuntime otherwise the start listeners are not registered
initialKsession.execute(new ExecutableCommand<Void>() {
private static final long serialVersionUID = 1L;
@Override
public Void execute(org.kie.api.runtime.Context context) {
KieSession ksession = ((RegistryContext) context).lookup( KieSession.class );
((InternalKnowledgeRuntime) ksession).getProcessRuntime();
return null;
}
});
initialKsession.execute(new DestroyKSessionCommand(initialKsession, this));
if (!"false".equalsIgnoreCase(System.getProperty("org.jbpm.rm.init.timer"))) {
if (mapper instanceof JPAMapper) {
List<Long> ksessionsToInit = ((JPAMapper) mapper).findKSessionToInit(this.identifier);
for (Long id : ksessionsToInit) {
initialKsession = factory.findKieSessionById(id);
initialKsession.execute(new DisposeKSessionCommand(initialKsession, this));
}
}
}
if (tm != null) {
tm.commit(owner);
}
} catch (Exception e) {
if (tm != null) {
tm.rollback(owner);
}
throw new RuntimeException("Exception while initializing runtime manager " + this.identifier, e);
}
}
@Override
public void activate() {
super.activate();
// need to init one session to bootstrap all case - such as start timers
KieSession initialKsession = factory.newKieSession();
initialKsession.execute(new DestroyKSessionCommand(initialKsession, this));
}
@Override
public void deactivate() {
super.deactivate();
}
public void destroyCase(CaseContext caseContext) {
KieSession kieSession = null;
RuntimeEngine localRuntime = findLocalRuntime(caseContext.getContextId());
if (localRuntime != null) {
kieSession = localRuntime.getKieSession();
} else {
Long ksessionId = mapper.findMapping(caseContext, this.identifier);
if (ksessionId != null) {
kieSession = factory.findKieSessionById(ksessionId);
}
}
List<ExecutableCommand<?>> cmds = new ArrayList<>();
RemoveMappingCommand removeMapping = new RemoveMappingCommand(mapper, caseContext, getIdentifier());
cmds.add(removeMapping);
DestroyKSessionCommand destroy = new DestroyKSessionCommand(kieSession, this);
cmds.add(destroy);
BatchExecutionCommand batchCmd = new BatchExecutionCommandImpl(cmds);
kieSession.execute(batchCmd);
}
public AbstractEventSupport<? extends EventListener> getCaseEventSupport() {
return caseEventSupport;
}
public void setCaseEventSupport(AbstractEventSupport<? extends EventListener> caseEventSupport) {
this.caseEventSupport = caseEventSupport;
}
private static class DestroyKSessionCommand implements ExecutableCommand<Void> {
private static final long serialVersionUID = 1L;
private KieSession initialKsession;
private AbstractRuntimeManager manager;
public DestroyKSessionCommand(KieSession initialKsession, AbstractRuntimeManager manager) {
this.initialKsession = initialKsession;
this.manager = manager;
}
@Override
public Void execute(org.kie.api.runtime.Context context) {
TransactionManager tm = (TransactionManager) initialKsession.getEnvironment().get(EnvironmentName.TRANSACTION_MANAGER);
if (manager.hasEnvironmentEntry("IS_JTA_TRANSACTION", false)) {
if (initialKsession instanceof CommandBasedStatefulKnowledgeSession) {
ExecutableRunner commandService = ((CommandBasedStatefulKnowledgeSession) initialKsession).getRunner();
((SingleSessionCommandService) commandService).destroy();
} else {
((RegistryContext) context).lookup( KieSession.class ).destroy();
}
return null;
}
if (tm != null && tm.getStatus() != TransactionManager.STATUS_NO_TRANSACTION && tm.getStatus() != TransactionManager.STATUS_ROLLEDBACK && tm.getStatus() != TransactionManager.STATUS_COMMITTED) {
TransactionManagerHelper.registerTransactionSyncInContainer(tm, new OrderedTransactionSynchronization(5, "PCRM-" + initialKsession.getIdentifier()) {
@Override
public void beforeCompletion() {
if (initialKsession instanceof CommandBasedStatefulKnowledgeSession) {
ExecutableRunner commandService = ((CommandBasedStatefulKnowledgeSession) initialKsession).getRunner();
((SingleSessionCommandService) commandService).destroy();
}
}
@Override
public void afterCompletion(int arg0) {
initialKsession.dispose();
}
});
} else {
initialKsession.destroy();
}
return null;
}
}
private static class DisposeKSessionCommand implements ExecutableCommand<Void> {
private static final long serialVersionUID = 1L;
private KieSession initialKsession;
private AbstractRuntimeManager manager;
public DisposeKSessionCommand(KieSession initialKsession, AbstractRuntimeManager manager) {
this.initialKsession = initialKsession;
this.manager = manager;
}
@Override
public Void execute(org.kie.api.runtime.Context context) {
if (manager.hasEnvironmentEntry("IS_JTA_TRANSACTION", false)) {
initialKsession.dispose();
return null;
}
TransactionManager tm = (TransactionManager) initialKsession.getEnvironment().get(EnvironmentName.TRANSACTION_MANAGER);
if (tm != null && tm.getStatus() != TransactionManager.STATUS_NO_TRANSACTION && tm.getStatus() != TransactionManager.STATUS_ROLLEDBACK && tm.getStatus() != TransactionManager.STATUS_COMMITTED) {
TransactionManagerHelper.registerTransactionSyncInContainer(tm, new OrderedTransactionSynchronization(5, "PPIRM-" + initialKsession.getIdentifier()) {
@Override
public void beforeCompletion() {
}
@Override
public void afterCompletion(int arg0) {
initialKsession.dispose();
}
});
} else {
initialKsession.dispose();
}
return null;
}
}
private static class SaveMappingCommand implements ExecutableCommand<Void> {
private static final long serialVersionUID = 1L;
private Mapper mapper;
private Context<?> caseContext;
private Long ksessionId;
private String ownerId;
public SaveMappingCommand(Mapper mapper, Context<?> caseContext, Long ksessionId, String ownerId) {
this.mapper = mapper;
this.caseContext = caseContext;
this.ksessionId = ksessionId;
this.ownerId = ownerId;
}
@Override
public Void execute(org.kie.api.runtime.Context context) {
mapper.saveMapping(caseContext, ksessionId, ownerId);
return null;
}
}
private static class RemoveMappingCommand implements ExecutableCommand<Void> {
private static final long serialVersionUID = 1L;
private Mapper mapper;
private Context<?> caseContext;
private String ownerId;
public RemoveMappingCommand(Mapper mapper, Context<?> caseContext, String ownerId) {
this.mapper = mapper;
this.caseContext = caseContext;
this.ownerId = ownerId;
}
@Override
public Void execute(org.kie.api.runtime.Context context) {
mapper.removeMapping(caseContext, ownerId);
return null;
}
}
private class PerCaseInitializer implements RuntimeEngineInitlializer {
@SuppressWarnings("unchecked")
@Override
public KieSession initKieSession(Context<?> context, InternalRuntimeManager manager, RuntimeEngine engine) {
Object contextId = context.getContextId();
if (contextId == null) {
contextId = manager.getIdentifier();
}
KieSession ksession = null;
Long ksessionId = null;
RuntimeEngine localRuntime = ((PerCaseRuntimeManager) manager).findLocalRuntime(contextId);
if (localRuntime != null && ((RuntimeEngineImpl) engine).internalGetKieSession() != null) {
return localRuntime.getKieSession();
}
synchronized (manager) {
ksessionId = mapper.findMapping(context, manager.getIdentifier());
if (ksessionId == null) {
ksession = factory.newKieSession();
ksessionId = ksession.getIdentifier();
if (context instanceof CaseContext) {
ksession.execute(new SaveMappingCommand(mapper, context, ksessionId, manager.getIdentifier()));
}
} else {
ksession = factory.findKieSessionById(ksessionId);
}
}
((RuntimeEngineImpl) engine).internalSetKieSession(ksession);
registerItems(engine);
attachManager(engine);
registerDisposeCallback(engine, new DisposeSessionTransactionSynchronization(manager, engine));
ksession.addEventListener(new MaintainMappingListener(ksessionId, engine, manager.getIdentifier(), contextId.toString()));
if (context instanceof CaseContext) {
ksession.getEnvironment().set("CaseId", context.getContextId());
} else {
Object contexts = mapper.findContextId(ksession.getIdentifier(), manager.getIdentifier());
if (contexts instanceof Collection) {
KieSession finalKieSession = ksession;
((Collection<Object>) contexts).forEach(o -> {
try {
saveLocalRuntime(null, Long.parseLong(o.toString()), engine);
} catch (NumberFormatException e) {
saveLocalRuntime(o.toString(), null, engine);
finalKieSession.getEnvironment().set("CaseId", o.toString());
}
});
}
}
return ksession;
}
@Override
public TaskService initTaskService(Context<?> context, InternalRuntimeManager manager, RuntimeEngine engine) {
InternalTaskService internalTaskService = (InternalTaskService) taskServiceFactory.newTaskService();
registerDisposeCallback(engine, new DisposeSessionTransactionSynchronization(manager, engine));
configureRuntimeOnTaskService(internalTaskService, engine);
return internalTaskService;
}
}
@Override
protected boolean isUseLocking() {
return useLocking;
}
}