state_server v0.4.10 StateServer.State behaviour View Source

A behaviour that lets you organize code for your StateServer states.

Organization

When you define your StateServer, the StateServer module gives you the opportunity to define state modules. These are typically (but not necessarily) submodules scoped under the main StateServer module. In this way, your code for handling events can be neatly organized by state. In some (but not all) cases, this may be the most appropriate way to keep your state machine codebase sane.

Defining the state module.

the basic syntax for defining a state module is as follows:

defstate MyModule, for: :state do
  # ... code goes here ...

  def handle_call(:increment, _from, data) do
    {:reply, :ok, update: data + 1}
  end
end

note that the callback directives defined in this module are identical to those of StateServer, except that they are missing the state argument.

External state modules

You might want to use an external module to handle event processing for one your state machine. Reasons might include:

  • to enable code reuse between state machines
  • if your codebase is getting too long and you would like to put state modules in different files.

If you choose to do so, there is a short form defstate call, which is as follows:

defstate ExternalModule, for: :state

Be sure to mark your ExternalModule as having the StateServer.State behaviour.

Precedence and delegate statements

Note that handle_* functions written directly in the body of the StateServer take precedence over any functions written as a part of a state module. In the case where there are competing function calls, your handler functions written in the body of the StateServer may emit :delegate as a result, which will punt the processing of the event to the state modules.

# make sure query calls happen regardless of state
def handle_call(:query, _from, _state, data) do
  {:reply, {state, data}}
end
# for all other call payloads, send to the state modules
def handle_call(_, _, _, _) do
  :delegate
end

defstate Start, for: :start do
  def handle_call(...) do...

since this is a common pattern, we provide a delegate macro which is equivalent to the above:

# make sure query calls happen regardless of state
def handle_call(:query, _from, _state, data) do
  {:reply, {state, data}}
end
# for all other call payloads, send to the state modules
delegate :handle_call

Important

If you handle an event via any instance of a handler function block in the main StateServer module, and return a :reply or :noreply, it will not be handled by the State module, you must explicitly specify :delegate to be handled by State modules.

If there are no instances of the handler function, then handling will default to the State modules without using the delegate macro.

delegate with common events

If you would like to perform analysis on the inbound data, generating events and delegate to the individual states for further state-specific event processing, you may do so with the {:delegate, events} result type. For example, the following code:

# perform some common processing
def handle_call({:query, payload}, _from, _state, data) do
  common_events = generate_common_events_from(payload)
  {:delegate, common_events}
end

defstate Start, for: :start do
  def handle_call({:query, payload}, _from, data) do
    # ...some code here...
    {:reply, result, start_events}
  end
end

Will result in the event stream common_events ++ start_events emitted when {:query, payload} is called to the state server, with the following exception:

  • If you have an {:update, <new_data>} in the first position, or in the second position with a {:goto, <state>}, or {:transition, <state>} in the first position, the update event will be reflected in the delegated state machine call.

ignore/1

inside of a defstate module you may use the ignore/1 macro, which used as follows:

ignore :handle_cast

and inserts the following stanza:

def handle_cast(_, _), do: :noreply

this works for handle_cast, handle_info, handle_continue, handle_internal, handle_transition, on_state_entry, and terminate.

Termination rules

If a State module implements the terminate/2 callback, then it will be called on termination. If it does not, termination will follow the parent StateServer's StateServer.terminate/3 if it exists. Otherwise, no action will be taken on terminate.

Example

The following code should produce a "light switch" state server that announces when it's been flipped.

defmodule SwitchWithStates do

  @doc """
  implements a light switch as a state server.  In data, it keeps a count of
  how many times the state of the light switch has changed.

  On transition, it sends to standard error a comment that it has been flipped.
  Note that the implementations are different between the two states.
  """

  use StateServer, off: [flip: :on],
                   on:  [flip: :off]

  @type data :: non_neg_integer

  def start_link, do: StateServer.start_link(__MODULE__, :ok)

  @impl true
  def init(:ok), do: {:ok, 0}

  def flip(srv), do: StateServer.call(srv, :flip)
  def query(srv), do: StateServer.call(srv, :query)

  @impl true
  def handle_call(:flip, _from, _state, _count) do
    {:reply, :ok, transition: :flip}
  end
  
  delegate :handle_call
  # we must delegate the handle_call statement because there are both shared and
  # individual implementation of handle_call features.

  defstate Off, for: :off do
    @impl true
    def handle_transition(:flip, count) do
      IO.puts(:stderr, "switch #{inspect self()} flipped on, #{count} times turned on")
      {:noreply, update: count + 1}
    end

    @impl true
    def handle_call(:query, _from, _count) do
      {:reply, "state is off"}
    end
  end

  defstate On, for: :on do
    @impl true
    def handle_transition(:flip, count) do
      IO.puts(:stderr, "switch #{inspect self()} flipped off, #{count} times turned on")
      :noreply
    end

    @impl true
    def handle_call(:query, _from, _count) do
      {:reply, "state is on"}
    end
  end

end

Link to this section Summary

Link to this section Callbacks

Link to this callback

handle_call(term, from, data)

View Source (optional)
handle_call(term(), from(), data :: term()) ::
  reply_response() | noreply_response() | stop_response()
Link to this callback

handle_cast(term, data)

View Source (optional)
handle_cast(term(), data :: term()) :: noreply_response() | stop_response()
Link to this callback

handle_continue(term, data)

View Source (optional)
handle_continue(term(), data :: term()) :: noreply_response() | stop_response()
Link to this callback

handle_info(term, data)

View Source (optional)
handle_info(term(), data :: term()) :: noreply_response() | stop_response()
Link to this callback

handle_internal(term, data)

View Source (optional)
handle_internal(term(), data :: term()) :: noreply_response() | stop_response()
Link to this callback

handle_timeout(term, data)

View Source (optional)
handle_timeout(term(), data :: term()) :: noreply_response() | stop_response()
Link to this callback

handle_transition(transition, data)

View Source (optional)
handle_transition(transition :: atom(), data :: term()) ::
  noreply_response() | stop_response() | :cancel
Link to this callback

on_state_entry(transition, data)

View Source (optional)
on_state_entry(transition :: atom(), data :: term()) ::
  StateServer.on_state_entry_response()
Link to this callback

terminate(reason, data)

View Source (optional)
terminate(reason :: term(), data :: term()) :: term()