Recipes
This chapter contains documentation for existing built-in state machine recipes.
Spring Statemachine is a foundational framework. That is, it does not have much higher-level functionality or many dependencies beyond Spring Framework. Consequently, correctly using a state machine may be difficult. To help, we have created a set of recipe modules that address common use cases.
What exactly is a recipe? A state machine recipe is a module that addresses a common use case. In essence, a state machine recipe is both an example that we have tried to make it easy for you to reuse and extend.
| Recipes are a great way to make external contributions to the Spring Statemachine project. If you are not ready to contribute to the framework core itself, a custom and common recipe is a great way to share functionality with other users. |
36. Persist
The persist recipe is a simple utility that lets you use a single state machine instance to persist and update the state of an arbitrary item in a repository.
The recipe’s main class is PersistStateMachineHandler, which makes three assumptions:
-
An instance of a
StateMachine<String, String>needs to be used with aPersistStateMachineHandler. Note that states and Events are required to be type ofString. -
PersistStateChangeListenerneeds to be registered with handler to react to persist request. -
The
handleEventWithStatemethod is used to orchestrate state changes.
You can find a sample that shows how to use this recipe at Persist.
37. Tasks
The tasks recipe is a concept to run DAG (Directed Acrylic Graph) of Runnable instances that use
a state machine. This recipe has been developed from ideas introduced
in Tasks sample.
The next image shows the generic concept of a state machine. In this state chart,
everything under TASKS shows a generic concept of how a single
task is executed. Because this recipe lets you register a deep
hierarchical DAG of tasks (meaning a real state chart would be a deeply
nested collection of sub-states and regions), we have no need to be
more precise.
For example, if you have only two registered tasks, the following state chart
would be correct when TASK_id is replaced with TASK_1 and TASK_2 (assuming
the registered tasks IDs are 1 and 2).
Executing a Runnable may result an error. Especially if a complex
DAG of tasks is involved, you want to have a way to handle
task execution errors and then have a way to continue execution
without executing already successfully executed tasks. Also,
it would be nice if some execution errors can be handled
automatically. As a last fallback, if an error cannot be handled
automatically, the state machine is put into a state where the user can handle
errors manually.
TasksHandler contains a builder method to configure a handler instance
and follows a simple builder pattern. You can use this builder to
register Runnable tasks and TasksListener instances and define
StateMachinePersist hook.
Now we can take a simple Runnable that runs a simple sleep as the following
example shows:
private Runnable sleepRunnable() {
return new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
};
}
| The preceding example is the base for all of the examples in this chapter. |
To execute multiple sleepRunnable tasks, you can register tasks and
execute runTasks() method from TasksHandler, as the following example shows:
TasksHandler handler = TasksHandler.builder()
.task("1", sleepRunnable())
.task("2", sleepRunnable())
.task("3", sleepRunnable())
.build();
handler.runTasks();
To listen to what is happening with a task execution, you can register an instance of
a TasksListener with a TasksHandler. This recipe
provides an adapter TasksListenerAdapter if you do not want to
implement a full interface. The listener provides a various hooks to
listen tasks execution events. The following example shows the definition of the
MyTasksListener class:
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) {
}
}
You can either register listeners by using a builder or register them directly with a
TasksHandler as the following example shows:
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();
Every task needs to have a unique identifier, and (optionally) a task can be defined to be a sub-task. Effectively, this creates a DAG of tasks. The following example shows how to create a deep nested 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 an error happens and the state machine running these tasks goes into an
ERROR state, you can call fixCurrentProblems handler method to
reset the current state of the tasks kept in the state machine’s extended state
variables. You can then use the continueFromError handler method to
instruct the state machine to transition from the ERROR state back to the
READY state, where you can again run tasks.
The following example shows how to do so:
TasksHandler handler = TasksHandler.builder()
.task("1", sleepRunnable())
.task("2", sleepRunnable())
.task("3", sleepRunnable())
.build();
handler.runTasks();
handler.fixCurrentProblems();
handler.continueFromError();