Current section
Files
Jump to
Current section
Files
lib/gen_loop.ex
defmodule PlainFsmRecords do
@moduledoc false
# We use the same records as in plain_fsm code, so a plain_fsm module and a
# GenLoop module are compatible.
# Extracting records with from_lib: "plain_fsm/src/plain_fsm.erl" does not
# work, so we redefine them here. But we have to be in syc with plain_fsm
# developers.
require Record
Record.defrecord(:fsm_sys, :sys, [
cont: :undefined,
mod: :undefined,
name: :undefined,
])
Record.defrecord(:fsm_info, :info, [
parent: :undefined,
debug: [],
sys: {:undefined, :undefined, :undefined}, # fsm_sys() does not work (macro)
])
end
defmodule GenLoop do
@typedoc "Return values of `start*` functions"
@type on_start :: {:ok, pid} | :ignore | {:error, {:already_started, pid} | term}
@typedoc "The GenLoop name"
@type name :: atom | {:global, term} | {:via, module, term}
@typedoc "Options used by the `start*` functions"
@type options :: [option]
@typedoc "Option values used by the `start*` functions"
@type option :: {:debug, debug} |
{:name, name} |
{:timeout, timeout} |
{:spawn_opt, Process.spawn_opt}
@typedoc "Debug options supported by the `start*` functions"
@type debug :: [:trace | :log | :statistics | {:log_to_file, Path.t}]
@typedoc "The server reference"
@type server :: pid | name | {atom, node}
@typedoc """
Tuple describing the client of a call request.
`pid` is the PID of the caller and `tag` is a unique term used to identify the
call.
"""
@type from :: {pid, tag :: term}
@callback init(args :: term) ::
{:ok, state} |
{:ok, state, timeout | :hibernate} |
:ignore |
{:stop, reason :: any} when state: any
@callback terminate(reason, state :: term) ::
term when reason: :normal | :shutdown | {:shutdown, term} | term
@callback code_change(old_vsn, state :: term, extra :: term) ::
{:ok, new_state :: term} |
{:error, reason :: term} when old_vsn: term | {:down, term}
@callback format_status(reason, pdict_and_state :: list) ::
term when reason: :normal | :terminate
@callback data_vsn() :: term
@optional_callbacks format_status: 2
@fsm_meta_key {:plain_fsm, :info}
@spec abcast([node], name :: atom, term) :: :abcast
defdelegate abcast(server, term), to: GenServer
defdelegate abcast(nodes, server, term), to: GenServer
# Cannot defdelegate because of __MODULE__
@spec call(server, term, timeout) :: term
def call(server, request, timeout \\ 5000) do
case whereis(server) do
nil ->
exit({:noproc, {__MODULE__, :call, [server, request, timeout]}})
pid when pid == self() ->
exit({:calling_self, {__MODULE__, :call, [server, request, timeout]}})
pid ->
try do
:gen.call(pid, :"$gen_call", request, timeout)
catch
:exit, reason ->
exit({reason, {__MODULE__, :call, [server, request, timeout]}})
else
{:ok, res} -> res
end
end
end
@doc """
Behaves just like Kernel.send but accepts atoms or registry tuples on top of
pids to identify a process.
"""
@spec send(server, term) :: term
def send(server, message) do
case whereis(server) do
nil ->
exit({:noproc, {__MODULE__, :send_to, [server, message]}})
pid when pid == self() ->
exit({:sending_to_self, {__MODULE__, :send_to, [server, message]}})
pid ->
Kernel.send(pid, message)
end
end
@spec cast(server, term) :: term
defdelegate cast(server, term), to: GenServer
@spec multi_call([node], name :: atom, term, timeout) :: {replies :: [{node, term}], bad_nodes :: [node]}
def multi_call(nodes \\ [node() | Node.list()], name, request, timeout \\ :infinity) do
:gen_server.multi_call(nodes, name, request, timeout)
end
@spec reply(from, term) :: :ok
defdelegate reply(from, term), to: GenServer
@spec start(module, any, options) :: on_start
def start(module, args, options \\ []) do
do_start(:nolink, module, args, options)
end
@spec start_link(module, any, options) :: on_start
def start_link(module, args, options \\ []) do
do_start(:link, module, args, options)
end
defp do_start(link, module, args, options) do
case Keyword.pop(options, :name) do
{nil, opts} ->
:gen.start(__MODULE__, link, module, args, opts)
{atom, opts} when is_atom(atom) ->
:gen.start(__MODULE__, link, {:local, atom}, module, args, opts)
{{:global, _term} = tuple, opts} ->
:gen.start(__MODULE__, link, tuple, module, args, opts)
{{:via, via_module, _term} = tuple, opts} when is_atom(via_module) ->
:gen.start(__MODULE__, link, tuple, module, args, opts)
{other, _} ->
raise ArgumentError, """
expected :name option to be one of:
* nil
* atom
* {:global, term}
* {:via, module, term}
Got: #{inspect(other)}
"""
end
end
@spec stop(server, reason :: term, timeout) :: :ok
defdelegate stop(server), to: GenServer
defdelegate stop(server, reason), to: GenServer
defdelegate stop(server, reason, timeout), to: GenServer
@spec whereis(server) :: pid | {atom, node} | nil
defdelegate whereis(name), to: GenServer
# -- plain_fsm wrapper ------------------------------------------------------
import PlainFsmRecords
# -- Macros -----------------------------------------------------------------
defmacro rcall(from, msg) do
quote do
{:'$gen_call', unquote(from), unquote(msg)}
end
end
defmacro rcast(msg) do
quote do
{:'$gen_cast', unquote(msg)}
end
end
defmacro from_pid(from) do
# from is {pid, ref}
quote do
elem(unquote(from), 0)
end
end
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts, fsm_meta_key: @fsm_meta_key] do
@behaviour GenLoop
@fsm_meta_key fsm_meta_key
# Import the macros/funs for receive
import GenLoop, only: [
from_pid: 1,
hibernate: 3,
rcall: 2,
rcast: 1,
receive: 2,
reply: 2,
]
enter_loop_name = opts[:enter] || :enter_loop
spec = [
id: opts[:id] || __MODULE__,
start: Macro.escape(opts[:start]) || quote(do: {__MODULE__, :start_link, [arg]}),
restart: opts[:restart] || :permanent,
shutdown: opts[:shutdown] || 5000,
type: :worker
]
@doc false
def child_spec(arg) do
%{unquote_splicing(spec)}
end
defoverridable child_spec: 1
@doc false
def init(args) do
{:ok, args}
end
@doc false
def terminate(_reason, _state) do
:ok
end
@doc false
def code_change(_old, state, _extra) do
{:ok, state}
end
@doc false
def start_link(args, options \\ []) do
GenLoop.start_link(__MODULE__, args, options)
end
# We set the same default version as in plain_fsm transforms
@doc false
def data_vsn() do
0
end
@doc false
def unquote(enter_loop_name)(_) do
fsm_info(sys: fsm_sys(mod: mod)) = Process.get(@fsm_meta_key)
raise """
You must define an #{unquote(enter_loop_name)}/1 function in module #{inspect mod}
according to GenLoop behaviour in order to receive the state returned
in your init/1 callback.
"""
end
def __gen_loop_enter_loop__(state) do
unquote(enter_loop_name)(state)
end
# defoverridable GenLoop
defoverridable [
code_change: 3,
data_vsn: 0,
init: 1,
start_link: 1,
start_link: 2,
terminate: 2,
]
defoverridable [
{enter_loop_name, 1},
]
end
end
# This macro is heavily inspired (i mean stolen) from
# ashneyderman/plain_fsm_ex from Github. The main difference is that we do not
# use a reference to the argument of the function, but rather require the
# state variable to be passed.
#
# I allows to change the state variable before entering the receive blog and
# keep the changes when a system message is handled or when a parent EXIT is
# received.
defmacro receive(state_var, blocks) do
{loop_name, arity} = __CALLER__.function
if arity !== 1 do
raise ArgumentError, bad_arity_msg(__CALLER__)
end
define_parent = quote do
plain_fsm_parent = :plain_fsm.info(:parent)
end
[parent_exit_clause] = quote do
{:'EXIT', ^plain_fsm_parent, reason} ->
:plain_fsm.parent_EXIT(reason, unquote(state_var))
end
[other_exit_clause] = quote do
{:'EXIT', _from, reason} = msg ->
IO.puts "Received : #{inspect msg}, parent is #{inspect plain_fsm_parent}"
exit(reason)
end
[system_message_clause] = quote do
{:system, from, req} ->
:plain_fsm.handle_system_msg(
req, from, unquote(state_var),
&unquote(Macro.var(loop_name, Elixir))/1
)
end
do_block =
blocks[:do]
|> List.insert_at(0, other_exit_clause)
|> List.insert_at(0, parent_exit_clause)
|> List.insert_at(0, system_message_clause)
# Put the clauses back together with the 'after' clauses
new_blocks = Keyword.put(blocks, :do, do_block)
whole_receive = {:receive, [], [new_blocks]}
_ast = quote do
unquote(define_parent)
unquote(whole_receive)
end
end
defp bad_arity_msg(caller) do
%{module: mod, function: {fun, _arity}} = caller
"""
Error when calling #{:receive} in module #{inspect mod}:
The calling function must be of arity 1. It should only accept the current
state of the process.
def #{fun}(state) do
# ... maybe change state ...
receive state do
# ... clauses ...
end
end
"""
end
# This macro comes from plain_fsm_ex too. It's just a call to the plain_fsm
# form of hibernate, i.e. calling the plain_fsm:wake_up function that will
# look into the process dictionary to get back the state.
# plain_fsm require that the function has only one argument in order to call
# code_change with state.
defmacro hibernate(module, function, [_] = args) do
quote do
:erlang.hibernate(:plain_fsm, :wake_up, [
data_vsn(), # The old version of code
unquote(module), # Module to call data_vsn() for new version of code
unquote(module), # Module …
unquote(function), # … Function …
unquote(args) # … Arguments to call after wakeup
])
end
end
defmacro hibernate(_module, _function, arguments) do
raise ArgumentError, """
GenLoop.hibernate(module, function, arguments) accepts only one element in arguments.
Got: #{Macro.to_string(arguments)} (#{length(arguments)} arguments)
"""
end
## -- Server side handler ---------------------------------------------------
@doc false
def init_it(starter, :self, name, mod, args, options),
do: init_it(starter, self(), name, mod, args, options)
def init_it(starter, parent, name0, mod, args, options) do
reg_name = name(name0)
# Copy pasta of plain_fsm init code storing meta into process dictionary
#
# info = fsm_info(parent: parent)
# sys = fsm_info(info, :sys)
# Process.put(@fsm_meta_key, fsm_info(info, sys: fsm_sys(sys, mod: mod)))
#
# but everything is empty so we not just write this
Process.put(@fsm_meta_key, fsm_info(parent: parent, sys: fsm_sys(mod: mod, name: reg_name)))
# Call the behaviour init function
case mod.init(args) do
{:ok, state} ->
:proc_lib.init_ack(starter, {:ok, self()})
mod.__gen_loop_enter_loop__(state)
{:stop, reason} ->
unregister_name(name0)
:proc_lib.init_ack(starter, {:error, reason})
exit(reason)
:ignore ->
unregister_name(name0)
:proc_lib.init_ack(starter, :ignore)
exit(:normal)
other ->
err = {:error, {:bad_return_value, other}}
:proc_lib.init_ack(starter, err)
exit(other)
end
end
defp name({:local ,name}),
do: name
defp name({:global ,name}),
do: name
defp name({:via ,_, name}),
do: name
defp name(pid) when is_pid(pid),
do: pid
defp unregister_name({:global, name}),
do: :global.unregister_name(name)
defp unregister_name({:via, mod, name}),
do: mod.unregister_name(name)
defp unregister_name(pid) when is_pid(pid),
do: pid
defp unregister_name({:local, name}) do
Process.unregister(name)
catch
_ -> :ok
rescue
_ -> :ok
end
end