This short blog post is a third in my series of Finite State Machines in Elixir. I wrote the first post a year ago never realizing what I had learnt I would get to apply for one of our projects.
A recent client asked us to write an OTP application providing a framework to streamline some of their business processes. Without going into details, what they essentially needed was a series of steps with actions being taken on or after each step and a automatic timeout mechanism at certain points. This problem lended itself perfectly to a finite state machine with automatic transitions based off a timer between states. So below is snippets of production code, a bit obfuscated to reduce noise.
Of course we are utitlizing everyones favorite Elixir library FSM for most of the FSM boilerplate and when I started on this project I did alot of things wrong. We knew our state machine would be serialized into a json field in the database and that database record would be responsible for handling events and maintaining state. A much much smarter collegue of mine had the idea to wrap the original FSM library code in custom macros, utitlizing Elixir meta-goodness to append the functionality that we needed. In our case, that is the timeout’s between states. We defined some state macros to accept a timeouts definition and append those as extra data within the FSM definition at compile time. Upside, it works and its quite elegants. Downside, timeouts are defined pre-runtime, an issue that was not necassary per our clients use-case.
fsm.ex
defmacro defgstate(state, opts, state_def) do
{state_name, _, _} = state
quote do
state_struct = %FSMState{name: unquote(state_name)}
if(unquote(opts)[:timeout]) do
state_struct = %FSMState{state_struct | timeout: unquote(opts)[:timeout]}
end
@accumulated_states state_struct
defstate(unquote(state), unquote(state_def))
end
end
Below is our actual definition of a sample state within our FSM. The timeout keyword can accept whatever you lide, remember the FSM is not responsible to sending the actually timeout messages, that is handled elsewhere. That was the key for us to keep the implementation seperate.
fsm_definition.ex
defgstate waiting_for_number_1, timeout: [{:send_1, 5000}] do
defgevent send_1(_), transitions_to: [:waiting_for_number_1], data: data do
IO.puts("#{inspect :calendar.local_time} auto transition waiting_for_number_1")
next_state(:waiting_for_number_1, data)
end
end
There is one more piece of the code that allows us to ask an FSM for all its current states and associated timeouts. We accomplished this with a macro that accumulates all the states and their timeouts within module definitions. Again, a much smarter individual was responsible for this code as I’m still getting the hang of Elixir meta-goodness.
fsm.ex
defmacro __before_compile__(_) do
quote do
@the_states (for state <- @accumulated_states do
state_transitions = for {state_name, transitions_to} <- @transitions, state_name == state.name do
transitions_to
end
%FSMState{state | transitions: state_transitions}
end |> Enum.reverse)
def states do
@the_states
end
def current_state(fsm) do
states
|> Enum.find(fn(state) -> state.name == fsm.state end)
end
def timeout(fsm) do
current_state(fsm).timeout
end
end
end
And below is the code that will calculate our timeout in miliseconds. It essentailly goes through the timeout array returning the most recent timer tuple that is not past the current datetime and converting its datetime to a timeout in miliseconds or just returning the milisecond number as we wanted the ability to specify a datetime or an integer of miliseconds (intervals).
timeout_server.ex
def do_calculate_timeout(t) when is_integer(t), do: t
def do_calculate_timeout(t={{_,_,_},{_,_,_}}) do
secs = t |> :calendar.datetime_to_gregorian_seconds
now = :calendar.datetime_to_gregorian_seconds(:calendar.local_time)
(secs - now) * 1000
end
def parse_timeout(t) when is_integer(t), do: {:default_event, t}
def parse_timeout({event, time}), do: {{:handle_event, {event, {event,0}}}, time}
def calculate_timeout(t) when is_integer(t), do: {:default_event, t}
def calculate_timeout(t={{_,_,_},{_,_,_}}), do: {:default_event, do_calculate_timeout(t)}
def calculate_timeout([]), do: {:default_event, -1}
def calculate_timeout([head | tail]) do
{event, time} = parse_timeout(head)
case do_calculate_timeout(time) do
x when x > 0 -> {event, x}
x when x <= 0 -> calculate_timeout(tail)
end
end
Cool, huh? I loved how we were able to keep data seperate from implementation in our FSM defintion.