AnsweredAssumed Answered

Activiti Multi Instance tasks and jobs  for recurrent tasks

Question asked by koallen on Jan 10, 2013
Latest reply on Jun 15, 2013 by koallen
We are looking to implement activiti 5.11 , I have a use case that I am not sure how to implement in activiti.

We need to schedule recurrent visits by a nurse to visit a patient at home, this may take place over a 30 day period for example. We also need to send multiple reminders to the nurse to submit the notes for each visit before transitioning to the next activity that requires the nurse to submit a missed visit report if the reminders are ignored.

The expected flow is that multiple instances are created and the timer for each instance should expire at different times, each triggering a separate escalation task based on whether the nurse submitted her notes before the timers expire .So, there might be five visits scheduled but escalation required for just two visits.

I set this up using the timecycle tag of timerevent definition with the intent that the escalation task is created every time the cycle is invoked.

Currently, the activity is set as multi-instance and the number of instances is passed as a process variable.

The test currently sets the number of requested instances at 5, and the number of instances are correctly created.

My expectation is that a timer is created for each created instance and that a job is created for each instance.

When I run the test, I see that there is just one job in the db rather than five jobs for each of the created instances.The job runs once and the test ends.

I am new to activiti after working with jbpm 3 for a number of years, what I would like to know is if my projected workflow is possible with activiti or whether it is possible to work directly with the Engine API to create the multiple instances myself and create the jobs as needed.

My diagram ,bpm20.xml file and test case are below.

I am not able to upload any attachments to this post, because I am getting errors when I try to do so,so, I cant provide the diagram


Thanks



