/*
* 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.kie.spring.jbpm;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import javax.persistence.LockTimeoutException;
import javax.persistence.PessimisticLockException;
import org.jbpm.process.audit.AuditLogService;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
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.RuntimeManager;
import org.kie.api.runtime.manager.audit.ProcessInstanceLog;
import org.kie.api.runtime.process.ProcessInstance;
import org.kie.internal.runtime.manager.context.EmptyContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
import org.springframework.transaction.support.DefaultTransactionDefinition;
@RunWith(Parameterized.class)
public class PessimisticLockingSpringTest extends AbstractJbpmSpringParameterizedTest {
private static final Logger log = LoggerFactory.getLogger(PessimisticLockingSpringTest.class);
@Parameterized.Parameters(name = "{index}: {0}")
public static Collection<Object[]> contextPath() {
Object[][] data = new Object[][] {
{ PESSIMISTIC_LOCK_LOCAL_EM_PATH, EmptyContext.get() },
{ PESSIMISTIC_LOCK_LOCAL_EMF_PATH, EmptyContext.get() },
};
return Arrays.asList(data);
};
@Rule
public TestRule watcher = new TestWatcher() {
protected void starting(Description description) {
log.debug(">>> " + description.getMethodName() + " <<<");
};
protected void finished(Description description) {
log.debug("<<< DONE >>>");
};
};
public PessimisticLockingSpringTest(String contextPath, Context<?> runtimeManagerContext) {
super(contextPath, runtimeManagerContext);
}
@Test
public void testPessimisticLock() throws Exception {
RuntimeManager manager = getManager();
final AbstractPlatformTransactionManager transactionManager = getTransactionManager();
AuditLogService logService = getLogService();
final DefaultTransactionDefinition defTransDefinition = new DefaultTransactionDefinition();
final List<Exception> exceptions = new ArrayList<Exception>();
RuntimeEngine engine = getEngine();
final KieSession ksession = getKieSession();
final ProcessInstance processInstance = ksession.startProcess(HUMAN_TASK_PROCESS_ID);
final ProcessInstanceStatus abortedProcessInstanceStatus = new ProcessInstanceStatus();
final CountDownLatch txAcquiredSignal = new CountDownLatch(1);
final CountDownLatch pessLockExceptionSignal = new CountDownLatch(1);
final CountDownLatch threadsAreDoneLatch = new CountDownLatch(2);
Thread t1 = new Thread() {
@Override
public void run() {
TransactionStatus status = transactionManager.getTransaction(defTransDefinition);
log.debug("Attempting to abort to lock process instance for 3 secs ");
// getProcessInstance does not lock reliably so let's make a change that actually does something to the entity
ksession.abortProcessInstance(processInstance.getId());
// let thread 2 start once we have the transaction
txAcquiredSignal.countDown();
try {
// keep the lock until thread 2 let's us know it's done
pessLockExceptionSignal.await();
} catch (InterruptedException e) {
// do nothing
}
log.debug("Commited process instance aborting after 3 secs");
transactionManager.commit(status);
// let main test thread know we're done
threadsAreDoneLatch.countDown();
}
};
// Trying to retrieve process instance in second thread.
// Should throw PessimisticLockException because we are trying to get write lock on process instance which already have lock
Thread t2 = new Thread() {
@Override
public void run() {
try {
// wait for thread 1 to tell us it has the lock
txAcquiredSignal.await();
} catch( InterruptedException e ) {
// do nothing
}
log.debug("Trying to get process instance - should fail because process instance is locked or wait until thread 1 finish and return null because process instance is deleted.");
try {
ProcessInstance abortedProcessInstance = ksession.getProcessInstance(processInstance.getId(), true);
if(abortedProcessInstance == null) {
abortedProcessInstanceStatus.setAbortedProcessInstance(true);
}
log.debug("Get request worked well");
} catch (Exception e) {
log.debug("Get request failed with error {}", e.getMessage());
exceptions.add(e);
} finally {
// Tell thread 1 that we're done
pessLockExceptionSignal.countDown();
}
// let main test thread know we're done
threadsAreDoneLatch.countDown();
}
};
t1.start();
t2.start();
// wait for both threads to finish!
threadsAreDoneLatch.await();
// If process instance read by thread 2 is aborted then it means that database transaction timeout is bigger than waiting time set here and
// getProcessInstance was waiting for thread 1 to finish its work.
// Therefore exception list should be empty.
if(abortedProcessInstanceStatus.isAbortedProcessInstance()) {
assertEquals(0, exceptions.size());
} else {
// Otherwise database transaction timeout should be lower than waiting time set and thread 2 should throw PessimisticLockException or
// LockTimeoutException.
assertEquals(1, exceptions.size());
assertThat(exceptions.get(0).getClass().getName(), anyOf(equalTo(PessimisticLockException.class.getName()), equalTo(LockTimeoutException.class.getName())));
}
TransactionStatus status = transactionManager.getTransaction(defTransDefinition);
ProcessInstanceLog instanceLog = logService.findProcessInstance(processInstance.getId());
transactionManager.commit(status);
assertNotNull(instanceLog);
assertEquals(ProcessInstance.STATE_ABORTED, instanceLog.getStatus().intValue());
manager.disposeRuntimeEngine(engine);
}
/**
* Helper class to pass information about aborted process instance between threads.
*/
private class ProcessInstanceStatus {
private boolean abortedProcessInstance = false;
public boolean isAbortedProcessInstance() {
return abortedProcessInstance;
}
public void setAbortedProcessInstance(boolean abortedProcessInstance) {
this.abortedProcessInstance = abortedProcessInstance;
}
}
}