LSPS documentation logo
LSPS Documentation
Creating a Task Type

Every Task in a BPMN Process is of a particular type: The type determines the Task's logic. Therefore, if you need a task that will perform actions specific to your business or interact with another system, consider implementing a custom task type.

Task Execution

When a token enters a task, a task instance is created and becomes Alive: At this point, the start() method of the task is executed. The method can return either with Result.FINISHED or Result.WAITING_FOR_INPUT:

  • If the method returns FINISHED, the task becomes Accomplished.
  • If the method returns WAITING_FOR_INPUT, the task remains Alive it becomes a transaction boundary of the current model transaction. When such a task receives a message from the CommunicationService, its processInput() method is executed. If it returns Result.FINISHED, the task becomes Accomplished; if it returns Result.WAITING_FOR_INPUT, it remains Alive and waits for the next message.

When a task instance is terminated abnormally, for example, by a timeout intermediate event attached to the task, the terminate() method is called.

To create a custom task type, you will need to do the following:

Declaring a Task Type

To declare the task type and make it accessible in PDS, do the following:

  1. Switch to the Modeling perspective.
  2. In your module, create a task type definition: right-click the module, then New -> Task Type Definition.
  3. In the Task Type Editor, click Add.
  4. Under Task Type Details, enter the task-type name and in the Classname field enter the fully qualified name of the class. Mind that the task class must be in the ejb package of your application.

    If you have already implemented the task type, you can copy the qualified name of the task class in its Outline view: right-click the class name and select Copy Qualified Name.

  5. Select the relevant flags:
    • Public: select to allow access from importing modules
    • Create activity reflection type: select to create a Record that represents the action taken by the task type. The Record is a subtype of the Activity record and Therefore can be set as the activity parameter of the Execute task.
    • Deprecated: select to display a validation marker for deprecated elements when a task of this task type is used.
  6. In the Parameters list box, define the task parameters if applicable.

    Check Dynamic to wrap the parameter value in a non-parametric closure: Such a parameter is processed as { -> <parameter_value> }. The task-type implementation has to process the parameter as a closure.

    customtaskparams.png
    Task type declaration
  7. If you have not created the implementation yet, generate the class for the task type:
    1. In the GO-BPMN Explorer view, right-click the GO-BPMN module.
    2. Go to Generate -> Task Java Sources.
    3. In the Task Source Code Generation dialog box:
      1. Select the check boxes of the relevant tasks.
      2. In the Destination folder text box, specify the destination path, possibly to a package of the <YOUR_APP>-ejb project.
      3. Click Finish.
        tasksourcecodegeneration.png
        Task source code generation
        The generator does the following:
      • places the task implementation in the correct directory structure.
      • generates a class declaration as an implementation of the com.whitestein.lsps.engine.ExecutableTask interface;
      • creates a variable for each parameter in the task type and the related setters used by the Execution Engine to access the variables;
      • generates the initial structure for the task implementation and documentation.
    4. Implement the task-type class.

Consider distributing the module as a library.

Implementing a Task Type

You can implement a task type that:

Implementing a Simple Task Type

To implement a custom task type that starts and finishes in the same transaction and does not wait for any event, do the following:

  1. Declare the task type in your Module. Consider distributing it as a part of a library.
  2. In a workspace with your LSPS Application, switch to the Java perspective.
  3. If you have not generated the Task Java Sources, create the implementing task-type class:
    1. Right-click your package, go to New -> Class.
    2. In the dialog, set ExecutableTask in the Interface field.
      customtaskclass.png
      Creating class for implementation of the custom task type
  4. Implement the logic in the start() method and make it return Result.FINISHED

    Important: The methods start() must be implemented in such a way that the thread is not blocked.

  5. To get the parameters of the task as defined in the Task Type definition, use the getParameter() method on the TaskContext. The TaskContext is passed as the first parameter of the start() method.
  6. To access other resources of the model instance, such as, variables, signal queue, etc. use the respective methods of the TaskContext, such as, getVariableValue(), getProcessModel(), getProcessInstance(), addSignal(), etc.
  7. If the implementing class is an EJB, register it with the ComponentServiceBean class.
  8. Build and deploy the application.

You can now use the task in processes: make sure the task declaration is available to the process, for example, by the means of module import.

Simple task implementation

public class RecordReturn implements ExecutableTask {
 
  @Override
  public Result processInput(TaskContext arg0, Object arg1) throws ErrorException {
    return null;
  }
 
  @Override
  public Result start(TaskContext context) throws ErrorException {
 
    //get the value of the param parameter of type String:
    String message = (String) context.getParameter("param");
 
    //to get the message parameter which is a Record:
    //RecordHolder message = (RecordHolder) context.getParameter("message");
    //save the value of the "text" property of the message Record instance:
    //String messageText = (String) message.getProperty("text");
 
    //set the "myProcessVariable" of the process to the messageText:
    context.setVariableValue(QID.create("procVar"), message);
    //finish and release the token:
    return Result.FINISHED;
 
  }
 
  @Override
  public void terminate(TaskContext context, TerminationReason reason) throws ErrorException {
  }
}

