View Source Siblings (Siblings v0.11.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
Performs a GenServer.call/3
on the named worker.
Returns the child spec
for the named or unnamed Siblings
process.
Performs a GenServer.call/3
on all the workers.
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
@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.
@spec multi_call(module(), Siblings.Worker.message()) :: [ Siblings.Worker.call_result() | {:error, :callback_not_implemented} ]
Performs a GenServer.call/3
on all the workers.
@spec multi_transition( module(), Finitomata.Transition.event(), Finitomata.event_payload() ) :: :ok
Initiates the transition of all the workers.
@spec payload(module(), Siblings.Worker.id()) :: Siblings.Worker.payload()
Returns the payload of FSM behind the named worker.
@spec reset(module(), Siblings.Worker.id(), non_neg_integer()) :: :ok
Resets the the named worker’s interval.
@spec start_child( module(), Siblings.Worker.id(), Siblings.Worker.payload(), Siblings.InternalWorker.options() ) :: :ok | DynamicSupervisor.on_start_child()
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
DynamicSupervisor
s, 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 theSiblings
instance, defaults toSiblings
workers: list()
the list of the workers to start imminently uponSiblings
startthrottler: keyword()
the throttler options, seeSiblings.Throttler
for detailsdie_with_children: true | false | (-> :ok) | {(-> :ok), timeout}
shutdown the process when there is no more active child, defaults tofalse
(if a function of arity 0 is given, it’ll be called before the process shuts down)callbacks: list()
the list of the handler to call back uponLookup
transitions
@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.
@spec states(module()) :: %{required(Siblings.Worker.id()) => Finitomata.State.t()}
Returns the states of all the workers as a map.
@spec transition( module(), Siblings.Worker.id(), Finitomata.Transition.event(), Finitomata.event_payload() ) :: :ok
Initiates the transition of the named worker.