Peeper.GenServer behaviour (peeper v0.3.1)
View SourceA drop-in replacement for use GenServer
that preserves state between crashes.
Overview
Peeper.GenServer
enables you to create GenServer-like processes that maintain their state
even when they crash. It accomplishes this through a specialized supervision tree that includes:
- A worker process (implementing your callbacks)
- A state keeper process (that preserves state between crashes)
- A supervisor process (managing the relationship between the worker and state keeper)
Comparison with Standard GenServer
Peeper.GenServer
supports all the standard GenServer callbacks with nearly identical
semantics, but with two important differences:
The
init/1
callback can only return one of:{:ok, state}
{:ok, state, timeout | :hibernate | {:continue, term()}}
You must use the
Peeper
module's communication functions:Peeper.call/3
instead ofGenServer.call/3
Peeper.cast/2
instead ofGenServer.cast/2
Peeper.send/2
instead of direct process messaging
State Preservation Mechanism
When the worker process crashes, the supervisor restarts it, and the state keeper
provides the most recent state to the restarted worker. This makes the init/1
callback
function primarily important only during the first start - subsequent restarts will
override whatever state is set in init/1
with the preserved state.
Performance Considerations
Since Peeper.GenServer
creates three processes instead of one, it introduces some
overhead compared to a standard GenServer. Use it when state preservation between crashes
outweighs the need for absolute performance in highly concurrent applications.
Advanced Features
- ETS Table Preservation: Peeper can preserve ETS tables between crashes
- Process Dictionary Preservation: Process dictionary entries are also preserved
- Listeners: Attach listeners to monitor state changes and termination events
- Dynamic Supervisor Transfer: Move Peeper processes between supervisors
Usage
Use Peeper.GenServer
by adding it to your module:
defmodule MyStatePreservingServer do
use Peeper.GenServer
# Define your callbacks similar to GenServer
end
See the examples below for more detailed usage patterns.
Summary
Callbacks
Initializes the GenServer state.
Functions
Declares a Peeper.GenServer
behaviour, injects start_link/1
function
and the child spec.
Starts a Peeper
sub-supervision process tree linked to the current process.
Callbacks
@callback handle_call(request :: term(), GenServer.from(), state :: term()) :: {:reply, reply, new_state} | {:reply, reply, new_state, timeout() | :hibernate | {:continue, continue_arg :: term()}} | {:noreply, new_state} | {:noreply, new_state, timeout() | :hibernate | {:continue, continue_arg :: term()}} | {:stop, reason, reply, new_state} | {:stop, reason, new_state} when reply: term(), new_state: term(), reason: term()
Initializes the GenServer state.
This callback serves a similar purpose to GenServer.init/1
but has more restricted
return values. Unlike standard GenServer's init/1, in Peeper.GenServer:
It may only return
{:ok, state}
or{:ok, state, timeout | :hibernate | {:continue, term()}}
- It can't return
:ignore
or{:stop, reason}
- It can't use other GenServer init/1 patterns like
{:ok, state, {:continue, term()}}
State Preservation Behavior
It's important to understand that:
- During the first start, the state you set in init/1 becomes the initial state
- During restarts after crashes, the state from init/1 will be immediately overridden by the state preserved in the Peeper state keeper process
- The continued_arg in
{:continue, continued_arg}
is still respected on all starts
Examples
# Basic initialization
@impl Peeper.GenServer
def init(args) do
state = %{counter: 0, config: Keyword.get(args, :config, %{})}
{:ok, state}
end
# Initialization with continue
@impl Peeper.GenServer
def init(args) do
{:ok, args, {:continue, :load_initial_data}}
end
See: GenServer.init/1
for more details on the standard GenServer behavior.
Functions
Declares a Peeper.GenServer
behaviour, injects start_link/1
function
and the child spec.
Customization Options
When using Peeper.GenServer
, you can pass options that will be applied as defaults
in all start_link/1
calls:
listener
: A module implementing thePeeper.Listener
behaviourkeep_ets
: Whether to preserve ETS tables between crashes (true
,:all
, or a list of table names)- Any other options that would be valid for
start_link/1
What's Injected
Using this macro injects the following functions into your module:
start_link/1
: For starting the Peeper processchild_spec/1
: For use in supervision treesstop/3
: For stopping the Peeper process
Basic Example
defmodule Counter do
use Peeper.GenServer
@impl Peeper.GenServer
def init(_) do
{:ok, 0} # Initial state is 0
end
@impl Peeper.GenServer
def handle_call(:get, _from, state) do
{:reply, state, state}
end
@impl Peeper.GenServer
def handle_cast(:inc, state) do
{:noreply, state + 1}
end
@impl Peeper.GenServer
def handle_cast({:add, n}, state) do
{:noreply, state + n}
end
end
Example with Listener
defmodule MyListener do
@behaviour Peeper.Listener
@impl Peeper.Listener
def on_state_changed(old_state, new_state) do
# Log, send telemetry event, etc.
IO.puts("State changed from #{inspect(old_state)} to #{inspect(new_state)}")
:ok
end
@impl Peeper.Listener
def on_terminate(reason, final_state) do
IO.puts("Process terminated with reason: #{inspect(reason)}")
IO.puts("Final state was: #{inspect(final_state)}")
:ok
end
end
defmodule MyGenServer do
use Peeper.GenServer, listener: MyListener
@impl Peeper.GenServer
def init(args) do
{:ok, args}
end
# ... other callbacks ...
end
Example with ETS Table Preservation
defmodule CacheServer do
use Peeper.GenServer, keep_ets: true
@impl Peeper.GenServer
def init(_) do
# Create an ETS table that will be preserved across crashes
:ets.new(:cache, [:named_table, :set, :public])
{:ok, %{last_updated: nil}}
end
@impl Peeper.GenServer
def handle_call({:get, key}, _from, state) do
result = :ets.lookup(:cache, key)
{:reply, result, state}
end
@impl Peeper.GenServer
def handle_cast({:set, key, value}, state) do
:ets.insert(:cache, {key, value})
{:noreply, %{state | last_updated: System.monotonic_time()}}
end
# Even if this process crashes, the ETS table will be preserved
end
Starts a Peeper
sub-supervision process tree linked to the current process.
This is the underlying implementation function called by the module-specific start_link/1
that gets injected when using use Peeper.GenServer
. It properly formats the options
and delegates to Peeper.Supervisor.start_link/1
.
Parameters
module
: The module that implements thePeeper.GenServer
behaviouropts
: Options for initializing and configuring the Peeper supervision tree
Options
- Any non-keyword argument is treated as the initial state
- If a keyword, it may include:
state
: The initial state valuelistener
: Module implementing thePeeper.Listener
behaviour- Any other valid options for
Peeper.Supervisor.start_link/1
Returns
{:ok, pid}
if the server is started successfully{:error, reason}
if the server failed to start