Menu Close

A Tour Of Finite State Machines Part 3

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.