<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
  <process id="visitsEmailReport" name="Visits Email" isExecutable="true">
    <startEvent id="startevent1" name="Start"></startEvent>
    <userTask id="visitPatient" name="VisitPatient">
      <extensionElements>
        <activiti:taskListener event="create" class="com.hha.curatio.activiti.listeners.VisitsEscalatorListener"></activiti:taskListener>
      </extensionElements>
      <multiInstanceLoopCharacteristics isSequential="false">
        <loopCardinality>${nrOfVisits}</loopCardinality>
      </multiInstanceLoopCharacteristics>
    </userTask>
    <sequenceFlow id="startVisitsflow" name="Start Visits" sourceRef="startevent1" targetRef="visitPatient"></sequenceFlow>
    <boundaryEvent id="boundarytimer1" name="Timer" attachedToRef="visitPatient" cancelActivity="false">
      <timerEventDefinition>
        <timeCycle>R5/PT45S</timeCycle>
      </timerEventDefinition>
    </boundaryEvent>
    <endEvent id="endevent1" name="End"></endEvent>
    <parallelGateway id="parallelgateway1" name="Parallel Gateway"></parallelGateway>
    <sequenceFlow id="flow3" sourceRef="parallelgateway1" targetRef="endevent1"></sequenceFlow>
    <userTask id="usertask1" name="Escalate">
      <extensionElements>
        <activiti:taskListener event="create" class="com.hha.curatio.activiti.listeners.VisitsEscalatorListener"></activiti:taskListener>
      </extensionElements>
    </userTask>
    <exclusiveGateway id="exgwvisits" name="Exclusive Gateway"></exclusiveGateway>
    <userTask id="usertask2" name="Missed Visit Report">
      <extensionElements>
        <activiti:taskListener event="create" class="com.hha.curatio.activiti.listeners.VisitsEscalatorListener"></activiti:taskListener>
      </extensionElements>
    </userTask>
    <sequenceFlow id="completedVisitflow" name="Completed Visit Flow" sourceRef="exgwvisits" targetRef="parallelgateway1">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${missedVisitsReport == false}]]></conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="flow9" sourceRef="usertask1" targetRef="parallelgateway1"></sequenceFlow>
    <sequenceFlow id="flow10" sourceRef="usertask2" targetRef="parallelgateway1"></sequenceFlow>
    <sequenceFlow id="flowtoMissedVisit" sourceRef="exgwvisits" targetRef="usertask2">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${missedVisitsReport == false}]]></conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="flowtoEscalate" sourceRef="boundarytimer1" targetRef="usertask1"></sequenceFlow>
    <sequenceFlow id="flow11" sourceRef="visitPatient" targetRef="exgwvisits"></sequenceFlow>
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_visitsEmailReport">
    <bpmndi:BPMNPlane bpmnElement="visitsEmailReport" id="BPMNPlane_visitsEmailReport">
      <bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
        <omgdc:Bounds height="35.0" width="35.0" x="40.0" y="80.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="visitPatient" id="BPMNShape_visitPatient">
        <omgdc:Bounds height="55.0" width="105.0" x="228.0" y="64.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="boundarytimer1" id="BPMNShape_boundarytimer1">
        <omgdc:Bounds height="30.0" width="30.0" x="320.0" y="74.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
        <omgdc:Bounds height="35.0" width="35.0" x="263.0" y="400.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="parallelgateway1" id="BPMNShape_parallelgateway1">
        <omgdc:Bounds height="40.0" width="40.0" x="260.0" y="300.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask1" id="BPMNShape_usertask1">
        <omgdc:Bounds height="55.0" width="105.0" x="562.0" y="61.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask2" id="BPMNShape_usertask2">
        <omgdc:Bounds height="55.0" width="105.0" x="20.0" y="173.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="exgwvisits" id="BPMNShape_exgwvisits">
        <omgdc:Bounds height="40.0" width="40.0" x="250.0" y="180.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3">
        <omgdi:waypoint x="280.0" y="340.0"></omgdi:waypoint>
        <omgdi:waypoint x="280.0" y="400.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow9" id="BPMNEdge_flow9">
        <omgdi:waypoint x="614.0" y="116.0"></omgdi:waypoint>
        <omgdi:waypoint x="612.0" y="319.0"></omgdi:waypoint>
        <omgdi:waypoint x="300.0" y="320.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow10" id="BPMNEdge_flow10">
        <omgdi:waypoint x="72.0" y="228.0"></omgdi:waypoint>
        <omgdi:waypoint x="72.0" y="319.0"></omgdi:waypoint>
        <omgdi:waypoint x="260.0" y="320.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="completedVisitflow" id="BPMNEdge_completedVisitflow">
        <omgdi:waypoint x="270.0" y="220.0"></omgdi:waypoint>
        <omgdi:waypoint x="280.0" y="300.0"></omgdi:waypoint>
        <bpmndi:BPMNLabel>
          <omgdc:Bounds height="42.0" width="100.0" x="-15.0" y="-20.0"></omgdc:Bounds>
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="startVisitsflow" id="BPMNEdge_startVisitsflow">
        <omgdi:waypoint x="75.0" y="97.0"></omgdi:waypoint>
        <omgdi:waypoint x="228.0" y="91.0"></omgdi:waypoint>
        <bpmndi:BPMNLabel>
          <omgdc:Bounds height="14.0" width="100.0" x="10.0" y="0.0"></omgdc:Bounds>
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow11" id="BPMNEdge_flow11">
        <omgdi:waypoint x="280.0" y="119.0"></omgdi:waypoint>
        <omgdi:waypoint x="270.0" y="180.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flowtoEscalate" id="BPMNEdge_flowtoEscalate">
        <omgdi:waypoint x="350.0" y="89.0"></omgdi:waypoint>
        <omgdi:waypoint x="562.0" y="88.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flowtoMissedVisit" id="BPMNEdge_flowtoMissedVisit">
        <omgdi:waypoint x="250.0" y="200.0"></omgdi:waypoint>
        <omgdi:waypoint x="125.0" y="200.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>


/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.hha.curatio.activiti;

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import org.activiti.engine.ActivitiException;
import org.activiti.engine.ManagementService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.impl.jobexecutor.JobExecutor;
import org.activiti.engine.impl.util.ClockUtil;
import org.activiti.engine.runtime.Job;
import org.activiti.engine.runtime.JobQuery;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.activiti.engine.test.ActivitiRule;
import org.activiti.engine.test.Deployment;
import org.activiti.spring.SpringProcessEngineConfiguration;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.junit.Assert.*;


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"activiti-context.xml"})
public class VisitsEmailReportTest {

    @Autowired
    @Rule
    public ActivitiRule actRule;

    @Autowired
    private RuntimeService rtService;
    @Autowired
    private TaskService tskService;
    @Autowired
    private ManagementService mgtService;

    @Autowired
    private SpringProcessEngineConfiguration processEngineConfig;

