Peeper (peeper v0.2.0)

View Source

Peeper.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} tuple (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(Counter.GenServer, :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

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.

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 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()}.

The counterpart for Kernel.send/2. Uses the very same semantics.

Functions

call(pid, msg, timeout \\ 5000)

The counterpart for GenServer.call/3. Uses the very same semantics.

cast(pid, msg)

The counterpart for GenServer.cast/2. Uses the very same semantics.

gen_server(pid)

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

gen_server_name(peeper_name)

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.

send(pid, msg)

The counterpart for Kernel.send/2. Uses the very same semantics.