24. Tasks

Tasks recipe is a concept to execute DAG of Runnable instances using a state machine. This recipe has been developed from ideas introduced in sample Chapter 28, Tasks.

Generic concept of a state machine is shown below. In this state chart everything under TASKS just shows a generic concept of how a single task is executed. Because this recipe allows to register deep hierarcical DAG of tasks, meaning a real state chart would be deep nested collection of sub-states and regions, there’s no need to be more presise.

For example if you have only two registered tasks, below state chart would be correct with TASK_id replaced with TASK_1 and TASK_2 if registered tasks ids are 1 and 2.

statechart9

Executing a Runnable may result an error and especially if a complex DAG of tasks is involved it is desirable that there is a way to handle tasks execution errors and then having a way to continue execution without executing already successfully executed tasks. Addition to this it would be nice if some execution errors can be handled automatically and as a last fallback, if error can’t be handled automatically, state machine is put into a state where user can handle errors manually.

TasksHandler contains a builder method to configure handler instance and follows a simple builder patter. This builder can be used to register Runnable tasks, TasksListener instances, define StateMachinePersist hook, and setup custom TaskExecutor instance.

Now lets take a simple Runnable just doing a simple sleep as shown below. This is a base of all examples in this chapter.

private static Runnable sleepRunnable() {
    return new Runnable() {

        @Override
        public void run() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
        }
    };
}

To execute multiple sleepRunnable tasks just register tasks and execute runTasks() method from TasksHandler.

TasksHandler handler = TasksHandler.builder()
        .task("1", sleepRunnable())
        .task("2", sleepRunnable())
        .task("3", sleepRunnable())
        .build();

handler.runTasks();

Order to listen what is happening with a task execution an instance of a TasksListener can be registered with a TasksHandler. Recipe provides an adapter TasksListenerAdapter if you don’t want to implement a full interface. Listener provides a various hooks to listen tasks execution events.

private static class MyTasksListener extends TasksListenerAdapter {

    @Override
    public void onTasksStarted() {
    }

    @Override
    public void onTasksContinue() {
    }

    @Override
    public void onTaskPreExecute(Object id) {
    }

    @Override
    public void onTaskPostExecute(Object id) {
    }

    @Override
    public void onTaskFailed(Object id, Exception exception) {
    }

    @Override
    public void onTaskSuccess(Object id) {
    }

    @Override
    public void onTasksSuccess() {
    }

    @Override
    public void onTasksError() {
    }

    @Override
    public void onTasksAutomaticFix(TasksHandler handler, StateContext<String, String> context) {
    }
}

Listeners can be either registered via a builder or directly with a TasksHandler as shown above.

MyTasksListener listener1 = new MyTasksListener();
MyTasksListener listener2 = new MyTasksListener();

TasksHandler handler = TasksHandler.builder()
        .task("1", sleepRunnable())
        .task("2", sleepRunnable())
        .task("3", sleepRunnable())
        .listener(listener1)
        .build();

handler.addTasksListener(listener2);
handler.removeTasksListener(listener2);

handler.runTasks();

Above sample show how to create a deep nested DAG of tasks. Every task needs to have an unique identifier and optionally as task can be defined to be a sub-task. Effectively this will create a DAG of tasks.

TasksHandler handler = TasksHandler.builder()
        .task("1", sleepRunnable())
        .task("1", "12", sleepRunnable())
        .task("1", "13", sleepRunnable())
        .task("2", sleepRunnable())
        .task("2", "22", sleepRunnable())
        .task("2", "23", sleepRunnable())
        .task("3", sleepRunnable())
        .task("3", "32", sleepRunnable())
        .task("3", "33", sleepRunnable())
        .build();

handler.runTasks();

When error happens and a state machine running these tasks goes into a ERROR state, user can call handler methods fixCurrentProblems to reset current state of tasks kept in a state machine extended state variables. Handler method continueFromError can then be used to instruct state machine to transition from ERROR state back to READY state where tasks can be executed again.

TasksHandler handler = TasksHandler.builder()
        .task("1", sleepRunnable())
        .task("2", sleepRunnable())
        .task("3", sleepRunnable())
        .build();

        handler.runTasks();
        handler.fixCurrentProblems();
        handler.continueFromError();