The partitioned dynamic supervision of FSM-backed workers.
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 different 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 c: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.
def deps do
[
{:siblings, "~> 0.1"}
]
end
0.11.3
— OTP26 ready0.11.2
— [FIX] wrong specs forstart_link/1
andchild_spec/1
0.11.1
— upgraded toFinitomata
(v0.11.0
)0.11.0
— throttler → generic + on perform0.10.3
— accept{(any() -> :ok), timeout}
asdie_with_children
, write-onlyInternalState
0.10.2
— accept(any() -> :ok)
asdie_with_children
option as a callback0.10.0
—die_with_children: boolean()
option0.8.2
— updated with lastfinitomata
compiler0.7.0
—Siblings.state/{0,1,2,3}
+ update toFinitoma 0.7
0.5.1
— allow{:reschedule, non_neg_integer()}
return fromperform/3
0.5.0
— use FSM for theSibling.Lookup
0.4.3
— accepthibernate?:
boolean parameter in call toSiblings.start_child/4
to hibernate children0.4.2
— acceptworkers:
in call toSiblings.child_spec/1
to statically initializeSiblings
0.4.1
— [BUG] many namedSiblings
instances0.4.0
—Siblings.{multi_call/2, multi_transition/3}
0.3.3
—Siblings.{state/1, payload/2}
0.3.2
—Siblings.{call/3, reset/3, transition/4}
0.3.1
— retrieve childrens as bothmap
andlist
0.3.0
—GenServer.cast(pid, {:reset, interval})
andGenServer.call(pid, {:message, message})
0.2.0
— FastWorker
lookup0.1.0
— Initial MVP