View Source Siblings (Siblings v0.8.1)

Bolerplate to effectively handle many long-lived entities of the same shape, driven by FSM.

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 -> :noop
    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.

Link to this section Summary

Functions

Returns the child spec for the named or unnamed Siblings process.

Initiates the transition of all the workers.

Returns the payload of FSM behind the named worker.

Resets the the named worker’s interval.

Starts the supervised child under the PartitionSupervisor.

Starts the supervision subtree, holding the PartitionSupervisor.

Returns the state of the Siblings instance itself, of the named worker, or the named worker’s underlying FSM, depending on the first argument.

Returns the states of all the workers as a map.

Initiates the transition of the named worker.

Link to this section Types

@type worker() :: %{
  module: module(),
  id: Siblings.Worker.id(),
  payload: Siblings.Worker.payload(),
  options: Siblings.InternalWorker.options()
}

Link to this section Functions

Link to this function

call(name \\ default_fqn(), id, message)

View Source
@spec call(module(), Siblings.Worker.id(), Siblings.Worker.message()) ::
  Siblings.Worker.call_result() | {:error, :callback_not_implemented}

Performs a GenServer.call/3 on the named worker.

@spec child_spec([
  {:name, module()} | {:lookup, boolean() | module()} | {:id, any()} | keyword()
]) ::
  Supervisor.child_spec()

Returns the child spec for the named or unnamed Siblings process.

Useful when many Siblings processes are running simultaneously.

Link to this function

multi_call(name \\ default_fqn(), message)

View Source
@spec multi_call(module(), Siblings.Worker.message()) :: [
  Siblings.Worker.call_result() | {:error, :callback_not_implemented}
]

Performs a GenServer.call/3 on all the workers.

Link to this function

multi_transition(name \\ default_fqn(), event, payload)

View Source
@spec multi_transition(
  module(),
  Finitomata.Transition.event(),
  Finitomata.event_payload()
) :: :ok

Initiates the transition of all the workers.

Link to this function

payload(name \\ default_fqn(), id)

View Source

Returns the payload of FSM behind the named worker.

Link to this function

reset(name \\ default_fqn(), id, interval)

View Source
@spec reset(module(), Siblings.Worker.id(), non_neg_integer()) :: :ok

Resets the the named worker’s interval.

Link to this function

start_child(worker, id, payload, opts \\ [])

View Source

Starts the supervised child under the PartitionSupervisor.

Starts the supervision subtree, holding the PartitionSupervisor.

This is the main entry point of Siblings. It starts the supervision tree, holding the partitioned DynamicSupervisors, the lookup to access children, and the optional set of workers to start immediately.

Siblings are fully controlled by FSM instances. Children are added using Siblings.Lookup interface methods which go all the way through underlying FSM implementation.

opts might include:

  • name: atom() which is a name of the Siblings instance, defaults to Siblings
  • workers: list() the list of the workers to start imminently upon Siblings start
  • callbacks: list() the list of the handler to call back upon Lookup transitions
Link to this function

state(request \\ :instance, id \\ nil, name \\ default_fqn())

View Source
@spec state(
  request :: :instance | :sibling | :fsm | Siblings.Worker.id(),
  Siblings.Worker.id() | module(),
  module()
) :: nil | Siblings.InternalWorker.State.t() | Finitomata.State.t()

Returns the state of the Siblings instance itself, of the named worker, or the named worker’s underlying FSM, depending on the first argument.

Link to this function

states(name \\ default_fqn())

View Source
@spec states(module()) :: %{required(Siblings.Worker.id()) => Finitomata.State.t()}

Returns the states of all the workers as a map.

Link to this function

transition(name \\ default_fqn(), id, event, payload)

View Source

Initiates the transition of the named worker.