    @Deployment(resources={"com/hha/curatio/activiti/VisitsEmailReport.bpmn20.xml"})
    @Test
    public void testitShouldRuntimersForVisits(){
        Map<String, Object> variables = new HashMap<String, Object>();
    variables.put("nrOfVisits", "5");
    ProcessInstance processInstance = rtService
        .startProcessInstanceByKey("visitsEmailReport", variables);

//    Task task = tskService.createTaskQuery().taskName("VisitPatient")
//        .singleResult();
//    assertEquals("VisitPatient", task.getName());
        List<Task> tasks;
        tasks = tskService.createTaskQuery().taskName("VisitPatient").list();
        assertEquals(1, tasks.size());
//        for(Task ta : tasks){
//
//    taskService.complete(ta.getId());
//        }


    Calendar nowCal = new GregorianCalendar();
    nowCal.add(Calendar.MINUTE, 30);
    ClockUtil.setCurrentTime(nowCal.getTime());
    JobQuery jobQuery = mgtService.createJobQuery().processInstanceId(processInstance.getId());
    assertEquals(1, jobQuery.count());
    waitForJobExecutorToProcessAllJobs(50000L, 25L);

//    Task task2 = tskService.createTaskQuery().taskName("Escalate").singleResult();
//    assertNotNull(task2);
//    assertEquals("Escalate", task2.getName());

//    tskService.complete(task2.getId());
    }

    public void waitForJobExecutorToProcessAllJobs(long maxMillisToWait, long intervalMillis) {
    JobExecutor jobExecutor = processEngineConfig.getJobExecutor();
    jobExecutor.start();

    try {
      Timer timer = new Timer();
      InteruptTask task = new InteruptTask(Thread.currentThread());
      timer.schedule(task, maxMillisToWait);
      boolean areJobsAvailable = true;
      try {
        while (areJobsAvailable && !task.isTimeLimitExceeded()) {
          Thread.sleep(intervalMillis);
          areJobsAvailable = areJobsAvailable();
        }
      } catch (InterruptedException e) {
      } finally {
        timer.cancel();
      }
      if (areJobsAvailable) {
        throw new ActivitiException("time limit of " + maxMillisToWait + " was exceeded");
      }

    } finally {
      jobExecutor.shutdown();
    }
  }

     public boolean areJobsAvailable() {
         boolean jobs = false;
        List<Job> list = mgtService
                      .createJobQuery()
                      .executable()
                      .list();
        jobs = mgtService
                          .createJobQuery()
                          .executable()
                          .list()
                          .isEmpty();

    return jobs;
  }

     private static class InteruptTask extends TimerTask {
    protected boolean timeLimitExceeded = false;
    protected Thread thread;
    public InteruptTask(Thread thread) {
      this.thread = thread;
    }
    public boolean isTimeLimitExceeded() {
      return timeLimitExceeded;
    }
    public void run() {
      timeLimitExceeded = true;
      thread.interrupt();
    }
  }

}

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://www.activiti.org/schema/spring/components"
   xsi:schemaLocation="http://www.springframework.org/schema/beans   
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context 
                           http://www.springframework.org/schema/context/spring-context-2.5.xsd
                           http://www.springframework.org/schema/tx      
                           http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
                           http://www.activiti.org/schema/spring/components
                           http://www.activiti.org/schema/spring/components/activiti.xsd">

   <activiti:annotation-driven process-engine="processEngine" />

<!–   
   <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
      <property name="dataSource" ref="dataSource" />
   </bean>–>

   <bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
      <property name="databaseType" value="mysql" />
      <property name="dataSource" ref="bonecpDS" />
      <property name="transactionManager" ref="transactionManager" />
      <property name="databaseSchemaUpdate" value="true" />
      <property name="history" value="audit" />
      <property name="jobExecutorActivate" value="false" />
   </bean>

   <bean id="processEngine" class="org.activiti.spring.ProcessEngineFactoryBean">
      <property name="processEngineConfiguration" ref="processEngineConfiguration" />
   </bean>

   <bean id="repositoryService" factory-bean="processEngine" factory-method="getRepositoryService" />
   <bean id="runtimeService" factory-bean="processEngine" factory-method="getRuntimeService" />
   <bean id="taskService" factory-bean="processEngine" factory-method="getTaskService" />
   <bean id="historyService" factory-bean="processEngine" factory-method="getHistoryService" />
   <bean id="managementService" factory-bean="processEngine" factory-method="getManagementService" />
        <bean id="identityService" factory-bean="processEngine" factory-method="getIdentityService" />

       <bean id="activitiRule" class="org.activiti.engine.test.ActivitiRule" >
          <property name="processEngine" ref="processEngine" />
       </bean>

</beans>

Outcomes