Durable State Machines
DSM implements a declarative state machine programming model as an extension for Azure Durable Functions. It's based on the "hierarchical state machine" concepts behind statecharts and the SCXML W3C Recommendation.
What are statecharts?
The best definition comes from here:
The primary feature of statecharts is that states can be organized in a hierarchy: A statechart is a state machine where each state in the state machine may define its own subordinate state machines, called substates. Those states can again define substates.
The utility of traditional state machines goes down as system complexity increases, due to state explosion. Also, state machines by themselves are merely a description of behavior, not behavior itself. Statecharts (and DSM) address both of these limitations.
Goals
DSM aims to provide a full-featured statechart implementation for Azure Durable Functions. This means you can run state machines locally on your laptop, or anywhere Azure Functions will run (Kubernetes, Azure serverless, edge compute, etc.).
Some specific design and implementation choices:
-
An abstraction for describing statechart definitions and an initial implementation
-
Abstractions for both pull- and push-based communication with external systems; talk to all your favorite native cloud services from within your statechart!
-
In addition to parent-child state relationships within a single statechart, there is also support for parent-child relationships between statechart instances (execute statechart A within the context of statechart B, etc.)
-
An extensible telemetry service implemented with SignalR... observe state machine execution as it occurs, etc.
Getting Started
Statecharts support standard state machine concepts like atomic and parallel states, state transitions, actions within states, etc.
Statecharts also support advanced concepts like history states, event-based transitions, and nested or compound state hierarchies.
In SCDN you define statecharts using declarative metadata in JSON. See below for examples.
For advanced scenarios, you can also define your own metadata syntax and map it to primitives the SCDN engines can interpret and execute directly.
Usage
Run statecharts as a Durable Function orchestration, using the standard IDurableClient APIs.
Option 1: strongly-typed C#
// in your DF app (on the server)
public class Definitions
{
[StateMachineDefinition("my-state-machine")]
public StateMachine<(int x, int y)> MyStateMachine =>
new StateMachine<(int x, int y)>
{
Id = "test",
States =
{
new AtomicState<(int x, int y)>
{
Id = "state1",
OnEntry = new OnEntryExit<(int x, int y)>
{
Actions =
{
new Assign<(int x, int y)>
{
Target = d => d.x,
ValueFunction = data => data.x + 1
}
}
},
OnExit = new OnEntryExit<(int x, int y)>
{
Actions =
{
new Assign<(int x, int y)>
{
Target = d => d.x,
ValueFunction = data => data.x + 1
}
}
},
Transitions =
{
new Transition<(int x, int y)>
{
Targets = { "alldone" }
}
}
},
new FinalState<(int x, int y)>
{
Id = "alldone"
}
}
};
}
// on the client
IDurableClient client = GetDurableFunctionClient(); // obtain via dependency injection
var data = (5, 0); // any serializable type
var instanceId = await client.StartNewStateMachineAsync("my-state-machine", data);
var result = await client.WaitForStateMachineCompletionAsync(instanceId);
data = result.ToOutput<(int x, int y)>();
Console.WriteLine(data.x);
Option 2: HTTP + JSON
// from any client
HTTP POST /runtime/webhooks/durabletask/orchestrators/statemachine-definition
{
'input': {
'items': [ 1, 2, 3, 4, 5 ],
'sum': 0
},
'statemachine': {
'id': 'test',
'initialstate': 'loop',
'states': [
{
'id': 'loop',
'type': 'atomic',
'onentry': {
'actions': [
{
'type': 'foreach',
'valueexpression': 'items',
'currentitemlocation': 'arrayItem',
'actions': [
{
'type': 'assign',
'target': 'sum',
'valueexpression': 'sum + arrayItem'
},
{
'type': 'log',
'messageexpression': '""item = "" + arrayItem'
}
]
}
]
},
'transitions': [
{
'conditionexpression': 'sum >= 15',
'target': 'done'
}
]
},
{
'id': 'done',
'type': 'final',
'onentry': {
'actions': [
{
'type': 'log',
'messageexpression': '""item = "" + arrayItem'
}
]
}
}
]
}
}
Samples and Tests
-
Unit tests are here (work-in-progress :-))
-
Small standalone durable tests are here
-
More full-featured sample applications are here