Current section

Files

Jump to
receiver lib receiver.ex
Raw

lib/receiver.ex

defmodule Receiver do
@moduledoc """
Conveniences for creating processes that hold state.
A simple wrapper around an `Agent` that reduces boilerplate code and makes it easy to store
state in a separate supervised process.
# Use cases
* Storing persistent process state outside of the worker process, or as a shared repository
for multiple processes.
* Creating a "stash" to persist process state across restarts. 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.
## Example
defmodule Counter do
@moduledoc false
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 its state will not change. The state of the stash is returned as the
# initial counter state whenever the counter is started.
def init(arg) do
start_stash(arg)
{: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.
def terminate(_reason, state) do
update_stash(state)
end
end
The line `use Receiver, as: :stash` creates a module and named `Agent` process with the name `Counter.Stash`.
The stash is supervised in the `Receiver` application supervision tree, not in your own application's. It also
defines the following client functions in the `Counter` module:
* `start_stash/0` - Defaults the inital state to an empty list.
* `start_stash/1` - Expects a value or anonymous function that will return the initial state.
* `start_stash/3` - Expects a module, function name, and list of args that will return the initial state
when called.
* `stop_stash/2` - Optional `reason` and `timeout` args. See `Agent.stop/3` for more information.
* `get_stash/0` - Returns the current state of the stash.
* `update_stash/1` - Updates the state of the stash. expects a value or an anonymous function that receives
the current state as an argument and returns the updated state.
If no `:as` option were given in this example then the process would take the name `Counter.Receiver`, and the
functions would be named like `start_receiver/0`.
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
# The stash is still set to the initial value
Counter.get_stash()
#=> 0
# Stop the counter, initiating a restart
GenServer.stop(Counter)
#=> :ok
# Get the counter state, which was persisted across restarts
Counter.get()
#=> 2
# Get the state of the stash, which was updated when the counter exited
Counter.get_stash()
#=> 2
"""
defmacro __using__(opts) do
{name, opts} = Keyword.pop(opts, :as, :receiver)
{test, _} = Keyword.pop(opts, :test, false)
module_name =
__CALLER__.module
|> Module.split()
|> Enum.concat([Atom.to_string(name) |> Macro.camelize()])
|> Module.concat()
quote bind_quoted: [name: name, module_name: module_name, test: test] do
import Receiver
@receiver_module_name module_name
@behaviour module_name
defmodule module_name do
@moduledoc false
use Agent, restart: :transient
@callback unquote(:"start_#{name}")() :: {:ok, pid}
@callback unquote(:"start_#{name}")(arg :: term) :: {:ok, pid}
@callback unquote(:"start_#{name}")(module, fun, args :: [term]) :: {:ok, pid}
@callback unquote(:"stop_#{name}")(reason :: term, timeout) :: :ok
@callback unquote(:"get_#{name}")() :: term
@callback unquote(:"update_#{name}")(fun) :: :ok
@callback unquote(:"update_#{name}")(term) :: :ok
def start_link(arg)
def start_link([module, fun, args]) do
Agent.start_link(module, fun, args, name: __MODULE__)
end
def start_link(fun) when is_function(fun) do
Agent.start_link(fun, name: __MODULE__)
end
def start_link(arg) do
Agent.start_link(fn -> arg end, name: __MODULE__)
end
end
if test do
def unquote(:"start_#{name}")(args \\ []) do
start_supervised({@receiver_module_name, args})
end
def unquote(:"start_#{name}")(module, fun, args) do
start_supervised({@receiver_module_name, [module, fun, args]})
end
else
def unquote(:"start_#{name}")(args \\ []) do
DynamicSupervisor.start_child(Receiver.Supervisor, {@receiver_module_name, args})
end
def unquote(:"start_#{name}")(module, fun, args) do
DynamicSupervisor.start_child(Receiver.Supervisor, {@receiver_module_name, [module, fun, args]})
end
end
def unquote(:"stop_#{name}")(reason \\ :normal, timeout \\ :infinity) do
Agent.stop(@receiver_module_name, reason, timeout)
end
def unquote(:"get_#{name}")() do
Agent.get(@receiver_module_name, & &1)
end
def unquote(:"update_#{name}")(fun) when is_function(fun) do
Agent.update(@receiver_module_name, fun)
end
def unquote(:"update_#{name}")(value) do
Agent.update(@receiver_module_name, fn _ -> value end)
end
end
end
end