Implementing a Task Type That Waits for a Message

To implement a custom task type that waits for an event and potentially continues in a new transaction, do the following:

  1. Declare the task type in your Module. Consider distributing it as a part of a library.
  2. In a workspace with your LSPS Application, switch to the Java perspective.
  3. If you have not generated the Task Java Sources, create the implementing task-type class:
    1. Right-click your package, go to New -> Class.
    2. In the dialog, set ExecutableTask in the Interface field.
  4. Make the task class a stateless EJB and add the @PermitAll annotation.
  5. Implement the logic in the start() method and make it return Result.FINISHED and Result.WAITING_FOR_INPUT as appropriate.
  6. Implement the processInput() method, which is executed when the task receives a message. The message is sent via the CommunicationService, which requires the task to be implemented as an EJB.
  7. Implement the terminate() method, which is called when the task instance is terminated abnormally.

    Important: The methods start() and processInput() must be implemented in such a way that the thread is not blocked.

    @Stateless
    @PermitAll
    public class WaitForInput implements ExecutableTask {
     
      @Override
      public Result start(TaskContext context) throws ErrorException {
     
        //implement the logic of the start method.
        return Result.WAITING_FOR_INPUT;
      }
     
      @Override
      public Result processInput(TaskContext context, Object input) throws ErrorException {
     
        return Result.FINISHED;
      }
     
      @Override
      public void terminate(TaskContext context, TerminationReason reason) throws ErrorException {
      }
    }
  8. Register the EJB in the ComponentServiceBean:
    @EJB(beanName = "WaitForInput")
    private ExecutableTask waitForInput;
    @Override
    protected void registerCustomComponents() {
      register(waitForInput, WaitForInput.class);
    }
  9. Send the message to the task using the CommunicationService (when the task receives the message, it runs the processInput() method):
    1. Store the id of the task so it can be accessed by the task or function, for example, from the start() method, or as a shared record from the module. You will need the id to address the message.
      @Override
        public Result start(TaskContext context) throws ErrorException {
       
          //saves the task id in process variable taskId
          //(taskId is modeled in the process):
          context.setVariableValue(QID.create("taskId"), new Decimal(context.getTaskId()));
          return Result.WAITING_FOR_INPUT;
        }
    2. Declare a task or function that will send the message that will wake up the Task that finished previously with Result.WAITING_FOR_INPUT: the task will run the processInput() method.
      //example function declaration
      //passing id of the target task as parameter:
      public void wakeUpTask(Integer taskId)
         native org.eko.taskapp.ejb.WakeUpFunctions.wakeUpWaitingTask;
      
    3. Implement the task or function as an EJB that calls the CommunicationService to send a message to the Task:
      • For the function, implement the function interface, function as an EJB, and register it with ComponentServiceBean.
      • For the task, implement the task as an EJB implementing ExecutableTask, and register it with ComponentServiceBean.
  10. From the model, call the function or run the task that sends the message to trigger the processInput() method of the task.

