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 30, 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
.
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 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 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();