Peeper (peeper v0.3.1)
View SourcePeeper.GenServer
is an almost drop-in replacement for GenServer
that preserves the state
between crashes. All the callbacks from GenServer
are supported.
Internally it creates a sub-supervision tree with an actual worker that delegates to the implementation and a state keeper. That said, it creates three processes instead of one, and this should be considered when building a very concurrent applications.
The main use-case would be a long-lived GenServer
process with no error path handling at all
(yeah, that famous Let It Crash.) Using this abstraction, one is free to send unexpected
messages to the process, raise from its handle_×××
clauses and whatnot.
There are two differencies compared to bare GenServer
. init/1
callback cannot return anything
but {:ok, state}
or {:ok, new_state, timeout() | :hibernate | {:continue, term()}
tuples
(this might have changed in the future,) and Peeper.{call/3,cast/2,send/2}
is to be used
instead of GenServer.{call/3,cast/2}
and Kernel.send/2
(this is not gonna change.)
Please note, that whatever is set in init/1
callback as a state, will be overriden by
the latest state available upon restarts. That being said, init/1
would play its role
in setting the state during the first run only.
Instead of using Peeper
’s counterparts, one might either name the process and use Name.GenServer
as a name of the underlying GenServer
or get the GenServer
’s pid
via Peeper.gen_server/1
and use GenServer.{call/3,cast/2}
with it.
Example
iex> {:ok, pid} = Peeper.Impls.Full.start_link(state: 0, name: Counter)
...> Peeper.call(pid, :state)
0
iex> Peeper.cast(pid, :inc)
:ok
iex> GenServer.call(Peeper.gen_server(Counter), :state)
1
iex> Peeper.call(pid, :state)
1
iex> # emulate crash
...> Process.exit(Peeper.Supervisor.worker(pid), :raise)
...> %{} = Peeper.Supervisor.which_children(pid)
iex> Peeper.call(Counter, :state)
1
iex> Peeper.send(pid, :inc)
:inc
iex> Peeper.call(Counter, :state)
2
start_link/1
The function receives either an initial state
, or a keyword having keys
state
and (optionally) name
. Also this keyword might have a configuration
for the top supervisor (keys: [:strategy, :max_restarts, :max_seconds, :auto_shutdown]
.)
All other values passed would be re-passed to the underlying GenServer
’s options.
Keeping ETS
Peeper
can preserve ETS tables created by the wrapped process between crashes in several ways.
The proper solution would be to add a :heir
option to the ETS created by the process as
:ets.new(name, [:private, :ordered_set, Peeper.heir(MyNamedPeeper)])
That way the ETS will remain private and not readable by any other part of the system, although it’ll be preserved between crashes and might be transferred to another dynamic supervisor (see below.)
If for some reason setting :heir
explicitly is not an option, one might pass
keep_ets: true | :all | [ets_name()]
option to Peeper.start_link/1
. It’s not efficient, because
ETS content will be passed tyo the backing state process every time the change to it happens to occur.
If the ETS created by the underlined process has {:heir, other_process_pid()}
set, ithe behaviour
after a process crash is undefined, because ETS will be transferred to another process and reach out
of the control of Peeper
.
Moving to another DynamicSupervior
The Peeper
branch might be transferred an a whole to another dynamic supervisor in the following way
Peeper.transfer(MyNamedPeeper, source_dynamic_supervisor, target_dynamic_supervisor)
The target_dynamic_supervisor
might be a remote pid
, in such a case Peeper
must be compiled
and loaded on the target node.
Listener
One might pass listener: MyListener
key to PeeperImpl.start_link/1
where MyListener
should be an implementation of Peeper.Listener
behaviour. The callbacks will be called
when the state of the underlying behaviour is changed and on termination respectively.
That might be a good place to attach telemetry events, or logs, or whatnot.
Summary
Functions
The counterpart for GenServer.call/3
. Uses the very same semantics.
The counterpart for GenServer.cast/2
. Uses the very same semantics.
Returns a specification to start a branch under a supervisor.
Returns the pid
of the actual GenServer
. Might be used to
avoid the necessity of calling other functions in this module.
Tries to produce a name for the underlying GenServer
process.
Succeeds if the name passed to start_link/1
of the implementation
is an atom()
, {:global, atom()}
, or {:via, module(), atom()}
.
Returns a {:heir, pid(), heir_data}
tuple where pid
is the
pid of the state holder. Useful when the GenServer
process creates persistent ETS, the result of this function is
to be passed to ETS options as a config parameter.
The counterpart for Kernel.send/2
. Uses the very same semantics.
Starts a supervisor branch with the given options.
Transfers the whole Peeper
branch from one supervisor to another.
Types
@type ets_option() :: {:type, :ets.table_type()} | :public | :protected | :private | :named_table | {:keypos, pos_integer()} | {:heir, pid(), term()} | {:heir, :none} | {:write_concurrency, boolean() | :auto} | {:read_concurrency, boolean()} | {:decentralized_counters, boolean()} | :compressed
The option accepted by :ets.new/2
@type name() :: atom()
The name of the processes branch
Functions
The counterpart for GenServer.call/3
. Uses the very same semantics.
The counterpart for GenServer.cast/2
. Uses the very same semantics.
Returns a specification to start a branch under a supervisor.
Returns the pid
of the actual GenServer
. Might be used to
avoid the necessity of calling other functions in this module.
The pid
returned might be used directly in calls to
GenServer.{call/3,cast/2}
and/or Kernel.send/2
Tries to produce a name for the underlying GenServer
process.
Succeeds if the name passed to start_link/1
of the implementation
is an atom()
, {:global, atom()}
, or {:via, module(), atom()}
.
In such a case, the underlying GenServer
module receives the name
Module.concat(name, GenServer)
and might be used as such.
Returns a {:heir, pid(), heir_data}
tuple where pid
is the
pid of the state holder. Useful when the GenServer
process creates persistent ETS, the result of this function is
to be passed to ETS options as a config parameter.
In that case, the tables will be given away to the state process
and then retaken by the restarted GenServer
.
The counterpart for Kernel.send/2
. Uses the very same semantics.
Starts a supervisor branch with the given options.
@spec transfer( peeper :: name(), source :: Supervisor.supervisor(), destination :: Supervisor.supervisor(), return_fun? :: boolean() ) :: nil | (-> nil | DynamicSupervisor.on_start_child()) | term()
Transfers the whole Peeper
branch from one supervisor to another.