FSM
FTL has first-class support for distributed finite-state machines. Each state in the state machine is a Sink, with events being values of the type of each sinks input. The FSM is declared once, with each executing instance of the FSM identified by a unique key when sending an event to it.
Here's an example of an FSM that models a simple payment flow:
var payment = ftl.FSM(
"payment",
ftl.Start(Invoiced),
ftl.Start(Paid),
ftl.Transition(Invoiced, Paid),
ftl.Transition(Invoiced, Defaulted),
)
//ftl:verb
func SendDefaulted(ctx context.Context, in DefaultedInvoice) error {
return payment.Send(ctx, in.InvoiceID, in.Timeout)
}
//ftl:verb
func Invoiced(ctx context.Context, in Invoice) error {
if timedOut {
return ftl.CallAsync(ctx, SendDefaulted, Timeout{...})
}
}
//ftl:verb
func Paid(ctx context.Context, in Receipt) error { /* ... */ }
//ftl:verb
func Defaulted(ctx context.Context, in Timeout) error { /* ... */ }
Creating and transitioning instances
To send an event to an fsm instance, call Send()
on the FSM with the instance's unique key. The first time you send an event for an instance key, an fsm instance will be created.
An example of creating an FSM instance and then transitioning it through it's states is shown below:
err := payment.Send(ctx, invoiceID, Invoice {Amount: 110})
err = payment.Send(ctx, invoiceID, Receipt {Amount: 110})
When an event is sent to the FSM the method to be called is determined by matching the current state and event payload
type to methods that can transition from the current state and have the same payload type. In the example above the first
Send
call will created the FSM, and will call the Invoiced
method as it is a start state and takes an Invoice
as
payload. The second Send
call will call the Paid
method as it is a transition from the Invoiced
state and takes a
Receipt
as payload. If the second call had sent a Timeout
instead of a Receipt
the FSM would have called the Defaulted
method instead.
It is important to note that in this model the methods both represent a state, and a way to transition into that state. This means when a method is invoked it always moves to the corresponding state, consider the following example:
err := payment.Send(ctx, invoiceID, Invoice {Amount: 110})
err = payment.Send(ctx, invoiceID, Receipt {Amount: 20})
In this case it would still moved to the Paid
state even though the customer only paid 20 of the 110.
Sending an event to an FSM is asynchronous. From the time an event is sent until the state function completes execution, the FSM is transitioning. It is invalid to send an event to an FSM that is transitioning.
During a transition you may need to trigger a transition to another state. This can be done by calling Next()
on the FSM:
err := payment.Next(ctx, invoiceID, Receipt{...})