/*
* Copyright 2015 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.
*
* 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.test.functional.timer;
import org.drools.core.command.SingleSessionCommandService;
import org.drools.core.command.impl.CommandBasedStatefulKnowledgeSession;
import org.hibernate.StaleObjectStateException;
import org.jbpm.process.core.timer.GlobalSchedulerService;
import org.jbpm.process.core.timer.impl.ThreadPoolSchedulerService;
import org.jbpm.services.task.exception.PermissionDeniedException;
import org.jbpm.services.task.identity.JBossUserGroupCallbackImpl;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.kie.api.io.ResourceType;
import org.kie.api.runtime.manager.RuntimeEngine;
import org.kie.api.runtime.manager.RuntimeEnvironment;
import org.kie.api.runtime.manager.RuntimeEnvironmentBuilder;
import org.kie.api.runtime.manager.RuntimeManager;
import org.kie.api.runtime.manager.RuntimeManagerFactory;
import org.kie.api.runtime.manager.audit.AuditService;
import org.kie.api.runtime.manager.audit.ProcessInstanceLog;
import org.kie.api.runtime.process.ProcessInstance;
import org.kie.api.task.TaskService;
import org.kie.api.task.model.Group;
import org.kie.api.task.model.Status;
import org.kie.api.task.model.TaskSummary;
import org.kie.api.task.model.User;
import org.kie.internal.io.ResourceFactory;
import org.kie.internal.runtime.manager.context.EmptyContext;
import org.kie.internal.runtime.manager.context.ProcessInstanceIdContext;
import org.kie.internal.task.api.InternalTaskService;
import org.kie.internal.task.api.TaskModelProvider;
import org.kie.internal.task.api.UserGroupCallback;
import org.kie.internal.task.api.model.InternalOrganizationalEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.naming.InitialContext;
import javax.persistence.EntityManagerFactory;
import javax.persistence.OptimisticLockException;
import javax.persistence.Persistence;
import javax.transaction.UserTransaction;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
public class ConcurrentGlobalTimerServiceTest extends TimerBaseTest {
private static final Logger logger = LoggerFactory.getLogger(ConcurrentGlobalTimerServiceTest.class);
private long maxWaitTime = 60*1000; // max wait to complete operation is set to 60 seconds to avoid build hangs
private int nbThreadsProcess = 10;
private int nbThreadsTask = 10;
private transient int completedStart = 0;
private transient int completedTask = 0;
private int wait = 2;
private UserGroupCallback userGroupCallback;
private GlobalSchedulerService globalScheduler;
private RuntimeManager manager;
private EntityManagerFactory emf;
@Before
public void setup() {
Properties properties= new Properties();
properties.setProperty("mary", "HR");
properties.setProperty("john", "HR");
userGroupCallback = new JBossUserGroupCallbackImpl(properties);
globalScheduler = new ThreadPoolSchedulerService(1);
emf = Persistence.createEntityManagerFactory("org.jbpm.test.persistence");
}
@After
public void teardown() {
globalScheduler.shutdown();
if (manager != null) {
manager.close();
}
emf.close();
}
@Test
public void testSessionPerProcessInstance() throws Exception {
RuntimeEnvironment environment = RuntimeEnvironmentBuilder.Factory.get()
.newDefaultBuilder()
.entityManagerFactory(emf)
.userGroupCallback(userGroupCallback)
.addAsset(ResourceFactory.newClassPathResource("org/jbpm/test/functional/timer/IntermediateCatchEventTimerCycleWithHT.bpmn2"), ResourceType.BPMN2)
.schedulerService(globalScheduler)
.get();
long startTimeStamp = System.currentTimeMillis();
long maxEndTime = startTimeStamp + maxWaitTime;
manager = RuntimeManagerFactory.Factory.get().newPerProcessInstanceRuntimeManager(environment);
// prepare task service with users and groups
RuntimeEngine engine = manager.getRuntimeEngine(EmptyContext.get());
TaskService taskService = engine.getTaskService();
Group grouphr = TaskModelProvider.getFactory().newGroup();
((InternalOrganizationalEntity) grouphr).setId("HR");
User mary = TaskModelProvider.getFactory().newUser();
((InternalOrganizationalEntity) mary).setId("mary");
User john = TaskModelProvider.getFactory().newUser();
((InternalOrganizationalEntity) john).setId("john");
((InternalTaskService)taskService).addGroup(grouphr);
((InternalTaskService)taskService).addUser(mary);
((InternalTaskService)taskService).addUser(john);
manager.disposeRuntimeEngine(engine);
completedStart = 0;
for (int i=0; i<nbThreadsProcess; i++) {
new StartProcessPerProcessInstanceRunnable(manager, i).run();
}
completedTask = 0;
for (int i=0; i<nbThreadsTask; i++) {
new Thread(new CompleteTaskPerProcessInstanceRunnable(manager, i)).start();
}
while (completedStart < nbThreadsProcess || completedTask < nbThreadsTask) {
Thread.sleep(100);
if (System.currentTimeMillis() > maxEndTime) {
fail("Failure, did not finish in time most likely hanging");
}
}
//make sure all process instance were completed
engine = manager.getRuntimeEngine(EmptyContext.get());
AuditService logService = engine.getAuditService();
//active
List<? extends ProcessInstanceLog> logs = logService.findActiveProcessInstances("IntermediateCatchEvent");
assertNotNull(logs);
for (ProcessInstanceLog log : logs) {
logger.debug("Left over {}", log.getProcessInstanceId());
}
assertEquals(0, logs.size());
// completed
logs = logService.findProcessInstances("IntermediateCatchEvent");
assertNotNull(logs);
assertEquals(nbThreadsProcess, logs.size());
manager.disposeRuntimeEngine(engine);
logger.debug("Done");
}
private void testStartProcess(RuntimeEngine runtime) throws Exception {
synchronized((SingleSessionCommandService) ((CommandBasedStatefulKnowledgeSession) runtime.getKieSession()).getRunner()) {
UserTransaction ut = (UserTransaction) new InitialContext().lookup( "java:comp/UserTransaction" );
try {
ut.begin();
logger.debug("Starting process on ksession {}", runtime.getKieSession().getIdentifier());
Map<String, Object> params = new HashMap<String, Object>();
params.put("x", "R2/PT1S");
ProcessInstance processInstance = runtime.getKieSession().startProcess("IntermediateCatchEvent", params);
logger.debug("Started process instance {} on ksession {}", processInstance.getId(), runtime.getKieSession().getIdentifier());
ut.commit();
} catch (Exception ex) {
ut.rollback();
throw ex;
}
}
}
private boolean testCompleteTaskByProcessInstance(RuntimeManager manager, RuntimeEngine runtime, long piId) throws InterruptedException, Exception {
boolean result = false;
List<Status> statusses = new ArrayList<Status>();
statusses.add(Status.Reserved);
List<TaskSummary> tasks = null;
tasks = runtime.getTaskService().getTasksByStatusByProcessInstanceId(piId, statusses, "en-UK");
if (tasks.isEmpty()) {
logger.debug("Task thread found no tasks for piId {}" + piId);
Thread.sleep(1000);
} else {
long taskId = tasks.get(0).getId();
logger.debug("Completing task {} piId {}", taskId, piId);
boolean success = false;
try {
runtime.getTaskService().start(taskId, "john");
success = true;
if (success) {
runtime.getTaskService().complete(taskId, "john", null);
logger.debug("Completed task {} piID {}", taskId, piId);
result = true;
}
} catch (PermissionDeniedException e) {
// TODO can we avoid these by doing it all in one transaction?
logger.debug("Task thread was too late for starting task {} piId {}", taskId, piId);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
return result;
}
private boolean testRetryCompleteTaskByProcessInstance(RuntimeManager manager, RuntimeEngine runtime, long piId) throws InterruptedException, Exception {
boolean result = false;
List<Status> statusses = new ArrayList<Status>();
statusses.add(Status.InProgress);
List<TaskSummary> tasks = null;
tasks = runtime.getTaskService().getTasksByStatusByProcessInstanceId(piId, statusses, "en-UK");
if (tasks.isEmpty()) {
logger.debug("Retry : Task thread found no tasks for piId {}", piId);
Thread.sleep(1000);
} else {
long taskId = tasks.get(0).getId();
logger.debug("Retry : Completing task {} piId {}", taskId, piId);
try {
runtime.getTaskService().complete(taskId, "john", null);
logger.debug("Retry : Completed task {} piId {}", taskId, piId);
result = true;
} catch (PermissionDeniedException e) {
// TODO can we avoid these by doing it all in one transaction?
logger.debug("Task thread was too late for starting task {} piId {}", taskId, piId);
} catch (Exception e) {
throw e;
}
}
return result;
}
public class StartProcessPerProcessInstanceRunnable implements Runnable {
private RuntimeManager manager;
private int counter;
public StartProcessPerProcessInstanceRunnable(RuntimeManager manager, int counter) {
this.manager = manager;
this.counter = counter;
}
public void run() {
try {
RuntimeEngine runtime = manager.getRuntimeEngine(ProcessInstanceIdContext.get());
testStartProcess(runtime);
manager.disposeRuntimeEngine(runtime);
completedStart++;
} catch (Throwable t) {
t.printStackTrace();
}
}
}
public class CompleteTaskPerProcessInstanceRunnable implements Runnable {
private RuntimeManager manager;
private int counter;
public CompleteTaskPerProcessInstanceRunnable(RuntimeManager manager, int counter) {
this.manager = manager;
this.counter = counter;
}
public void run() {
try {
// wait for amount of time timer expires and plus 1s initially
Thread.sleep(wait * 1000 + 1000);
long processInstanceId = counter+1;
for (int y = 0; y<wait; y++) {
RuntimeEngine runtime = manager.getRuntimeEngine(ProcessInstanceIdContext.get(processInstanceId));
try {
testCompleteTaskByProcessInstance(manager, runtime, processInstanceId);
} catch (Throwable e) {
if (checkOptimiticLockException(e)) {
logger.debug("{} retrying for process instance {}", counter, processInstanceId);
manager.disposeRuntimeEngine(runtime);
runtime = manager.getRuntimeEngine(ProcessInstanceIdContext.get(processInstanceId));
testRetryCompleteTaskByProcessInstance(manager, runtime, processInstanceId);
} else {
throw e;
}
}
manager.disposeRuntimeEngine(runtime);
}
completedTask++;
} catch (Throwable t) {
t.printStackTrace();
}
}
}
public static boolean checkOptimiticLockException(Throwable e) {
Throwable rootCause = e.getCause();
while (rootCause != null) {
if ((rootCause instanceof OptimisticLockException || rootCause instanceof StaleObjectStateException) ){
return true;
}
rootCause = rootCause.getCause();
}
if (e instanceof InvocationTargetException) {
return checkOptimiticLockException(((InvocationTargetException) e).getTargetException());
}
return false;
}
}