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. |
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
. -
PersistStateChangeListener
needs to be registered with handler to react to persist request. -
The
handleEventWithState
method is used to orchestrate state changes.
You can find a sample that shows how to use this recipe at [statemachine-examples-persist].
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 [statemachine-examples-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();