Peeper (peeper v0.3.1)

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} 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

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

Types

The option accepted by :ets.new/2

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.

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

ets_option()

@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

name()

@type name() :: atom()

The name of the processes branch

Functions

call(pid, msg, timeout \\ 5000)

@spec call(name() | pid(), term(), timeout()) :: term()

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

cast(pid, msg, delay \\ 0)

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

child_spec(opts)

Returns a specification to start a branch under a supervisor.

gen_server(pid, delay \\ 0)

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)

@spec gen_server_name(name() | pid() | nil) :: name() | nil

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.

heir(pid, heir_data \\ nil)

@spec heir(name() | pid(), heir_data) :: {:heir, pid(), heir_data}
when heir_data: term()

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.

send(pid, msg, delay \\ 0)

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

start_link(opts)

Starts a supervisor branch with the given options.

transfer(name, source_pid, destination_pid, return_fun? \\ false)

@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.