Receiver v0.2.0 Receiver behaviour View Source
Conveniences for creating processes that hold important state.
A wrapper around an Agent
that adds callbacks and reduces boilerplate code, making it
quick and easy to store important state in a separate supervised process.
Use cases
Creating a "stash" to persist process state across restarts. See example below.
Application or server configuration. See example below.
Storing mutable state outside of a worker process, or as a shared repository for multiple processes running the same module code. See example below.
Testing higher order functions. By passing a function call to a
Receiver
process into a higher order function you can test if the function is executed as intended by checking the change in state. SeeExUnitReceiver
module documentation.
Using as a stash
defmodule Counter do
use GenServer
use Receiver, as: :stash
def start_link(arg) do
GenServer.start_link(__MODULE__, arg, name: __MODULE__)
end
def increment(num) do
GenServer.cast(__MODULE__, {:increment, num})
end
def get do
GenServer.call(__MODULE__, :get)
end
# The stash is started with the initial state of the counter. If the stash is already
# started when `start_stash/1` is called then its state will not change. The current state
# of the stash is returned as the initial counter state whenever the counter is started.
def init(arg) do
start_stash(fn -> arg end)
{:ok, get_stash()}
end
def handle_cast({:increment, num}, state) do
{:noreply, state + num}
end
def handle_call(:get, _from, state) do
{:reply, state, state}
end
# The stash is updated to the current counter state before the counter exits.
# This state will be stored for use as the initial state of the counter when
# it restarts, allowing the state to persist in the event of failure.
def terminate(_reason, state) do
update_stash(fn _ -> state end)
end
end
The line use Receiver, as: :stash
creates an Agent
named with the :via
semantics of the Registry
module.
The stash is supervised in the Receiver
application supervision tree, not in your own application's. It also
defines the following private client functions in the Counter
module:
start_stash/0
- Defaults the inital state to an empty list.start_stash/1
- Expects an anonymous function that will return the initial state when called.start_stash/3
- Expects a module, function name, and list of args that will return the initial state when called.stop_stash/2
- Optionalreason
andtimeout
args. Seestop/3
for more information.get_stash/0
- Returns the current state of the stash.get_stash/1
- Expects an anonymous function that accepts a single argument. The state of the stash is passed to the anonymous function, and the result of the function is returned.update_stash/1
- Updates the state of the stash. Expects an anonymous function that receives the current state as an argument and returns the updated state.get_and_update_stash/1
- Gets and updates the stash. Expects an anonymous function that receives the current state as an argument and returns a two element tuple, the first element being the value to return, the second element is the updated state.
If no :as
option were given in this example then the default function names are used:
start_receiver/0
start_receiver/1
start_receiver/3
stop_receiver/2
get_receiver/0
get_receiver/1
update_receiver/1
get_and_update_receiver/1
See more detail on the generated functions in the client functions section below.
The Counter
can now be supervised and its state will be isolated from failure and persisted across restarts.
# Start the counter under a supervisor
{:ok, _pid} = Supervisor.start_link([{Counter, 0}], strategy: :one_for_one)
# Get the state of the counter
Counter.get()
#=> 0
# Increment the counter
Counter.increment(2)
#=> :ok
# Get the updated state of the counter
Counter.get()
#=> 2
# Stop the counter, initiating a restart and losing the counter state
GenServer.stop(Counter)
#=> :ok
# Get the counter state, which was persisted across restarts with help of the stash
Counter.get()
#=> 2
Client functions
When we use Receiver, as: :stash
above, the following private function definitions
are automatically generated inside the Counter
module:
defp start_stash do
Receiver.start_supervised({__MODULE__, :stash}, fn -> [] end)
end
defp start_stash(fun) do
Receiver.start_supervised({__MODULE__, :stash}, fun)
end
defp start_stash(module, fun, args)
Receiver.start_supervised({__MODULE__, :stash}, module, fun, args)
end
defp stop_stash(reason \\ :normal, timeout \\ :infinity) do
Receiver.stop({__MODULE__, :stash}, reason, timeout)
end
defp get_stash do
Receiver.get({__MODULE__, :stash})
end
defp get_stash(fun) do
Receiver.get({__MODULE__, :stash}, fun)
end
defp update_stash(fun) do
Receiver.update({__MODULE__, :stash}, fun)
end
defp get_and_update_stash(fun) do
Receiver.get_and_update({__MODULE__, :stash}, fun)
end
These are private to encourage starting, stopping, and updating the stash from only the Counter
API.
A receiver can always be manipulated by calling the Receiver
functions directly
i.e. Receiver.update({Counter, :stash}, & &1 + 1)
, but use these functions with caution to avoid
race conditions.
Using as a configuration store
A Receiver
can be used to store application configuration, and even be initialized
at startup. Since the receiver processes are supervised in a separate application
that is started as a dependency of yours, it will already be ready to start even before your
application's start/2
callback has returned:
defmodule MyApp do
@doc false
use Application
use Receiver, as: :config
def start(_app, _type) do
start_config(fn ->
Application.get_env(:my_app, :configuration, [setup: :default])
|> Enum.into(%{})
end)
children = [
MyApp.Worker,
MyApp.Task
]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp)
end
def config, do: get_config()
end
Now the configuration can be globally read with the public MyApp.config/0
.
MyApp.config()
#=> %{setup: :default}
MyApp.config.setup
#=> :default
A look at callbacks
The first argument to all of the callbacks is the name of the receiver. This will either be the atom passed to
the :as
option or the default name :receiver
. The intent is to avoid any naming collisions with other handle_*
callbacks.
defmodule Account do
use GenServer
use Receiver, as: :ledger
# Client API
def start_link(initial_balance) do
start_ledger(fn -> %{} end)
GenServer.start_link(__MODULE__, initial_balance)
end
def get_balance_history(pid) do
get_ledger(fn ledger -> Map.get(ledger, pid) end)
end
def transact(pid, amount) do
GenServer.cast(pid, {:transact, amount})
end
# GenServer callbacks
def init(initial_balance) do
pid = self()
update_ledger(fn ledger -> Map.put(ledger, pid, [initial_balance]) end)
{:ok, initial_balance}
end
def handle_cast({:transact, amount}, balance) do
pid = self()
new_balance = balance + amount
update_ledger(fn ledger -> Map.update(ledger, pid, [new_balance], &([new_balance | &1])) end)
{:noreply, new_balance}
end
# Receiver callbacks
def handle_start(:ledger, pid, _state) do
IO.inspect(pid, label: "Started ledger")
IO.inspect(self(), label: "From caller")
end
def handle_get(:ledger, history) do
current_balance = history |> List.first()
IO.inspect(self(), label: "Handling get from")
IO.inspect(current_balance, label: "Current balance")
{:reply, history}
end
def handle_update(:ledger, _old_state, new_state) do
pid = self()
new_balance = new_state |> Map.get(pid) |> List.first()
IO.inspect(pid, label: "Handling update from")
IO.inspect(new_balance, label: "Balance updated to")
end
end
All of the callbacks are invoked within the calling process, not the receiver process.
{:ok, one} = Account.start_link(10.0)
# Started ledger: #PID<0.213.0>
# From caller: #PID<0.206.0>
# Handling update from: #PID<0.214.0>
# Balance updated to: 10.0
#=> {:ok, #PID<0.214.0>}
Process.whereis(Receiver.Sup)
#=> #PID<0.206.0>
Receiver.whereis({Account, :ledger})
#=> #PID<0.213.0>
self()
#=> #PID<0.210.0>
In Account.start_link/1
a ledger is started with a call to start_ledger/1
. #PID<0.213.0>
is the ledger pid,
and the calling process #PID<0.206.0>
handles the handle_start/3
callback as can be seen in the output.
The calling process in this case is Receiver.Sup
, the DynamicSupervisor
that supervises all receivers
when started with the private convenience functions and is the process that makes the actual call to
Receiver.start_link/1
.
When init/1
is invoked in the account server (#PID<0.214.0>
) it updates the ledger with it's starting balance by
making a call to update_ledger/1
, and receives the handle_update/3
callback.
{:ok, two} = Account.start_link(15.0)
# Handling update from: #PID<0.219.0>
# Balance updated to: 15.0
#=> {:ok, #PID<0.219.0>}
When start_link/1
is called the second time the ledger already exists so the call to start_ledger/1
is
a noop and the handle_start/3
callback is never invoked.
Account.get_balance_history(one)
# Handling get from: #PID<0.210.0>
# Current balance: 10.0
#=> [10.0]
Account.get_balance_history(two)
# Handling get from: #PID<0.210.0>
# Current balance: 15.0
#=> [15.0]
Account.transact(one, 15.0)
# Handling update from: #PID<0.214.0>
# Balance updated to: 25.0
#=> :ok
This may be confusing at first, and it's different from the way callbacks are dispatched in a GenServer for example. The important thing to remember is that the receiver does not invoke the callbacks, they are always invoked from the process that's sending it the message.
A Receiver
is meant to be isolated from complex and potentially error-prone operations. It only exists to
hold important state and should be protected from failure and remain highly available. The callbacks provide
an opportunity to perform additional operations with the receiver data, such as interacting with the outside
world, that may have no impact on the return value and do not expose the receiver itself to errors or block
the process from answering other callers. The goal is to keep the functions passed to the receiver as simple
as possible and perform more complex operations in the callbacks.
Link to this section Summary
Types
A list of function arguments
Error returned from bad arguments
Return values of start_supervised/3
and start_supervised/5
Option values used by the start*
functions
Options used by the start*
functions
The receiver name
The registered name of a receiver
A list of arguments accepted by start*
functions
The receiver attributes required for successful start and registration
Error tuple returned for pattern matching on function results
The receiver state
Functions
Returns a specification to start this module under a supervisor
Starts a new receiver without links (outside of a supervision tree)
Starts a Receiver
process linked to the current process
Returns the PID of a receiver process, or nil
if it does not exist
Returns the name
of a registered process associated with a receiver. name
must be an atom that
can be used to register a process with Process.register/2
Returns a two element tuple containing the callback module and name of the receiver associated with a PID or a registered process name
Callbacks
Invoked in the calling process after a get_and_update
is sent to the receiver. get_and_update/2
will
block until it returns
Invoked in the calling process after the receiver is started. All start*
functions will block until it returns
Invoked in the calling process after the receiver is stopped. stop/3
will block until it returns
Invoked in the calling process after an update
is sent to the receiver. update/2
will
block until it returns
Link to this section Types
args()
View Source
args() :: [term()]
args() :: [term()]
A list of function arguments
not_found_error() View Source
Error returned from bad arguments
on_start()
View Source
on_start() :: Agent.on_start() | start_error()
on_start() :: Agent.on_start() | start_error()
on_start_supervised()
View Source
on_start_supervised() :: DynamicSupervisor.on_start_child() | start_error()
on_start_supervised() :: DynamicSupervisor.on_start_child() | start_error()
Return values of start_supervised/3
and start_supervised/5
option() View Source
Option values used by the start*
functions
options()
View Source
options() :: [option()]
options() :: [option()]
Options used by the start*
functions
receiver() View Source
The receiver name
registered_name() View Source
The registered name of a receiver
start_args() View Source
A list of arguments accepted by start*
functions
start_attrs()
View Source
start_attrs() :: %{
module: module(),
receiver: atom(),
name: atom() | registered_name(),
initial_state: term()
}
start_attrs() :: %{ module: module(), receiver: atom(), name: atom() | registered_name(), initial_state: term() }
The receiver attributes required for successful start and registration
start_error()
View Source
start_error() ::
{:error,
{%UndefinedFunctionError{
__exception__: term(),
arity: term(),
function: term(),
message: term(),
module: term(),
reason: term()
}
| %FunctionClauseError{
__exception__: term(),
args: term(),
arity: term(),
clauses: term(),
function: term(),
kind: term(),
module: term()
}, stacktrace :: list()}}
start_error() :: {:error, {%UndefinedFunctionError{ __exception__: term(), arity: term(), function: term(), message: term(), module: term(), reason: term() } | %FunctionClauseError{ __exception__: term(), args: term(), arity: term(), clauses: term(), function: term(), kind: term(), module: term() }, stacktrace :: list()}}
Error tuple returned for pattern matching on function results
state()
View Source
state() :: term()
state() :: term()
The receiver state
Link to this section Functions
child_spec(arg) View Source
Returns a specification to start this module under a supervisor.
See Supervisor
.
get(name) View Source
get(name, fun) View Source
get_and_update(name, fun) View Source
start(module, fun, opts \\ []) View Source
Starts a new receiver without links (outside of a supervision tree).
See start_link/3
for more information.
start(module, mod, fun, args, opts \\ []) View Source
start_link(list_of_args)
View Source
start_link(start_args()) :: on_start()
start_link(start_args()) :: on_start()
Starts a Receiver
process linked to the current process.
This is the function used to start a receiver as part of a supervision tree. It accepts a list containing from two to five arguments.
Usually this should be used to build a child spec in your supervision tree.
Examples
children = [
{Receiver, [One, fn -> 1 end]},
{Receiver, [Two, fn -> 2 end, [name: Two]]},
{Receiver, [Three, Kernel, :+, [2, 1]]},
{Receiver, [Four, Kernerl, :+, [2, 2], [name: Four]]}
]
Supervisor.start_link(children, strategy: one_for_one)
Only use this is if you want to supervise your own receiver from application startup. In most cases you can
simply use the start_supervised*
functions to start a supervised receiver dynamically in an isolated
application. See start_supervised/3
and start_supervised/5
for more information.
start_link(module, fun, opts \\ []) View Source
start_link(module, mod, fun, args, opts \\ []) View Source
start_supervised(module, fun, opts \\ [])
View Source
start_supervised(module(), (() -> term()), options()) :: on_start_supervised()
start_supervised(module(), (() -> term()), options()) :: on_start_supervised()
start_supervised(module, mod, fun, args, opts \\ []) View Source
stop(name, reason \\ :normal, timeout \\ :infinity) View Source
update(name, fun) View Source
whereis(pid) View Source
Returns the PID of a receiver process, or nil
if it does not exist.
Accepts one argument, either a two-element tuple containing the name of the callback module and an atom that is the name of the receiver, or a PID.
which_name(pid) View Source
Returns the name
of a registered process associated with a receiver. name
must be an atom that
can be used to register a process with Process.register/2
.
Accepts one argument, a PID or a two element tuple containing the callback module and the name of the receiver. Returns nil if no name was registered with the process.
which_receiver(pid) View Source
Returns a two element tuple containing the callback module and name of the receiver associated with a PID or a registered process name.
Accepts one argument, a PID or a name
. name
must be an atom that can be used to
register a process with Process.register/2
.
Link to this section Callbacks
handle_get(atom, return_value) View Source (optional)
Invoked in the calling process after a get
request is sent to the receiver. get/1
and get/2
will block until it returns.
atom
is the name of the receiver passed to the :as
option at start. Defaults to :receiver
.
return_value
is the return value of the get*
anonymous function. With a basic get
function this is
often the current state of the receiver.
Returning {:reply, reply}
causes reply
to be the return value of get/1
and get/2
(and the private get_receiver
client functions).
handle_get_and_update(atom, return_value, state) View Source (optional)
Invoked in the calling process after a get_and_update
is sent to the receiver. get_and_update/2
will
block until it returns.
atom
is the name of the receiver passed to the :as
option at start. Defaults to :receiver
.
return_val
is the first element of the tuple (the return value) of the anonymous function passed to
get_and_update/2
.
state
is the second element of the tuple and is the new state of the receiver.
Returning {:reply, reply}
causes reply
to be the return value of get_and_update/2
(and the private get_and_update_receiver
client function).
Returning :noreply
defaults the return value of get_and_update/2
to return_val
.
handle_start(atom, pid, state) View Source (optional)
Invoked in the calling process after the receiver is started. All start*
functions will block until it returns.
atom
is the name of the receiver passed to the :as
option at start. Defaults to :receiver
.
pid
is the PID of the receiver process, state
is the starting state of the receiver after the initializing
function is called.
If the receiver was already started when start*
was called then the callback will not be invoked.
The return value is ignored.
handle_stop(atom, reason, state) View Source (optional)
Invoked in the calling process after the receiver is stopped. stop/3
will block until it returns.
atom
is the name of the receiver passed to the :as
option at start. Defaults to :receiver
.
reason
is the exit reason, state
is the receiver state at the time of shutdown. See Agent.stop/3
for more information.
The return value is ignored.
handle_update(atom, old_state, state) View Source (optional)
Invoked in the calling process after an update
is sent to the receiver. update/2
will
block until it returns.
atom
is the name of the receiver passed to the :as
option at start. Defaults to :receiver
.
old_state
is the state of the receiver before update/2
was called. state
is the updated
state of the receiver.
The return value is ignored.