View Source Siblings    Kantox ❤ OSS  Test  Dialyzer

The partitioned dynamic supervision of FSM-backed workers.

usage

Usage

Siblings is a library to painlessly manage many uniform processes, all having the lifecycle and the FSM behind.

Consider the service, that polls the market rates from several diffferent sources, allowing semi-automated trading based on predefined conditions. For each bid, the process is to be spawn, polling the external resources. Once the bid condition is met, the bid gets traded.

With Siblings, one should implement Siblings.Worker.perform/3 callback, doing actual work and returning either :ok if no action should be taken, or {:transition, event, payload} to initiate the FSM transition. When the FSM get exhausted (reaches its end state,) both the performing process and the FSM itself do shut down.

FSM instances leverage Finitomata library, which should be used alone if no recurrent perform should be accomplished or if the instances are not uniform.

Typical code for the Siblings.Worker implementation would be as follows

defmodule MyApp.Worker do
  @fsm """
  born --> |reject| rejected
  born --> |bid| traded
  """

  use Finitomata, @fsm

  def on_transition(:born, :reject, _nil, payload) do
    perform_rejection(payload)
    {:ok, :rejected, payload}
  end

  def on_transition(:born, :bid, _nil, payload) do
    perform_bidding(payload)
    {:ok, :traded, payload}
  end

  @behaviour Siblings.Worker

  @impl Siblings.Worker
  def perform(state, id, payload)

  def perform(:born, id, payload) do
    cond do
      time_to_bid?() -> {:transition, :bid, nil}
      stale?() -> {:transition, :reject, nil}
      true -> :ok
    end
  end

  def perform(:rejected, id, _payload) do
    Logger.info("The bid #{id} was rejected")
    {:transition, :__end__, nil}
  end

  def perform(:traded, id, _payload) do
    Logger.info("The bid #{id} was traded")
    {:transition, :__end__, nil}
  end
end

Now it can be used as shown below

{:ok, pid} = Siblings.start_link()
Siblings.start_child(MyApp.Worker, "Bid1", %{}, interval: 1_000)
Siblings.start_child(MyApp.Worker, "Bid2", %{}, interval: 1_000)
...

The above would spawn two processes, checking the conditions once per a second (interval,) and manipulating the underlying FSM to walk through the bids’ lifecycles.

Worker’s interval might be reset with GenServer.cast(pid, {:reset, interval}) and the message might be casted to it with GenServer.call(pid, {:message, message}). For the latter to work, the optional callback on_call/2 must be implemented.

Sidenote: Normally, Siblings supervisor would be put into the supervision tree of the target application.

installation

Installation

def deps do
  [
    {:siblings, "~> 0.1"}
  ]
end

changelog

Changelog

  • 0.3.3Siblings.{state/1. payload/2}
  • 0.3.2Siblings.{call/3, reset/3, transition/4}
  • 0.3.1 — retrieve childrens as both map and list
  • 0.3.0GenServer.cast(pid, {:reset, interval}) and GenServer.call(pid, {:message, message})
  • 0.2.0 — Fast Worker lookup
  • 0.1.0 — Initial MVP

documentation

Documentation