Saga state machine configuration

Updated at:

Saga mode transactions are based on a state machine engine. The distributed transaction console provides a state designer for orchestrating state graphs and defining node details and property configurations.

The implementation works as follows:

  • You define the service invocation flow using a state graph, which generates a JSON state language definition file.

  • A node in the state graph can represent a service invocation. You can configure a compensation node for the node.

  • The state machine engine executes the JSON state definition file. If an exception occurs, the engine executes the compensation nodes for successfully completed nodes in reverse order to roll back the transaction.

    Note

    If an exception occurs, you can also customize whether to compensate. This feature lets you implement service orchestration. It supports features such as single choice, concurrency, child flows, parameter transforms, parameter mapping, service execution status checks, and exception catching.

This topic describes the properties of state machines and states to help you design and orchestrate your business flows.

State machine properties

The properties of a state machine are as follows:

{
     "Name":"reduceInventoryAndBalance",
     "Comment":"reduce inventory then reduce balance in a transaction",
     "StartState":"ReduceInventory",
     "Version":"0.0.1",
     "States":{}
}

Property descriptions

Parameter

Description

Name

The name of the state machine. This name must be unique.

Comment

The description of the state machine.

Version

The version of the state machine.

StartState

The first state to run at startup.

States

A list of states, which is a map structure. The key is the name of the state and must be unique within the state machine. The value is a map structure that represents the property list of the state.

State properties

ServiceTask

"States":{
            ...
            "ReduceBalance":{
                "Type":"ServiceTask",
                "ServiceName":"balanceAction",
                "ServiceMethod":"reduce",
                "CompensateState":"CompensateReduceBalance",
                "IsForUpdate":true,
                "IsPersist":true,
                "IsAsync":false,
                "Input":[
                    "$.[businessKey]",
                    "$.[amount]",
                    {
                    "throwException":"$.[mockReduceBalanceFail]"
                    }
                ],
                "Output":{
                    "compensateReduceBalanceResult":"$.#root"
                },
                "Status":{
                    "#root == true":"SU",
                    "#root == false":"FA",
                    "$Exception{java.lang.Throwable}":"UN"
                },
                "Retry":[
                    {
                    "Exceptions":["io.seata.saga.engine.mock.DemoException"],
                    "IntervalSeconds":1.5,
                    "MaxAttempts":3,
                    "BackoffRate":1.5
                    },
                    {
                    "IntervalSeconds":1,
                    "MaxAttempts":3,
                    "BackoffRate":1.5
                    }
                ],
                "Catch":[
                    {
                    "Exceptions":[
                    "java.lang.Throwable"
                    ],
                    "Next":"CompensationTrigger"
                    }
                ],
                "Next":"Succeed"
            }
            ...
        }

Property descriptions

Parameter

Description

ServiceName

The service name. This is usually the beanId of the service.

ServiceMethod

The name of the service method.

CompensateState

The compensation state for this state.

IsForUpdate

Indicates whether the service updates data. The default value is `false`. If `CompensateState` is configured, the default value is `true`. A service with a compensation service is always a data update service.

IsPersist

Specifies whether to store the execution log. The default value is `true`. For some query services, you can set this property to `false` to improve performance by not storing logs. This is safe because these services can be re-executed during exception recovery.

IsAsync

Invokes the service asynchronously. Asynchronous invocation ignores the return value of the service. Therefore, the user-defined service execution status mapping (the `status` property) is ignored, and the service call is considered successful by default. If the asynchronous call fails to submit, for example, because the thread pool is full, the service execution fails.

Input

A list of input parameters for the service call, provided as an array that corresponds to the parameter list of the service method. $. indicates that you can use a SpringEL expression to retrieve parameters from the state machine context. If a parameter is a constant, enter its value directly. For more information about how to pass complex parameters, see Input definition for complex parameters.

Output

Assigns parameters from the service's return value to the state machine context. This is a map structure.

  • The key is the key used when placing the value into the state machine context, which is also a map.

  • In the value, `$.` indicates a SpringEL expression that retrieves a value from the service's return parameter. `#root` represents the entire return parameter of the service.

Status

Maps the service execution status. The framework defines three statuses: `SU` (success), `FA` (failure), and `UN` (unknown). You must map the service execution status to one of these three statuses to help the framework determine the consistency of the entire transaction. This property is a map structure.

  • The key is a conditional expression. It typically evaluates the service's return value or a thrown exception. The default is a SpringEL expression that evaluates the service's return parameters. An expression starting with `$Exception{` evaluates the exception type.

  • The value is the status to map to when the conditional expression is true.

Catch

The routing to take after an exception is caught.

Retry

The retry policy to use after an exception is caught. This is an array, and you can configure multiple rules.

  • `Exceptions`: A list of exceptions to match.

  • `IntervalSeconds`: The retry interval in seconds.

  • `MaxAttempts`: The maximum number of retry attempts.

  • `BackoffRate`: The multiplier for calculating the next retry interval based on the previous one. For example, if the last retry interval was 2 seconds and `BackoffRate=1.5`, the next retry interval is 3 seconds.

  • You can omit the `Exceptions` property. If omitted, the framework automatically matches network timeout exceptions. If a different exception occurs during a retry attempt, the framework re-evaluates the rules and retries based on the new matching rule. The total number of retries for a single rule will not exceed its `MaxAttempts` value.

Next

The next state to execute after the service completes.

If you do not configure `Status` to map the service execution status, the system automatically determines the status as follows:

  • If there is no exception, the execution is considered successful.

  • If there is an exception, the system checks if it is a network connectivity timeout. If it is, the status is considered `FA`.

  • For any other exception, if the service has `IsForUpdate=true`, the status is `UN`. Otherwise, the status is `FA`.

The framework determines the execution status of the entire state machine. A state machine has two statuses: `status` (forward execution status) and `compensateStatus` (compensation status).

  • If all services execute successfully (transaction commits), then `status`=SU and `compensateStatus`=null.

  • If a service fails, a data update service has succeeded, and no compensation has occurred (transaction fails to commit), then `status`=UN and `compensateStatus`=null.

  • If a service fails, no data update service has succeeded, and no compensation has occurred (transaction fails to commit), then `status`=FA and `compensateStatus`=null.

  • If compensation succeeds (transaction rolls back), then `status`=FA/UN and `compensateStatus`=SU.

  • If compensation occurs but some services fail to compensate (rollback fails), then `status`=FA/UN and `compensateStatus`=UN.

  • If a transaction fails to commit or roll back, the Seata Server continuously retries the operation.

Choice

"ChoiceState":{
        "Type":"Choice",
        "Choices":[
            {
            "Expression":"[reduceInventoryResult] == true",
            "Next":"ReduceBalance"
            }
        ],
        "Default":"Fail"
}

A `Choice` state provides single-choice routing.

  • `Choices`: A list of available branches. Only the first branch that meets its condition is executed.

  • `Expression`: A SpringEL expression.

  • `Next`: The next state to execute when the `Expression` is true.

Succeed

"Succeed":{
    "Type":"Succeed"
}

Reaching a Succeed state means the state machine terminated normally. Normal termination does not mean the transaction was successful. Success depends on whether all states completed successfully.

Fail

"Fail":{
    "Type":"Fail",
    "ErrorCode":"PURCHASE_FAILED",
    "Message":"purchase failed"
}

Reaching a Fail state means the state machine has terminated with an error. In this case, you can configure `ErrorCode` and `Message` to specify the error details, which are then returned to the caller.

CompensationTrigger

"CompensationTrigger":{
    "Type":"CompensationTrigger",
    "Next":"Fail"
}

A `CompensationTrigger` state is used to trigger a compensation event and roll back the distributed transaction. `Next` specifies the state to route to after compensation is successful.

SubStateMachine

"CallSubStateMachine":{
        "Type":"SubStateMachine",
        "StateMachineName":"simpleCompensationStateMachine",
        "CompensateState":"CompensateSubMachine",
        "Input":[
            {
            "a":"$.1",
            "barThrowException":"$.[barThrowException]",
            "fooThrowException":"$.[fooThrowException]",
            "compensateFooThrowException":"$.[compensateFooThrowException]"
            }
        ],
        "Output":{
             "fooResult":"$.#root"
        },
        "Next":"Succeed"
}

A `SubStateMachine` state calls a child state machine.

  • `StateMachineName`: The name of the child state machine to call.

  • `CompensateState`: The compensation state for the child state machine. This property is optional. If you do not configure it, the system automatically creates a compensation state. Compensating a child state machine involves calling its `compensate` method. Therefore, you do not need to implement a compensation service for the child state machine yourself. When you configure this property, you can use the `Input` property to pass custom variables. See `CompensateSubMachine` below.

CompensateSubMachine

"CompensateSubMachine":{
      "Type":"CompensateSubMachine",
      "Input":[
           {
                 "compensateFooThrowException":"$.[compensateFooThrowException]"
           }
       ]
}

A CompensateSubMachine state is used to compensate a child state machine by calling its compensate method. You can use the Input property to pass custom variables and the Status property to determine whether the compensation was successful.

Defining complex parameter inputs

"FirstState":{
        "Type":"ServiceTask",
        "ServiceName":"demoService",
        "ServiceMethod":"complexParameterMethod",
        "Next":"ChoiceState",
        "ParameterTypes":["java.lang.String","int","io.seata.saga.engine.mock.DemoService$People","[Lio.seata.saga.engine.mock.DemoService$People;","java.util.List","java.util.Map"],
        "Input":[
                "$.[people].name",
                "$.[people].age",
                 {
                    "name":"$.[people].name",
                    "age":"$.[people].age",
                    "childrenArray":[
                        {
                        "name":"$.[people].name",
                        "age":"$.[people].age"
                        },
                        {
                        "name":"$.[people].name",
                        "age":"$.[people].age"
                        }
                    ],
                    "childrenList":[
                        {
                        "name":"$.[people].name",
                        "age":"$.[people].age"
                        },
                        {
                        "name":"$.[people].name",
                        "age":"$.[people].age"
                        }
                    ],
                    "childrenMap":{
                        "lilei":{
                        "name":"$.[people].name",
                        "age":"$.[people].age"
                        }
                    }
                 },
                [
                    {
                    "name":"$.[people].name",
                    "age":"$.[people].age"
                    },
                    {
                    "name":"$.[people].name",
                    "age":"$.[people].age"
                    }
                ],
                [
                    {
                    "@type":"io.seata.saga.engine.mock.DemoService$People",
                    "name":"$.[people].name",
                    "age":"$.[people].age"
                    }
                ],
                {
                "lilei":{
                "@type":"io.seata.saga.engine.mock.DemoService$People",
                "name":"$.[people].name",
                "age":"$.[people].age"
                }
            }
        ],
        "Output":{
             "complexParameterMethodResult":"$.#root"
        }
}

The `complexParameterMethod` method shown above is defined as follows:

People complexParameterMethod(String name, int age,People people,People[] peopleArray,List<People> peopleList,Map<String,People> peopleMap)
class People{
    private String name;
    private int    age;
    private People[] childrenArray;
    private List<People> childrenList;
    private Map<String,People> childrenMap;
...
}

When you start the state machine, pass the parameters as follows:

Map<String,Object> paramMap =new HashMap<>(1);
People people =new People();
people.setName("lilei");
people.setAge(18);
paramMap.put("people", people);
String stateMachineName ="simpleStateMachineWithComplexParams";
StateMachineInstance inst = stateMachineEngine.start(stateMachineName,null, paramMap);
Note

The `ParameterTypes` property is optional but required when the called method's parameter list includes collection types with generics, such as `Map` or `List`. This is necessary because Java compilation erases generic type information. You must also add an `@type` attribute to the corresponding JSON in the `Input` to declare the generic type, which is the element type of the collection.