GenStateMachine

repository·master·Indexed 43 days ago

https://github.com/ericentin/gen_state_machine

An idiomatic Elixir wrapper for the Erlang gen_statem behavior (OTP 20+). It simplifies state machine definitions with support for :handle_event_function and :state_functions callback modes, provides an OTP error translator for Logger, and includes client APIs for synchronous calls and asynchronous casts.

Tokens
1.9K
Snippets
9
Records
9
Agent score
5%

What's inside GenStateMachine

  1. Install GenStateMachine via Mix

    master

    To use GenStateMachine in your Elixir project, add it to your mix.exs dependencies and ensure it is included in your application list.

    Note: If you do not want the OTP error translator to be automatically added to Logger, add :gen_state_machine to :included_applications instead of :applications.

    # In mix.exs
    def deps do
      [{:gen_state_machine, "~> 3.0"}]
    end
    
    def application do
      [applications: [:gen_state_machine]]
    end
  2. Implement a state machine with GenStateMachine

    master

    To implement a state machine, use the GenStateMachine behaviour in your module. You can choose between two primary callback modes via the callback_mode option in use GenStateMachine/2:

    1. :handle_event_function (Default): You implement a single handle_event/4 callback that receives the event type, content, current state, and data. This is the standard approach.
    2. :state_functions: You implement functions named after your states (e.g., off/3, on/3). When the machine is in a specific state, the corresponding function is called. This is useful for complex state logic.

    You can also enable :state_enter by passing it in a list to callback_mode, allowing you to register for :enter events.

    Important: Never use a manual receive loop inside your callbacks; instead, handle custom messages using the :info event type.

    defmodule Switch do
      use GenStateMachine
    
      # Default mode: handle_event/4
      def handle_event(:cast, :flip, :off, data) do
        {:next_state, :on, data + 1}
      end
    
      def handle_event({:call, from}, :get_count, state, data) do
        {:next_state, state, data, [{:reply, from, data}]}
      end
    end
  3. Use the :state_functions callback mode

    master

    In :state_functions mode, the state machine dispatches events to functions named after the current state. For example, if the state is :off, the machine will call off/3 (for :enter events) or off/4 (for other events).

    To use this mode, call use GenStateMachine, callback_mode: :state_functions.

    defmodule Switch do
      use GenStateMachine, callback_mode: :state_functions
    
      def off(:cast, :flip, data) do
        {:next_state, :on, data + 1}
      end
    
      def off({:call, from}, :get_count, data) do
        {:keep_state_and_data, [{:reply, from, data}]}
      end
    
      def on(:cast, :flip, data) do
        {:next_state, :off, data}
      end
    end
  4. Register a GenStateMachine by name

    master

    When starting a server, you can use the :name option to register it. This allows you to interact with the machine using the name instead of a pid.

    Supported registration types:

    • atom: Local registration via Process.register/2.
    • {:global, term}: Global registration via the :global module.
    • {:via, module, term}: Registration via a custom registry module (must export register_name/2, unregister_name/1, whereis_name/1, and send/2).
    # Start and register locally
    {:ok, _} = GenStateMachine.start_link(Switch, {:off, 0}, name: MySwitch)
    
    # Use the name to call
    GenStateMachine.call(MySwitch, :get_count)
  5. Start a GenStateMachine server

    master

    You can start a state machine using GenStateMachine.start_link/3 (linked to the caller, suitable for supervision trees) or GenStateMachine.start/3 (unlinked).

    Both functions require the module implementing the behaviour and an initial argument (typically a tuple containing the initial state and initial data).

    Options:

    • :name - Register the process locally (atom), globally ({:global, term}), or via a registry ({:via, module, term}).
    • :timeout - Maximum milliseconds allowed for init/1 to complete.
    • :debug - If present, invokes :sys.debug/1.
    • :spawn_opt - Options passed to the underlying process (e.g., Process.spawn/4).
    defmodule Switch do
      use GenStateMachine
      def start_link, do: GenStateMachine.start_link(__MODULE__, {:off, 0})
    end
    
    {:ok, pid} = Switch.start_link()
  6. Send replies to clients manually

    master

    If you cannot return a reply directly from a state function (e.g., because the reply is delayed), use GenStateMachine.reply/2.

    To do this, you must capture the from argument from a {:call, from} event and pass it to reply/2 later, often inside an :info event handler.

    def handle_event({:call, from}, :reply_in_one_second, state, data) do
      Process.send_after(self(), {:reply, from}, 1_000)
      :keep_state_and_data
    end
    
    def handle_event(:info, {:reply, from}, state, data) do
      GenStateMachine.reply(from, :one_second_has_passed)
    end
  7. Communicate with a GenStateMachine (call and cast)

    master

    Interact with a running state machine using these client APIs:

    • GenStateMachine.call(server, request, timeout \ :infinity): A synchronous call that waits for a reply. If no reply arrives within the timeout, the caller exits.
    • GenStateMachine.cast(server, request): An asynchronous request that returns :ok immediately. It does not wait for a response.

    server can be a pid, a locally registered atom, a {:global, term}, or a {:via, module, name} tuple.

    GenStateMachine.cast(pid, :flip)
    GenStateMachine.call(pid, :get_count)
  8. Implement the terminate/2 callback

    master

    The terminate/2 callback is invoked when the server is about to exit. It is used for cleanup.

    Parameters:

    • reason: The exit reason (e.g., :normal, :shutdown, or an error reason).
    • state: The current state.
    • data: The current data.

    Note: terminate/2 is not guaranteed to be called if the process is killed via :brutal_kill in a supervisor or if it crashes abruptly without trapping exits. For critical resources like sockets, rely on the OS or linked processes for cleanup.

    def terminate(reason, state, data) do
      # Perform cleanup here
      :ok
    end
  9. Implement the init/1 callback

    master

    The init/1 callback is called when the server starts. It receives the arguments passed to start_link/3 or start/3.

    Return values:

    • {:ok, state, data}: Starts the loop and returns {:ok, pid} to the caller.
    • {:ok, state, data, actions}: Starts the loop and executes the provided actions.
    • :ignore: The process exits normally without entering the loop. Useful for disabling features via configuration without failing a supervisor.
    • {:stop, reason}: The process exits with reason, and start_link/3 returns {:error, reason}.
    def init(args) do
      # args is the second argument from start_link
      {:ok, :initial_state, args}
    end