Awaitable Task
Execution
Callbacks from the previous chapter is a powerful mechanism to implements the state machine with multiple decision flows. But sometimes you need to implement a chain of responsibility, where next state is triggered once the current state service operation is done, or move to some error state if the current state operation has triggered error.
This can be achieved by using the async service with cancellation token.
Cancellation token need to be checked as the service can be cancelled by another service if state invokes several services in parallel. Services then are raised and first service finalized, either Timeout, or with callback, or with async task, are causing state machine to switch state. See parallel services chapter.
As an example, for example we have an elevator, that reacts on user's calling it to come to the 5th floor.
Example of Awaitable Service
As you can see, there is no any decisions on each step, the state machine performs the chain of actions one after another. This example is a simple elevator state machine, not the full one. It is here only for demonstration purposes.
Below is the C# code for the illustrated diagram.
// creating all necessary states
State waitingForUserRequest = new State("waitingForUserRequest");
State movingToRequestedFloor = new State("movingToRequestedFloor");
State stoppingAtFloor = new State("stoppingAtFloor");
State openingDoor = new State("openingDoor");
State waitingUserToStepIn = new State("waitingUserToStepIn");
// fill up waiting for user request
waitingForUserRequest.WithInvoke(async (cancel) => {
// await until queue with user floor requests is empty
while(requestQueue.Length == 0)
{
// wait a bit to check queue a bit later
// in this case providing the cancellation token is not needed
// as each state executes only one service, but it is good practice
// to handle it as you never know if you need to reuse this service
// in another states later.
await Task.Delay(1000, cancel);
}
// once queue has user requests, finalize the service
await Task.FromResult(0);
// again, main idea here is to exit, once service exits without error
// it moves to another state, specified here as "movingToRequestedFloor".
// third argument is given as null, as there is no state to move to in case of exception
// if exception is thrown here, the state machine stops with this exception
}, "movingToRequestedFloor", null);
movingToRequestedFloor.WithInvoke(async (cancel) => {
// get the floor number to move to
int requestedFloor = requestQueue.Pop();
int direction = Math.Sign(requestedFloor - currentFloor);
int speed = 10 * direction;
// start motor with 10, -10 or 0.
motor.Start(speed);
// wait until we reach the needed floor
while(currentFloor != requestedFloor)
{
// wait until the cabin moved to the requested floor
// if we are at the same floor, this will exit right aray.
await Task.Delay(1000);
}
// starting the motor to the correct direction
}, "stoppingAtFloor", null);
stoppingAtFloor.WithInvoke(async (cancel) => {
// command to motor to stop
motor.Stop();
// wait for cabin to stop
await cabin.HasStopped();
},"openingDoor", null);
openingDoor.WithInvoke(async (cancel) => {
await door.Open();
}, "waitingUserToStepIn", null);
waitingUserToStepIn.WithInvoke(async (cancel) => {
while(cabin.LoadWeight == 0)
{
// here is the example where we need to use cancellation token
// we await for user to step in by checking the cabin load weight
// notice the below WithTimeout service, it will cancel the other services execution if
// after 30 sec no other services raised any event or finalized
// and it will move state machine to "closingTheDoor" state (not implemented in this example)
if(cancel.IsCancellationRequested)
{
// nobody stepped in to the elevator,
// exiting the loop.
return;
}
// wait a bit and check again
await Task.Delay(1000);
}
}, "waitingUserToSelectFloor", null)
// where to go if no other services invoked any event, a timeout service. See corresponding chapter for
// details. If other services are cancelled, their transitions are not executed
.WithTimeout(30000, "closingTheDoor");
// creating a state machine and running it
StateMachine elevatorMachine = new StateMachine("elevatorMachine",
"State machine for the elevator",
"waitingForUserRequest");
// set states
elevatorMachine.States = new []{
waitingForUserRequest,
movingToRequestedFloor,
stoppingAtFloor,
openingDoor,
waitingUserToStepIn
};
// start the state machine
Interpreter interpreter = new Interpreter(elevatorMachine);
interpreter.StartStateMachine();
Cancellation Token
IsCancellationRequested Property
The awaitable service had the CancellationToken
as argument. It is useful if state has multiple services and first service finalizes work first, then all other services must be cancelled.
You must check cancellation token IsCancellationRequested
property and stop your service, if it's value is true
.
For example:
// creating the state with three on enter actions
// all of them are asyncronouse.
State state1 = new State("movingCabinToRequestedFloor")
// register service that starts elevator moving
// and checkign while it is moving, if cabin is ar requested floor
// if yes, it switches to "openingDoor"
.WithInvoke(async (cancel) => {
// start elevator motor with speed 10
motor.start(10);
// wait until we reached the needed floor
while(currentFloor != requestedFloor)
{
// check if this was cancelled by user
if(cancel.IsCancellationRequested)
{
return;
}
await Task.Delay(1000);
}
motor.stop();
}, "openingDoor", null)
.WithInvoke(async (callback) => {
// subscribe for user's cancel request by STOP button
commands.onStopRequested(() => {
// if user pressed STOP button, emit event to stop the elevator
await callback("STOP_REQUESTED")
});
})
// when stop is requetsed, cabin is then moved to nearest floor
.WithTransition("STOP_REQUESTED", "movingToNearestFloor");
The example state above has two competing parallel services. One of then is awaitable service that has OnDone
and OnError
target state IDs that are activated on service is done (error target state ID is null
).
Another service is async with callback, it reacts on user pressed the "STOP" button (imagine one in the elevator). Once pressed, the state machine is moved to "movingToNearestFloor"
state and the previous service need to be cancelled.
hence we also check the cancel.IsCancellationRequested
property and exiting the while
loop if it was cancelled.
If cancellation token has IsCancellationRequested
equals to true
, no onDone
state is activated.
Register Action on Token is Cancelled
Another way to get known if cancellation token was cancelled is to use its Register(Action action)
method.
You can register a callback and perform action on cancellation token is cancelled.
// creating the state with three on enter actions
// all of them are asyncronouse.
State state1 = new State("movingCabinToRequestedFloor")
// register service that starts elevator moving
// and checkign while it is moving, if cabin is ar requested floor
// if yes, it switches to "openingDoor"
.WithInvoke(async (cancel) => {
// start elevator motor with speed 10
motor.start(10);
bool stop = false;
// receive a call when token was cancelled
cancel.Register(() => {
stop = true;
});
// wait until we reached the needed floor
while((currentFloor != requestedFloor) && !stop)
{
await Task.Delay(1000);
}
motor.stop();
}, "openingDoor", null)
.WithInvoke(async (callback) => {
// subscribe for user's cancel request by STOP button
commands.onStopRequested(() => {
// if user pressed STOP button, emit event to stop the elevator
await callback("STOP_REQUESTED")
});
})
// when stop is requetsed, cabin is then moved to nearest floor
.WithTransition("STOP_REQUESTED", "movingToNearestFloor");