# Siblings [![Kantox ❤ OSS](https://img.shields.io/badge/❤-kantox_oss-informational.svg)](https://kantox.com/) [![Test](https://github.com/am-kantox/siblings/workflows/Test/badge.svg)](https://github.com/am-kantox/siblings/actions?query=workflow%3ATest) [![Dialyzer](https://github.com/am-kantox/siblings/workflows/Dialyzer/badge.svg)](https://github.com/am-kantox/siblings/actions?query=workflow%3ADialyzer)
**The partitioned dynamic supervision of FSM-backed workers.**
## 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 `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`](https://hexdocs.pm/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
```elixir
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
```elixir
{: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
```elixir
def deps do
[
{:siblings, "~> 0.1"}
]
end
```
## Changelog
* `0.11.3` — OTP26 ready
* `0.11.2` — [FIX] wrong specs for `start_link/1` and `child_spec/1`
* `0.11.1` — upgraded to `Finitomata` (`v0.11.0`)
* `0.11.0` — throttler → generic + on perform
* `0.10.3` — accept `{(any() -> :ok), timeout}` as `die_with_children`, write-only `InternalState`
* `0.10.2` — accept `(any() -> :ok)` as `die_with_children` option as a callback
* `0.10.0` — `die_with_children: boolean()` option
* `0.8.2` — updated with last `finitomata` compiler
* `0.7.0` — `Siblings.state/{0,1,2,3}` + update to `Finitoma 0.7`
* `0.5.1` — allow `{:reschedule, non_neg_integer()}` return from `perform/3`
* `0.5.0` — use _FSM_ for the `Sibling.Lookup`
* `0.4.3` — accept `hibernate?:` boolean parameter in call to `Siblings.start_child/4` to hibernate children
* `0.4.2` — accept `workers:` in call to `Siblings.child_spec/1` to statically initialize `Siblings`
* `0.4.1` — [BUG] many named `Siblings` instances
* `0.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 both `map` and `list`
* `0.3.0` — `GenServer.cast(pid, {:reset, interval})` and `GenServer.call(pid, {:message, message})`
* `0.2.0` — Fast `Worker` lookup
* `0.1.0` — Initial MVP
## [Documentation](https://hexdocs.pm/siblings)