Receiver v0.1.5 Receiver behaviour View Source

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

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

  • 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. See example below.

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 a named Agent using 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 - Optional reason and timeout args. See Agent.stop/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
GenServer.stop(Counter)
#=> :ok

# Get the counter state, which was persisted across restarts
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(__MODULE__, :stash, fn -> [] end)
end

defp start_stash(fun) when is_function(fun) do
  Receiver.start(__MODULE__, :stash, fun)
end

defp start_stash(module, fun, args) do
  Receiver.start(__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 update_stash(fun) when is_function(fun) do
  Receiver.update(__MODULE__, :stash, fun)
end

defp get_and_update_stash(fun) when is_function(fun) do
  Receiver.get_and_update(__MODULE__, :stash, fun)
end

These are private so the stash cannot easily be started, stopped, or updated from outside the counter process. A receiver can always be manipulated by calling the Receiver functions directly i.e. Receiver.update(Counter, :stash, & &1 + 1), but in many cases these functions should be used 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 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

Usage in testing

A Receiver can also be used to test higher order functions by using it in an ExUnit test case and passing the test: true option. Consider the following example:

defmodule Worker do
  def perform_complex_work(val, fun) do
    val
    |> do_some_work()
    |> fun.()
    |> do_other_work()
  end

  def do_some_work(val), do: :math.log(val)

  def do_other_work(val), do: :math.exp(val) |> :math.floor()
end

defmodule ExUnit.HigherOrderTest do
  use ExUnit.Case
  use Receiver, test: true

  setup do
    start_receiver(fn -> nil end)
    :ok
  end

  def register(x) do
    get_and_update_receiver(fn _ -> {x, x} end)
  end

  test "it does the work in stages with help from an anonymous function" do
    assert get_receiver() == nil

    result = Worker.perform_complex_work(1.0, fn x -> register(x) end)
    receiver = get_receiver()

    assert Worker.do_some_work(1.0) == receiver
    assert Worker.do_other_work(receiver) == result
  end
end

When the :test option is set to true within a module using ExUnit.Case, you can call the start_receiver functions within a setup block, delegating to ExUnit.Callbacks.start_supervised/2. This will start the receiver as a supervised process under the test supervisor, automatically starting and shutting it down between tests to clean up state. This can help you test that your higher order functions are executing with the correct arguments and returning the expected results.

A note on 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. All of the callbacks are invoked within the calling process, not the receiver process.

Link to this section Summary

Types

A list of function arguments

Return values of start/3 and start/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 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 request is sent to the receiver. get/2 and get/3 will block until it returns

Invoked in the calling process after the receiver is started. start/3 and start/5 will block until it returns

Invoked in the calling process after the receiver is stopped. stop/4 will block until it returns

Link to this section Types

A list of function arguments

Return values of start/3 and start/5

Link to this type

on_start_supervised() View Source
on_start_supervised() :: DynamicSupervisor.on_start_child()

Return values of start_supervised/3 and start_supervised/5

Link to this type

option() View Source
option() :: {:as, atom()} | {:name, atom()}

Option values used by the start* functions

Link to this type

options() View Source
options() :: [option()]

Options used by the start* functions

Link to this type

receiver() View Source
receiver() :: :receiver | atom() | {module(), atom()}

The receiver name

Link to this type

registered_name() View Source
registered_name() ::
  {:via, Registry, {Receiver.Registry, {module(), receiver()}}}

The registered name of a receiver

Link to this type

start_args() View Source
start_args() ::
  [module() | (... -> any())]
  | [module() | (... -> any()) | options()]
  | [module() | atom() | args()]
  | [module() | atom() | args() | options()]

A list of arguments accepted by start* functions

The receiver state

Link to this section Functions

Returns a specification to start this module under a supervisor.

See Supervisor.

Link to this function

get(name, fun) View Source
get(receiver(), (state() -> term())) :: term()

Link to this function

get_and_update(name, fun) View Source
get_and_update(receiver(), (state() -> {term(), state()})) :: term()

Link to this function

start(module, fun, opts \\ []) View Source
start(module(), (() -> term()), options()) :: on_start()

Starts a new receiver without links (outside of a supervision tree).

See start_link/3 for more information.

Examples

{:ok, _} = Receiver.start(Example, fn -> %{} end)
Receiver.get({Example, :receiver})
#=> %{}

{:ok, _} = Receiver.start(Example, fn -> %{} end, name: Example.Map)
Receiver.get(Example.Map)
#=> %{}
Link to this function

start(module, mod, fun, args, opts \\ []) View Source
start(module(), module(), atom(), args(), options()) :: on_start()

Link to this function

start_link(list) View Source
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 really need to supervise your own receiver. In most cases you should 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.

Link to this function

start_link(module, fun, opts \\ []) View Source
start_link(module(), (() -> term()), options()) :: on_start()

Link to this function

start_link(module, mod, fun, args, opts \\ []) View Source
start_link(module(), module(), atom(), args(), options()) :: on_start()

Link to this function

start_supervised(module, fun, opts \\ []) View Source
start_supervised(module(), (() -> term()), options()) :: on_start_supervised()

Link to this function

start_supervised(module, mod, fun, args, opts \\ []) View Source
start_supervised(module(), module(), atom(), args(), options()) ::
  on_start_supervised()

Link to this function

stop(name, reason \\ :normal, timeout \\ :infinity) View Source
stop(receiver(), reason :: term(), timeout()) :: :ok

Link to this function

update(name, fun) View Source
update(receiver(), (state() -> state())) :: :ok

Link to this function

whereis(name) View Source
whereis(pid() | {module(), receiver()}) :: pid() | nil

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.

Link to this function

which_name(pid) View Source
which_name(pid() | receiver()) :: atom() | nil

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.

Link to this function

which_receiver(pid) View Source
which_receiver(pid() | atom()) :: receiver() | nil

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

Link to this callback

handle_get(receiver, state) View Source (optional)
handle_get(receiver(), state :: term()) :: {:reply, reply :: term()} | :noreply

Invoked in the calling process after a get request is sent to the receiver. get/2 and get/3 will block until it returns.

state is the return value of the function passed to get/2 or get/3 and invoked in the receiver. With basic get functions this will be the current state of the receiver.

Returning {:reply, reply} causes reply to be the return value of get/2 and get/3 (and the private get_receiver client functions).

If :noreply is the return value then state will be the return value of get/2 and get/3. This can be useful if action needs to be performed with the state value but there's no desire to return the results of those actions to the caller.

Link to this callback

handle_get_and_update(receiver, return_val, state) View Source (optional)
handle_get_and_update(receiver(), return_val :: term(), state()) ::
  {:reply, reply :: term()} | :noreply

Link to this callback

handle_start(receiver, pid, state) View Source (optional)
handle_start(receiver(), pid(), state :: term()) :: term()

Invoked in the calling process after the receiver is started. start/3 and start/5 will block until it returns.

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/3 or start/5 was called then the callback will not be invoked.

The return value is ignored.

Link to this callback

handle_stop(receiver, reason, state) View Source (optional)
handle_stop(receiver(), reason :: term(), state :: term()) :: term()

Invoked in the calling process after the receiver is stopped. stop/4 will block until it returns.

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.

Link to this callback

handle_update(receiver, old_state, state) View Source (optional)
handle_update(receiver(), old_state :: term(), state()) :: term()