package org.jbpm.jbpm1071; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jbpm.JbpmConfiguration; import org.jbpm.JbpmContext; import org.jbpm.db.AbstractDbTestCase; import org.jbpm.graph.def.ProcessDefinition; import org.jbpm.graph.def.Node.NodeType; import org.jbpm.graph.exe.Comment; import org.jbpm.graph.exe.ProcessInstance; import org.jbpm.graph.exe.Token; import edu.emory.mathcs.backport.java.util.concurrent.Semaphore; /** * Possible problem in concurrent signaling from multiple threads. * * @see <a href='https://jira.jboss.org/jira/browse/JBPM-1071'>JBPM-1071</a> */ public class JBPM1071Test extends AbstractDbTestCase { static final int nbrOfThreads = 4; static final int nbrOfIterations = 10; protected void setUp() throws Exception { super.setUp(); ProcessDefinition processDefinition = ProcessDefinition.parseXmlString("<process-definition name='jbpm1071'>" + " <start-state name='start'>" + " <transition to='end'/>" + " </start-state>" + " <end-state name='end' />" + "</process-definition>"); deployProcessDefinition(processDefinition); } public void testLocking() { // the process will be executed in 2 separate transactions: // Transaction 1 will create the process instance and position // the root token in the start state // Transaction 2 will signal the process instance while it is in the // start state, and that signal will bring the process to it's end state. // multiple competing threads will be set up for the second transaction for (int i = 0; i < nbrOfIterations; i++) { long processInstanceId = jbpmContext.newProcessInstanceForUpdate("jbpm1071").getId(); newTransaction(); // create a bunch of threads that will all wait on the // semaphore before they will try to signal the same process instance Semaphore semaphore = new Semaphore(0); List threads = startThreads(semaphore, processInstanceId); // release all the threads semaphore.release(nbrOfThreads); // wait for all threads to finish joinAllThreads(threads); // check that only 1 of those threads committed List results = session.createCriteria(Comment.class).list(); assertEquals(results.toString(), 1, results.size()); // delete the comment session.delete(results.get(0)); // check that the process instance has ended ProcessInstance processInstance = jbpmContext.loadProcessInstance(processInstanceId); assertTrue("expected " + processInstance + " to have ended", processInstance.hasEnded()); } } private List startThreads(Semaphore semaphore, long processInstanceId) { List threads = new ArrayList(); for (int i = 0; i < nbrOfThreads; i++) { Thread thread = new Thread(new Signaller(semaphore, jbpmConfiguration, processInstanceId)); thread.start(); threads.add(thread); } return threads; } private void joinAllThreads(List threads) { for (Iterator i = threads.iterator(); i.hasNext();) { Thread thread = (Thread) i.next(); try { thread.join(10000); } catch (InterruptedException e) { fail("join interrupted"); } } } static class Signaller implements Runnable { private final Semaphore semaphore; private final JbpmConfiguration jbpmConfiguration; private final long processInstanceId; Signaller(Semaphore semaphore, JbpmConfiguration jbpmConfiguration, long processInstanceId) { this.semaphore = semaphore; this.jbpmConfiguration = jbpmConfiguration; this.processInstanceId = processInstanceId; } public void run() { try { // first wait until the all threads are released at once in the // method testLocking semaphore.acquire(); } catch (InterruptedException e) { fail("semaphore waiting got interrupted"); } // after a thread is released (=notified), it will try to load the process // instance, // signal it and then commit the transaction String threadName = Thread.currentThread().getName(); Log log = LogFactory.getLog(JBPM1071Test.class); JbpmContext jbpmContext = jbpmConfiguration.createJbpmContext(); try { ProcessInstance processInstance = jbpmContext.loadProcessInstance(processInstanceId); Token rootToken = processInstance.getRootToken(); // check whether root token is still in start state if (rootToken.getNode().getNodeType() != NodeType.StartState) { jbpmContext.setRollbackOnly(); return; } // move to end state processInstance.signal(); // add a comment to see which thread won Comment comment = new Comment(threadName + " committed"); rootToken.addComment(comment); jbpmContext.save(processInstance); } catch (RuntimeException e) { jbpmContext.setRollbackOnly(); log.debug(threadName + " rolled back", e); } finally { try { jbpmContext.close(); } catch (RuntimeException e) { log.debug(threadName + " failed to close jbpm context", e); } } } } }