Note: In the example, the wakeUpTask() call used within the same model instance (as the task context parameter, you get the context tree of the model instance, through the current process instance, to the task. If you want to call the task from another model instance, make the id of the model instance with the task accessible, for example, via database.

Example interface of the function

@Local
public interface WakeUpFunctions {
  void wakeUpWaitTask(ExecutionContext context, Decimal taskId)
      throws ModelInstanceNotFoundException, InvalidModelInstanceStateException, ErrorException;
}

Example bean that implements the interface of the function

@Stateless
@PermitAll
public class WakeUpFunctionsBean implements WakeUpFunctions {
  @EJB
  private CommunicationService communicationService;
 
  @Override
  public void wakeUpWaitTask(ExecutionContext context, Decimal taskId) throws ModelInstanceNotFoundException, InvalidModelInstanceStateException, ErrorException {
 
    //message from the current model instance to the task passed in the param: 
    Message myMessage = new Message(Identifier.ofModelInstance(context.getModelInstance().getId()),
        Identifier.ofTask(context.getModelInstance().getId(), taskId.longValue()));
    communicationService.sendAsync(myMessage);
  }
}

Example registration of the EJB

@EJB(beanName = "WaitForInputTask")
private ExecutableTask waitForInput;
@EJB
private WakeUpFunctions wakeUpFunctions;
@Override
protected void registerCustomComponents() {
  register(waitForInput, WaitForInputTask.class);
  register(wakeUpFunctions, WakeUpFunctions.class);
}

Example task implementation that sends a message to the waiting task

@Stateless
@PermitAll
public class WakeTask implements ExecutableTask {
  @EJB
  private CommunicationService communicationService;
  @Override
  public Result start(TaskContext context) throws ErrorException {
    context.getVariableValue(QID.create("taskId"));
    Message myMessage = new Message(
        Identifier.ofModelInstance(
            context.getModelInstance().getId()),
        Identifier.ofTask(
            context.getModelInstance().getId(), ((Decimal) context.getVariableValue(QID.create("taskId"))).longValue()));
    communicationService.sendAsync(myMessage);
    return Result.FINISHED;
  }
...

Implementing an Asynchronous Task Type

Asynchronous tasks represent the border of a model transaction: they hold the token but do not block further process execution (other tokens can continue). The task still waits for the result of its implementation and only then finishes.

An asynchronous task type must implement ExecutableTask and extend the AbstractAsynchronousExecutionTask which is an EJB.

Note that implementations of asynchronous task types must run in a single transaction: if your task-type implementation requires multiple transactions, split it into multiple task types. For such tasks, if the server is restarted during the task execution, the execution is repeated.

Important: If you still decide to use multiple transactions in the implementation of your asynchronous tasks, make sure that the execution is always consistent: consider handling the scenario when the server is restarted while the task is running: since the asynchronous execution of the task is not governed by the task, on server restart, the execution seizes to exist while the task itself becomes running again and waits for the result from the execution. To solve this problem, you can

  • define the timeout on the task in your models to handle the case when the task receives no results after server restart: add an interrupting timer event on the border of your task with a timeout duration. Mind that such handling can cause premature interruption of the task as a side effect.
  • persist data about the execution phase in the database and check its status regularly from the running task (heartbeat check).

To implement an asynchronous task type, do the following:

  1. In the <YOUR_APP>-ejb project, create the class for the task type implementation that extends AbstractAsynchronousExecutionTask and implements ExecutableTask.
  2. Make the class an EJB (AbstractAsynchronousExecutionTask is implemented as an EJB).
  3. Implement the methods:
    • executeAsynchronously(): checks prior to task execution whether the task should be executed asynchronously; this is useful for tasks that can run both as asynchronous or synchronous;
    • collectDataForExecution(): provides the data from the task context required for the Java implementation, typically parameters of the task type
    • getImplementationClass(): returns the class that implements the task type (typically this class)
    • processDataAsynchronously(): processing logic that returns the result of the actions
    • processExecutionResult(): processes the result after the asynchronous action
      
      @Stateless
      public class GoogleSearchResultStats extends AbstractAsynchronousExecutionTask implements ExecutableTask {
        @Override
        public Serializable collectDataForExecution(TaskContext context) throws ErrorException {
          String collectedData = (String) context.getParameter("queryString");
          return collectedData;
        }
        @Override
        public boolean executeAsynchronously(TaskContext context) throws ErrorException {
          //the task is always asynchronous:
          return true;
        }
        @Override
        public Class<? extends AbstractAsynchronousExecutionTask> getImplementationClass() {
          return GoogleSearchResultStats.class;
        }
        @Override
        public Serializable processDataAsynchronously(Serializable data) {
          String result;
          try {
            String keyword = (String) data;
            result = readFromUrl(keyword);
          } catch (IOException e) {
            result = "not found";
          }
          return result;
        }
        @Override
        public void processExecutionResult(TaskContext context, Serializable result) throws ErrorException {
          System.out.println("Number of results: " + result);
        }
        /**
         * 
         * @param keyword
         * @return number of results
         * @throws IOException
         */
        public String readFromUrl(String keyword) throws IOException {
          String url = "https://www.google.sk/search?q=" + keyword;
          Document document = Jsoup.connect(url).userAgent("Mozilla").timeout(10000).get();
          String question = document.select("#resultStats").text();
          String resultCount = question.split(": ")[1];
          return resultCount;
        }
      }
  4. If you want to repeat the execution under some circumstances, throw a runtime exception from the respective method, for example, LSPSRuntimeException.
  5. Inject and register your class in the ComponentServiceBean.
      @EJB(beanName = "GoogleSearchResultStats")
      private ExecutableTask searchResultTask;
      @Override
      protected void registerCustomComponents() {
        // parameter 1 is the ejb, parameter 2 is the implementation class as referenced in the task type definition:
        register(searchResultTask, GoogleSearchResultStats.class);
  6. If your class is an EBJ, register it with the ComponentServiceBean class.
  7. Build and deploy your application
  8. Declare the task type in a module.

Creating an EJB Task Type

To have a task type implementation that is an EJB, you need to register it with the server via the register() method of the ComponentServiceBean class.

  1. Inject the task EJB in the ComponentServiceBean class as the ExecutableTask:
    @EJB(beanName = "DecimalAddition")
    private ExecutableTask decimalAddition;
  2. Register it with the ComponentServiceBean class in the registerCustomComponents() method.
    register(decimalAddition, DecimalAddition.class);

Example EJB Task Type

Implementation:

@Stateless
public class DecimalAddition implements ExecutableTask {
  @Override
  public Result processInput(TaskContext context, Object input) throws ErrorException {
    return null;
  }
  @Override
  public Result start(TaskContext context) throws ErrorException {
    Decimal a = (Decimal) context.getParameter("a");
    Decimal b = (Decimal) context.getParameter("b");
    System.out.println("################## result" + a.add(b));
    return Result.FINISHED;
  }
  @Override
  public void terminate(TaskContext context, TerminationReason reason) throws ErrorException {
  }
}

Registration:

//Modifications in the ComponentServiceBean class:
  (beanName = "DecimalAddition")
  private ExecutableTask decimalAddition;
  protected void registerCustomComponents() {
    register(decimalAddition, DecimalAddition.class);
  }