Papillon6814

Papillon6814

Hello. I 'm creating a mix project which builds a finite state machine on redis. While I’m writing code of the repository, I referred a repository finist which is written in ruby.

Implementation

defmodule Dfm do
  @moduledoc """
  Documentation for `Dfm`.
  """

  require Logger

  @script """
  local curr = redis.call("GET", KEYS[1])
  local next = redis.call("HGET", KEYS[2], curr)
  if next then
    redis.call("SET", KEYS[1], next)
    return { next, true }
  else
    return { curr, false }
  end
  """

  @redis_host "localhost"
  @redis_port 6379

  defp conn() do
    with {:ok, conn} <- Redix.start_link(host: @redis_host, port: @redis_port) do
      conn
    else
      error -> raise "Failed to connect to #{@redis_host}:#{@redis_port} #{error}"
    end
  end

  def flushall() do
    conn = conn()
    Redix.command(conn, ["FLUSHALL"])

    Logger.info("Flushed all")
  end

  @doc """
  Initializes state of automaton.
  """
  @spec initialize(String.t(), integer(), String.t()) :: Redix.Protocol.redis_value()
  def initialize(key_name, db_index, initial_state) do
    conn = conn()
    name = name(key_name)

    Redix.command!(conn, ["SELECT", db_index])
    Redix.command!(conn, ["SET", name, initial_state, "NX"])
  end

  @spec name(String.t()) :: String.t()
  defp name(key_name), do: "finite:#{key_name}"

  @spec event_key(String.t(), String.t()) :: String.t()
  defp event_key(key_name, event), do: "#{key_name}:#{event}"

  @doc """
  Defines how automaton changes the state.
  """
  @spec on(String.t(), integer(), String.t(), String.t(), String.t()) :: Redix.Protocol.redis_value()
  def on(key_name, db_index, event, current_state, next_state) do
    conn = conn()

    Redix.command!(conn, ["SELECT", db_index])
    Redix.command!(conn, ["HSET", event_key(key_name, event), current_state, next_state])
  end

  @doc """
  Removes a pattern of state change.
  """
  @spec rm(String.t(), integer(), String.t()) :: Redix.Protocol.redis_value()
  def rm(key_name, db_index, event) do
    conn = conn()

    Redix.command!(conn, ["SELECT", db_index])
    Redix.command!(conn, ["HDEL", event_key(key_name, event)])
  end

  @doc """
  Return current state.
  """
  @spec state(String.t(), integer()) :: Redix.Protocol.redis_value()
  def state(key_name, db_index) do
    conn = conn()

    Redix.command!(conn, ["SELECT", db_index])
    Redix.command!(conn, ["GET", name(key_name)])
  end

  @spec send_event(String.t(), integer(), String.t()) :: Redix.Protocol.redis_value()
  defp send_event(key_name, db_index, event) do
    conn = conn()

    Redix.command!(conn, ["SELECT", db_index])
    Redix.command!(conn, ["EVAL", @script, 2, name(key_name), event_key(key_name, event)])
  end

  @doc """
  Triggers state change.
  """
  @spec trigger(String.t(), integer(), String.t()) :: {:ok, String.t()} | {:error, String.t()}
  def trigger(key_name, db_index, event) do
    [state, result] = send_event(key_name, db_index, event)
    do_trigger(state, result)
  end

  defp do_trigger(state, nil), do: {:error, state}
  defp do_trigger(state, _), do: {:ok, state}
end

Test

defmodule DfmTest do
  use ExUnit.Case
  doctest Dfm

  describe "dfm" do
    test "simple automaton" do
      Dfm.flushall()

      1..5
      |> Enum.to_list()
      |> Enum.each(fn n ->
        key_name = "user#{n}"
        db_index = 15

        state1 = "a"
        state2 = "b"
        state3 = "c"
        trigger1 = "x"
        trigger2 = "y"
        invalid_trigger = "invalid"

        # NOTE: Define state changes
        Dfm.initialize(key_name, db_index, state1)
        Dfm.on(key_name, db_index, trigger1, state1, state2)
        Dfm.on(key_name, db_index, trigger1, state2, state3)
        Dfm.on(key_name, db_index, trigger1, state3, state1)
        Dfm.on(key_name, db_index, trigger2, state1, state3)

        assert Dfm.state(key_name, db_index) == state1
        assert {:ok, state3} = Dfm.trigger(key_name, db_index, trigger1)
        assert Dfm.state(key_name, db_index) == state2
        assert {:ok, state3} = Dfm.trigger(key_name, db_index, trigger1)
        assert Dfm.state(key_name, db_index) == state3
        assert {:ok, state3} = Dfm.trigger(key_name, db_index, trigger1)
        assert Dfm.state(key_name, db_index) == state1
        assert {:ok, state3} = Dfm.trigger(key_name, db_index, trigger2)
        assert Dfm.state(key_name, db_index) == state3

        # NOTE: Invalid patterns
        assert {:error, state3} = Dfm.trigger(key_name, db_index, trigger2)
        assert {:error, state3} = Dfm.trigger(key_name, db_index, invalid_trigger)
      end)
    end
  end
end

That’s all of this repository. I think it works as a finite state machine properly. But I’m not a skilled engineer, so I’d like to show the codes to everyone. I want advice for the repository!
Thank you.

Showing Posts 1 to 3

karlosmid

karlosmid

Hi, based on principle do not invent your own wheel, you should implement your finite state machine using this simple library that will guarantee that your machine is a finite state machine:

https://github.com/subvisual/fsmx

Sebb

Sebb

Or dont use any lib at all.

def handle(:state_1, :signal_a) do
  ...
  :new_state
end
MrDoops

MrDoops

I’d recommend a vanilla elixir module and struct + functions for the state machine model. Then a GenServer (possibly dynamically supervised & registered) wrapping the FSM struct and persisting to a system of your choice (Redis, Postgres, etc).

— All posts loaded —

Where Next? Top

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
jonnycharles
I’m in search of an Elixir library that offers PDF generation capabilities similar to Ruby’s Prawn. While there have been discussions abo...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
dli
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app? Looking for hints regarding: Addi...
New
roeland
Kia ora, We have been using elixir-google-api to connect to Google Drive. However, with the updates to Tesla due to CVEs this is now bro...
New
bottlenecked
Hi all, I wanted to ask how the community is dealing with post-release steps. Today we have Ecto migrations, which make sure that the db...
New
rahultumpala
Hello, I have an Elixir backend that implements a custom protocol over TCP. I want to load test the backend and assess the performance o...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews