830 lines
29 KiB
Elixir
830 lines
29 KiB
Elixir
|
defmodule Connection do
|
||
|
@moduledoc """
|
||
|
A behaviour module for implementing connection processes.
|
||
|
|
||
|
The `Connection` behaviour is a superset of the `GenServer` behaviour. The
|
||
|
additional return values and callbacks are designed to aid building a
|
||
|
connection process that can handle a peer being (temporarily) unavailable.
|
||
|
|
||
|
An example `Connection` process:
|
||
|
|
||
|
defmodule TCPConnection do
|
||
|
|
||
|
use Connection
|
||
|
|
||
|
def start_link(host, port, opts, timeout \\\\ 5000) do
|
||
|
Connection.start_link(__MODULE__, {host, port, opts, timeout})
|
||
|
end
|
||
|
|
||
|
def send(conn, data), do: Connection.call(conn, {:send, data})
|
||
|
|
||
|
def recv(conn, bytes, timeout \\\\ 3000) do
|
||
|
Connection.call(conn, {:recv, bytes, timeout})
|
||
|
end
|
||
|
|
||
|
def close(conn), do: Connection.call(conn, :close)
|
||
|
|
||
|
def init({host, port, opts, timeout}) do
|
||
|
s = %{host: host, port: port, opts: opts, timeout: timeout, sock: nil}
|
||
|
{:connect, :init, s}
|
||
|
end
|
||
|
|
||
|
def connect(_, %{sock: nil, host: host, port: port, opts: opts,
|
||
|
timeout: timeout} = s) do
|
||
|
case :gen_tcp.connect(host, port, [active: false] ++ opts, timeout) do
|
||
|
{:ok, sock} ->
|
||
|
{:ok, %{s | sock: sock}}
|
||
|
{:error, _} ->
|
||
|
{:backoff, 1000, s}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def disconnect(info, %{sock: sock} = s) do
|
||
|
:ok = :gen_tcp.close(sock)
|
||
|
case info do
|
||
|
{:close, from} ->
|
||
|
Connection.reply(from, :ok)
|
||
|
{:error, :closed} ->
|
||
|
:error_logger.format("Connection closed~n", [])
|
||
|
{:error, reason} ->
|
||
|
reason = :inet.format_error(reason)
|
||
|
:error_logger.format("Connection error: ~s~n", [reason])
|
||
|
end
|
||
|
{:connect, :reconnect, %{s | sock: nil}}
|
||
|
end
|
||
|
|
||
|
def handle_call(_, _, %{sock: nil} = s) do
|
||
|
{:reply, {:error, :closed}, s}
|
||
|
end
|
||
|
|
||
|
def handle_call({:send, data}, _, %{sock: sock} = s) do
|
||
|
case :gen_tcp.send(sock, data) do
|
||
|
:ok ->
|
||
|
{:reply, :ok, s}
|
||
|
{:error, _} = error ->
|
||
|
{:disconnect, error, error, s}
|
||
|
end
|
||
|
end
|
||
|
def handle_call({:recv, bytes, timeout}, _, %{sock: sock} = s) do
|
||
|
case :gen_tcp.recv(sock, bytes, timeout) do
|
||
|
{:ok, _} = ok ->
|
||
|
{:reply, ok, s}
|
||
|
{:error, :timeout} = timeout ->
|
||
|
{:reply, timeout, s}
|
||
|
{:error, _} = error ->
|
||
|
{:disconnect, error, error, s}
|
||
|
end
|
||
|
end
|
||
|
def handle_call(:close, from, s) do
|
||
|
{:disconnect, {:close, from}, s}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
The example above follows a common pattern. Try to connect immediately, if
|
||
|
that fails backoff and retry after a delay. If a retry fails make another
|
||
|
attempt after another delay. If the process disconnects a reconnection attempt
|
||
|
is made immediately, if that fails backoff begins.
|
||
|
|
||
|
Importantly when backing off requests will still be received by the process,
|
||
|
which will need to be handled. In the above example the process replies with
|
||
|
`{:error, :closed}` when it is disconnected.
|
||
|
"""
|
||
|
|
||
|
@behaviour :gen_server
|
||
|
|
||
|
@doc """
|
||
|
Called when the connection process is first started. `start_link/3` will block
|
||
|
until it returns.
|
||
|
|
||
|
Returning `{:ok, state}` will cause `start_link/3` to return
|
||
|
`{:ok, pid}` and the process to enter its loop with state `state` without
|
||
|
calling `connect/2`.
|
||
|
|
||
|
This return value is useful when the process connects inside `init/1` so that
|
||
|
`start_link/3` blocks until a connection is established.
|
||
|
|
||
|
Returning `{:ok, state, timeout}` is similar to `{:ok, state}`
|
||
|
except `handle_info(:timeout, state)` will be called after `timeout` if no
|
||
|
message arrives.
|
||
|
|
||
|
Returning `{:ok, state, :hibernate}` is similar to
|
||
|
`{:ok, state}` except the process is hibernated awaiting a message.
|
||
|
|
||
|
Returning `{:connect, info, state}` will cause `start_link/3` to return
|
||
|
`{:ok, pid}` and `connect(info, state)` will be called immediately - even if
|
||
|
messages are in the processes message queue. `state` contains the state of the
|
||
|
process and `info` should contain any information not contained in the state
|
||
|
that is needed to connect.
|
||
|
|
||
|
This return value is very useful because connecting in `connect/2` will not
|
||
|
block the parent process and a connection attempt is guaranteed to occur
|
||
|
before any messages are handled, which is not possible when using a
|
||
|
`GenServer`.
|
||
|
|
||
|
Returning `{:backoff, timeout, state}` will cause `start_link/3` to return
|
||
|
`{:ok, pid}` and the process to enter its normal loop with state `state`.
|
||
|
`connect(:backoff, state)` is called after `timeout` if `connect/2` or
|
||
|
`disconnect/2` is not called within the timeout.
|
||
|
|
||
|
This return value can be used to delay or stagger the initial connection
|
||
|
attempt.
|
||
|
|
||
|
Returning `{:backoff, timeout, state, timeout2}` is similar to
|
||
|
`{:backoff, timeout, state}` except `handle_info(:timeout, state)` will be
|
||
|
called after `timeout2` if no message arrives.
|
||
|
|
||
|
Returning `{:backoff, timeout, state, :hibernate}` is similar to
|
||
|
`{:backoff, timeout, state}` except the process hibernates.
|
||
|
|
||
|
Returning `:ignore` will cause `start_link/3` to return `:ignore` and the
|
||
|
process will exit normally without entering the loop or calling
|
||
|
`terminate/2`.
|
||
|
|
||
|
Returning `{:stop, reason}` will cause `start_link/3` to return
|
||
|
`{:error, reason}` and the process to exit with reason `reason` without
|
||
|
entering the loop or calling `terminate/2`.
|
||
|
"""
|
||
|
@callback init(any) ::
|
||
|
{:ok, any} | {:ok, any, timeout | :hibernate} |
|
||
|
{:connect, any, any} |
|
||
|
{:backoff, timeout, any} | {:backoff, timeout, any, timeout | :hibernate} |
|
||
|
:ignore | {:stop, any}
|
||
|
|
||
|
|
||
|
@doc """
|
||
|
Called when the process should try to connect. The first argument will either
|
||
|
be the `info` term from `{:connect, info, state}` or
|
||
|
`{:connect, info, reply, state}`, or `:backoff` if the connection attempt is
|
||
|
triggered by backing off.
|
||
|
|
||
|
It might be beneficial to do handshaking in this callback if connecting is
|
||
|
successful.
|
||
|
|
||
|
Returning `{:ok, state}` or `{:ok, state, timeout | :hibernate}` will cause
|
||
|
the process to continue its loop. This should be returned when the connection
|
||
|
attempt was successful. In the later case `handle_info(:timeout, state)` is
|
||
|
called after `timeout` if no message has been received, if the third element
|
||
|
is a timeout. Otherwise if the third element is `:hibernate` the process
|
||
|
hibernates.
|
||
|
|
||
|
Returning `{:backoff, timeout, state}` will cause the process to continue
|
||
|
its loop but `connect(:backoff, state)` will be called after `timeout` if
|
||
|
`connect/2` or `disconnect/2` is not called before that point.
|
||
|
|
||
|
This return value is used when a connection attempt fails but another attempt
|
||
|
should be made after a delay. It might be beneficial to increase the delay
|
||
|
up to a maximum if successive attempts fail to prevent unnecessary work. If
|
||
|
several connection processes are connecting to the same peer it may also be
|
||
|
beneficial to add some jitter (randomness) to the delays. This spreads out the
|
||
|
connection attempts and helps prevent many attempts occurring at the same time.
|
||
|
|
||
|
Returning `{:backoff, timeout, state, timeout2 | :hibernate}` is similar to
|
||
|
`{:backoff, timeout, state}` except `handle_info(:timeout, state)` is called
|
||
|
after `timeout2` if no message has been received, or if `:hibernate`, the
|
||
|
process hibernates.
|
||
|
|
||
|
Returning `{:stop, reason, state}` will terminate the loop and call
|
||
|
`terminate(reason, state)` before the process exits with reason `reason`.
|
||
|
"""
|
||
|
@callback connect(any, any) ::
|
||
|
{:ok, any} | {:ok, any, timeout | :hibernate} |
|
||
|
{:backoff, timeout, any} | {:backoff, timeout, any, timeout | :hibernate} |
|
||
|
{:stop, any, any}
|
||
|
|
||
|
@doc """
|
||
|
Called when the process should disconnect. The first argument will either
|
||
|
be the `info` term from `{:disconnect, info, state}` or
|
||
|
`{:disconnect, info, reply, state}`. This callback should do any cleaning up
|
||
|
required to disconnect the connection and update the state of the process.
|
||
|
|
||
|
Returning `{:connect, info, state}` will call `connect(info, state)`
|
||
|
immediately - even if there are messages in the message queue.
|
||
|
|
||
|
Returning `{:backoff, timeout, state}` or
|
||
|
`{:backoff, timeout, state, timeout2 | :hibernate}` starts a backoff timer and
|
||
|
behaves the same as when returned from `connect/2`. See the `connect/2`
|
||
|
callback for more information.
|
||
|
|
||
|
Returning `{:noconnect, state}` or `{:noconnect, state, timeout | :hibernate}`
|
||
|
will cause the process to continue is loop (and NOT call `connect/2` to
|
||
|
try to reconnect). In the later case a timeout is started or the process
|
||
|
hibernates.
|
||
|
|
||
|
Returning `{:stop, reason, state}` will terminate the loop and call
|
||
|
`terminate(reason, state)` before the process exits with reason `reason`.
|
||
|
"""
|
||
|
@callback disconnect(any, any) ::
|
||
|
{:connect, any, any} |
|
||
|
{:backoff, timeout, any} | {:backoff, timeout, any, timeout | :hibernate} |
|
||
|
{:noconnect, any} | {:noconnect, any, timeout | :hibernate} |
|
||
|
{:stop, any, any}
|
||
|
|
||
|
@doc """
|
||
|
Called when the process receives a call message sent by `call/3`. This
|
||
|
callback has the same arguments as the `GenServer` equivalent and the
|
||
|
`:reply`, `:noreply` and `:stop` return tuples behave the same. However
|
||
|
there are a few additional return values:
|
||
|
|
||
|
Returning `{:connect, info, reply, state}` will reply to the call with `reply`
|
||
|
and immediately call `connect(info, state)`. Similarly for
|
||
|
`{:disconnect, info, reply, state}`, except `disconnect/2` is called.
|
||
|
|
||
|
Returning `{:connect, info, state}` or `{:disconnect, info, state}` will
|
||
|
call the relevant callback immediately without replying to the call. This
|
||
|
might be useful when the call should block until the process has connected,
|
||
|
failed to connect or disconnected. The second argument passed to this callback
|
||
|
can be included in the `info` or `state` terms and a reply sent in the next
|
||
|
or a later callback using `reply/2`.
|
||
|
"""
|
||
|
@callback handle_call(any, {pid, any}, any) ::
|
||
|
{:reply, any, any} | {:reply, any, any, timeout | :hibernate} |
|
||
|
{:noreply, any} | {:noreply, any, timeout | :hibernate} |
|
||
|
{:disconnect | :connect, any, any, any} |
|
||
|
{:disconnect | :connect, any, any} |
|
||
|
{:stop, any, any} | {:stop, any, any, any}
|
||
|
|
||
|
@doc """
|
||
|
Called when the process receives a cast message sent by `cast/3`. This
|
||
|
callback has the same arguments as the `GenServer` equivalent and the
|
||
|
`:noreply` and `:stop` return tuples behave the same. However
|
||
|
there are two additional return values:
|
||
|
|
||
|
Returning `{:connect, info, state}` will immediately call
|
||
|
`connect(info, state)`. Similarly for `{:disconnect, info, state}`,
|
||
|
except `disconnect/2` is called.
|
||
|
"""
|
||
|
@callback handle_cast(any, any) ::
|
||
|
{:noreply, any} | {:noreply, any, timeout | :hibernate} |
|
||
|
{:disconnect | :connect, any, any} |
|
||
|
{:stop, any, any}
|
||
|
|
||
|
@doc """
|
||
|
Called when the process receives a message that is not a call or cast. This
|
||
|
callback has the same arguments as the `GenServer` equivalent and the `:noreply`
|
||
|
and `:stop` return tuples behave the same. However there are two additional
|
||
|
return values:
|
||
|
|
||
|
Returning `{:connect, info, state}` will immediately call
|
||
|
`connect(info, state)`. Similarly for `{:disconnect, info, state}`,
|
||
|
except `disconnect/2` is called.
|
||
|
"""
|
||
|
@callback handle_info(any, any) ::
|
||
|
{:noreply, any} | {:noreply, any, timeout | :hibernate} |
|
||
|
{:disconnect | :connect, any, any} |
|
||
|
{:stop, any, any}
|
||
|
|
||
|
@doc """
|
||
|
This callback is the same as the `GenServer` equivalent and is used to change
|
||
|
the state when loading a different version of the callback module.
|
||
|
"""
|
||
|
@callback code_change(any, any, any) :: {:ok, any}
|
||
|
|
||
|
@doc """
|
||
|
This callback is the same as the `GenServer` equivalent and is called when the
|
||
|
process terminates. The first argument is the reason the process is about
|
||
|
to exit with.
|
||
|
"""
|
||
|
@callback terminate(any, any) :: any
|
||
|
|
||
|
defmacro __using__(_) do
|
||
|
quote location: :keep do
|
||
|
@behaviour Connection
|
||
|
|
||
|
# The default implementations of init/1, handle_call/3, handle_info/2,
|
||
|
# handle_cast/2, terminate/2 and code_change/3 have been taken verbatim
|
||
|
# from Elixir's GenServer default implementation.
|
||
|
|
||
|
@doc false
|
||
|
def init(args) do
|
||
|
{:ok, args}
|
||
|
end
|
||
|
|
||
|
@doc false
|
||
|
def handle_call(msg, _from, state) do
|
||
|
# We do this to trick dialyzer to not complain about non-local returns.
|
||
|
reason = {:bad_call, msg}
|
||
|
case :erlang.phash2(1, 1) do
|
||
|
0 -> exit(reason)
|
||
|
1 -> {:stop, reason, state}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
@doc false
|
||
|
def handle_info(_msg, state) do
|
||
|
{:noreply, state}
|
||
|
end
|
||
|
|
||
|
@doc false
|
||
|
def handle_cast(msg, state) do
|
||
|
# We do this to trick dialyzer to not complain about non-local returns.
|
||
|
reason = {:bad_cast, msg}
|
||
|
case :erlang.phash2(1, 1) do
|
||
|
0 -> exit(reason)
|
||
|
1 -> {:stop, reason, state}
|
||
|
end
|
||
|
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 connect(info, state) do
|
||
|
reason = {:bad_connect, info}
|
||
|
case :erlang.phash2(1, 1) do
|
||
|
0 -> exit(reason)
|
||
|
1 -> {:stop, reason, state}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
@doc false
|
||
|
def disconnect(info, state) do
|
||
|
reason = {:bad_disconnect, info}
|
||
|
case :erlang.phash2(1, 1) do
|
||
|
0 -> exit(reason)
|
||
|
1 -> {:stop, reason, state}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defoverridable [init: 1, handle_call: 3, handle_info: 2,
|
||
|
handle_cast: 2, terminate: 2, code_change: 3,
|
||
|
connect: 2, disconnect: 2]
|
||
|
end
|
||
|
end
|
||
|
|
||
|
@doc """
|
||
|
Starts a `Connection` process linked to the current process.
|
||
|
|
||
|
This function is used to start a `Connection` process in a supervision tree.
|
||
|
The process will be started by calling `init/1` in the callback module with
|
||
|
the given argument.
|
||
|
|
||
|
This function will return after `init/1` has returned in the spawned process.
|
||
|
The return values are controlled by the `init/1` callback.
|
||
|
|
||
|
See `GenServer.start_link/3` for more information.
|
||
|
"""
|
||
|
@spec start_link(module, any, GenServer.options) :: GenServer.on_start
|
||
|
def start_link(mod, args, opts \\ []) do
|
||
|
start(mod, args, opts, :link)
|
||
|
end
|
||
|
|
||
|
@doc """
|
||
|
Starts a `Connection` process without links (outside of a supervision tree).
|
||
|
|
||
|
See `start_link/3` for more information.
|
||
|
"""
|
||
|
@spec start(module, any, GenServer.options) :: GenServer.on_start
|
||
|
def start(mod, args, opts \\ []) do
|
||
|
start(mod, args, opts, :nolink)
|
||
|
end
|
||
|
|
||
|
@doc """
|
||
|
Sends a synchronous call to the `Connection` process and waits for a reply.
|
||
|
|
||
|
See `GenServer.call/2` for more information.
|
||
|
"""
|
||
|
defdelegate call(conn, req), to: :gen_server
|
||
|
|
||
|
@doc """
|
||
|
Sends a synchronous request to the `Connection` process and waits for a reply.
|
||
|
|
||
|
See `GenServer.call/3` for more information.
|
||
|
"""
|
||
|
defdelegate call(conn, req, timeout), to: :gen_server
|
||
|
|
||
|
@doc """
|
||
|
Sends a asynchronous request to the `Connection` process.
|
||
|
|
||
|
See `GenServer.cast/2` for more information.
|
||
|
"""
|
||
|
defdelegate cast(conn, req), to: GenServer
|
||
|
|
||
|
@doc """
|
||
|
Sends a reply to a request sent by `call/3`.
|
||
|
|
||
|
See `GenServer.reply/2` for more information.
|
||
|
"""
|
||
|
defdelegate reply(from, response), to: :gen_server
|
||
|
|
||
|
defstruct [:mod, :backoff, :raise, :mod_state]
|
||
|
|
||
|
## :gen callback
|
||
|
|
||
|
@doc false
|
||
|
def init_it(starter, _, name, mod, args, opts) do
|
||
|
Process.put(:"$initial_call", {mod, :init, 1})
|
||
|
try do
|
||
|
apply(mod, :init, [args])
|
||
|
catch
|
||
|
:exit, reason ->
|
||
|
init_stop(starter, name, reason)
|
||
|
:error, reason ->
|
||
|
init_stop(starter, name, {reason, __STACKTRACE__})
|
||
|
:throw, value ->
|
||
|
reason = {{:nocatch, value}, __STACKTRACE__}
|
||
|
init_stop(starter, name, reason)
|
||
|
else
|
||
|
{:ok, mod_state} ->
|
||
|
:proc_lib.init_ack(starter, {:ok, self()})
|
||
|
enter_loop(mod, nil, mod_state, name, opts, :infinity)
|
||
|
{:ok, mod_state, timeout} ->
|
||
|
:proc_lib.init_ack(starter, {:ok, self()})
|
||
|
enter_loop(mod, nil, mod_state, name, opts, timeout)
|
||
|
{:connect, info, mod_state} ->
|
||
|
:proc_lib.init_ack(starter, {:ok, self()})
|
||
|
enter_connect(mod, info, mod_state, name, opts)
|
||
|
{:backoff, backoff_timeout, mod_state} ->
|
||
|
backoff = start_backoff(backoff_timeout)
|
||
|
:proc_lib.init_ack(starter, {:ok, self()})
|
||
|
enter_loop(mod, backoff, mod_state, name, opts, :infinity)
|
||
|
{:backoff, backoff_timeout, mod_state, timeout} ->
|
||
|
backoff = start_backoff(backoff_timeout)
|
||
|
:proc_lib.init_ack(starter, {:ok, self()})
|
||
|
enter_loop(mod, backoff, mod_state, name, opts, timeout)
|
||
|
:ignore ->
|
||
|
_ = unregister(name)
|
||
|
:proc_lib.init_ack(starter, :ignore)
|
||
|
exit(:normal)
|
||
|
{:stop, reason} ->
|
||
|
init_stop(starter, name, reason)
|
||
|
other ->
|
||
|
init_stop(starter, name, {:bad_return_value, other})
|
||
|
end
|
||
|
end
|
||
|
## :proc_lib callback
|
||
|
|
||
|
@doc false
|
||
|
def enter_loop(mod, backoff, mod_state, name, opts, :hibernate) do
|
||
|
args = [mod, backoff, mod_state, name, opts, :infinity]
|
||
|
:proc_lib.hibernate(__MODULE__, :enter_loop, args)
|
||
|
end
|
||
|
def enter_loop(mod, backoff, mod_state, name, opts, timeout)
|
||
|
when name === self() do
|
||
|
s = %Connection{mod: mod, backoff: backoff, mod_state: mod_state,
|
||
|
raise: nil}
|
||
|
:gen_server.enter_loop(__MODULE__, opts, s, timeout)
|
||
|
end
|
||
|
def enter_loop(mod, backoff, mod_state, name, opts, timeout) do
|
||
|
s = %Connection{mod: mod, backoff: backoff, mod_state: mod_state,
|
||
|
raise: nil}
|
||
|
:gen_server.enter_loop(__MODULE__, opts, s, name, timeout)
|
||
|
end
|
||
|
|
||
|
@doc false
|
||
|
def init(_) do
|
||
|
{:stop, __MODULE__}
|
||
|
end
|
||
|
|
||
|
@doc false
|
||
|
def handle_call(request, from, %{mod: mod, mod_state: mod_state} = s) do
|
||
|
try do
|
||
|
apply(mod, :handle_call, [request, from, mod_state])
|
||
|
catch
|
||
|
:throw, value ->
|
||
|
:erlang.raise(:error, {:nocatch, value}, __STACKTRACE__)
|
||
|
else
|
||
|
{:noreply, mod_state} = noreply ->
|
||
|
put_elem(noreply, 1, %{s | mod_state: mod_state})
|
||
|
{:noreply, mod_state, _} = noreply ->
|
||
|
put_elem(noreply, 1, %{s | mod_state: mod_state})
|
||
|
{:reply, _, mod_state} = reply ->
|
||
|
put_elem(reply, 2, %{s | mod_state: mod_state})
|
||
|
{:reply, _, mod_state, _} = reply ->
|
||
|
put_elem(reply, 2, %{s | mod_state: mod_state})
|
||
|
{:connect, info, mod_state} ->
|
||
|
connect(info, mod_state, s)
|
||
|
{:connect, info, reply, mod_state} ->
|
||
|
reply(from, reply)
|
||
|
connect(info, mod_state, s)
|
||
|
{:disconnect, info, mod_state} ->
|
||
|
disconnect(info, mod_state, s)
|
||
|
{:disconnect, info, reply, mod_state} ->
|
||
|
reply(from, reply)
|
||
|
disconnect(info, mod_state, s)
|
||
|
{:stop, _, mod_state} = stop ->
|
||
|
put_elem(stop, 2, %{s | mod_state: mod_state})
|
||
|
{:stop, _, _, mod_state} = stop ->
|
||
|
put_elem(stop, 3, %{s | mod_state: mod_state})
|
||
|
other ->
|
||
|
{:stop, {:bad_return_value, other}, %{s | mod_state: mod_state}}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
@doc false
|
||
|
def handle_cast(request, s) do
|
||
|
handle_async(:handle_cast, request, s)
|
||
|
end
|
||
|
|
||
|
@doc false
|
||
|
def handle_info({:timeout, backoff, __MODULE__},
|
||
|
%{backoff: backoff, mod_state: mod_state} = s) do
|
||
|
connect(:backoff, mod_state, %{s | backoff: nil})
|
||
|
end
|
||
|
def handle_info(msg, s) do
|
||
|
handle_async(:handle_info, msg, s)
|
||
|
end
|
||
|
|
||
|
@doc false
|
||
|
def code_change(old_vsn, %{mod: mod, mod_state: mod_state} = s, extra) do
|
||
|
try do
|
||
|
apply(mod, :code_change, [old_vsn, mod_state, extra])
|
||
|
catch
|
||
|
:throw, value ->
|
||
|
exit({{:nocatch, value}, __STACKTRACE__})
|
||
|
else
|
||
|
{:ok, mod_state} ->
|
||
|
{:ok, %{s | mod_state: mod_state}}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
@doc false
|
||
|
def format_status(:normal, [pdict, %{mod: mod, mod_state: mod_state}]) do
|
||
|
try do
|
||
|
apply(mod, :format_status, [:normal, [pdict, mod_state]])
|
||
|
catch
|
||
|
_, _ ->
|
||
|
[{:data, [{'State', mod_state}]}]
|
||
|
else
|
||
|
mod_status ->
|
||
|
mod_status
|
||
|
end
|
||
|
end
|
||
|
def format_status(:terminate, [pdict, %{mod: mod, mod_state: mod_state}]) do
|
||
|
try do
|
||
|
apply(mod, :format_status, [:terminate, [pdict, mod_state]])
|
||
|
catch
|
||
|
_, _ ->
|
||
|
mod_state
|
||
|
else
|
||
|
mod_state ->
|
||
|
mod_state
|
||
|
end
|
||
|
end
|
||
|
|
||
|
@doc false
|
||
|
def terminate(reason, %{mod: mod, mod_state: mod_state, raise: nil}) do
|
||
|
apply(mod, :terminate, [reason, mod_state])
|
||
|
end
|
||
|
def terminate(stop, %{raise: {class, reason, stack}} = s) do
|
||
|
%{mod: mod, mod_state: mod_state} = s
|
||
|
try do
|
||
|
apply(mod, :terminate, [stop, mod_state])
|
||
|
catch
|
||
|
:throw, value ->
|
||
|
:erlang.raise(:error, {:nocatch, value}, __STACKTRACE__)
|
||
|
else
|
||
|
_ when stop in [:normal, :shutdown] ->
|
||
|
:ok
|
||
|
_ when tuple_size(stop) == 2 and elem(stop, 0) == :shutdown ->
|
||
|
:ok
|
||
|
_ ->
|
||
|
:erlang.raise(class, reason, stack)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# start helpers
|
||
|
|
||
|
defp start(mod, args, options, link) do
|
||
|
case Keyword.pop(options, :name) do
|
||
|
{nil, opts} ->
|
||
|
:gen.start(__MODULE__, link, mod, args, opts)
|
||
|
{atom, opts} when is_atom(atom) ->
|
||
|
:gen.start(__MODULE__, link, {:local, atom}, mod, args, opts)
|
||
|
{{:global, _} = name, opts} ->
|
||
|
:gen.start(__MODULE__, link, name, mod, args, opts)
|
||
|
{{:via, _, _} = name, opts} ->
|
||
|
:gen.start(__MODULE__, link, name, mod, args, opts)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# init helpers
|
||
|
|
||
|
defp init_stop(starter, name, reason) do
|
||
|
_ = unregister(name)
|
||
|
:proc_lib.init_ack(starter, {:error, reason})
|
||
|
exit(reason)
|
||
|
end
|
||
|
|
||
|
defp unregister(name) when name === self(), do: :ok
|
||
|
defp unregister({:local, name}), do: Process.unregister(name)
|
||
|
defp unregister({:global, name}), do: :global.unregister_name(name)
|
||
|
defp unregister({:via, mod, name}), do: apply(mod, :unregister_name, [name])
|
||
|
|
||
|
defp enter_connect(mod, info, mod_state, name, opts) do
|
||
|
try do
|
||
|
apply(mod, :connect, [info, mod_state])
|
||
|
catch
|
||
|
:exit, reason ->
|
||
|
report_reason = {:EXIT, {reason, __STACKTRACE__}}
|
||
|
enter_terminate(mod, mod_state, name, reason, report_reason)
|
||
|
:error, reason ->
|
||
|
reason = {reason, __STACKTRACE__}
|
||
|
enter_terminate(mod, mod_state, name, reason, {:EXIT, reason})
|
||
|
:throw, value ->
|
||
|
reason = {{:nocatch, value}, __STACKTRACE__}
|
||
|
enter_terminate(mod, mod_state, name, reason, {:EXIT, reason})
|
||
|
else
|
||
|
{:ok, mod_state} ->
|
||
|
enter_loop(mod, nil, mod_state, name, opts, :infinity)
|
||
|
{:ok, mod_state, timeout} ->
|
||
|
enter_loop(mod, nil, mod_state, name, opts, timeout)
|
||
|
{:backoff, backoff_timeout, mod_state} ->
|
||
|
backoff = start_backoff(backoff_timeout)
|
||
|
enter_loop(mod, backoff, mod_state, name, opts, :infinity)
|
||
|
{:backoff, backoff_timeout, mod_state, timeout} ->
|
||
|
backoff = start_backoff(backoff_timeout)
|
||
|
enter_loop(mod, backoff, mod_state, name, opts, timeout)
|
||
|
{:stop, reason, mod_state} ->
|
||
|
enter_terminate(mod, mod_state, name, reason, {:stop, reason})
|
||
|
other ->
|
||
|
reason = {:bad_return_value, other}
|
||
|
enter_terminate(mod, mod_state, name, reason, {:stop, reason})
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defp enter_terminate(mod, mod_state, name, reason, report_reason) do
|
||
|
try do
|
||
|
apply(mod, :terminate, [reason, mod_state])
|
||
|
catch
|
||
|
:exit, reason ->
|
||
|
report_reason = {:EXIT, {reason, __STACKTRACE__}}
|
||
|
enter_stop(mod, mod_state, name, reason, report_reason)
|
||
|
:error, reason ->
|
||
|
reason = {reason, __STACKTRACE__}
|
||
|
enter_stop(mod, mod_state, name, reason, {:EXIT, reason})
|
||
|
:throw, value ->
|
||
|
reason = {{:nocatch, value}, __STACKTRACE__}
|
||
|
enter_stop(mod, mod_state, name, reason, {:EXIT, reason})
|
||
|
else
|
||
|
_ ->
|
||
|
enter_stop(mod, mod_state, name, reason, report_reason)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defp enter_stop(_, _, _, :normal, {:stop, :normal}), do: exit(:normal)
|
||
|
defp enter_stop(_, _, _, :shutdown, {:stop, :shutdown}), do: exit(:shutdown)
|
||
|
defp enter_stop(_, _, _, {:shutdown, reason} = shutdown,
|
||
|
{:stop, {:shutdown, reason}}) do
|
||
|
exit(shutdown)
|
||
|
end
|
||
|
defp enter_stop(mod, mod_state, name, reason, {_, reason2}) do
|
||
|
s = %{mod: mod, backoff: nil, mod_state: mod_state}
|
||
|
mod_state = format_status(:terminate, [Process.get(), s])
|
||
|
format = '** Generic server ~p terminating \n' ++
|
||
|
'** Last message in was ~p~n' ++ ## No last message
|
||
|
'** When Server state == ~p~n' ++
|
||
|
'** Reason for termination == ~n** ~p~n'
|
||
|
args = [report_name(name), nil, mod_state, report_reason(reason2)]
|
||
|
:error_logger.format(format, args)
|
||
|
exit(reason)
|
||
|
end
|
||
|
|
||
|
defp report_name(name) when name === self(), do: name
|
||
|
defp report_name({:local, name}), do: name
|
||
|
defp report_name({:global, name}), do: name
|
||
|
defp report_name({:via, _, name}), do: name
|
||
|
|
||
|
defp report_reason({:undef, [{mod, fun, args, _} | _] = stack} = reason) do
|
||
|
cond do
|
||
|
:code.is_loaded(mod) === false ->
|
||
|
{:"module could not be loaded", stack}
|
||
|
not function_exported?(mod, fun, length(args)) ->
|
||
|
{:"function not exported", stack}
|
||
|
true ->
|
||
|
reason
|
||
|
end
|
||
|
end
|
||
|
defp report_reason(reason) do
|
||
|
reason
|
||
|
end
|
||
|
|
||
|
## backoff helpers
|
||
|
|
||
|
defp start_backoff(:infinity), do: nil
|
||
|
defp start_backoff(timeout) do
|
||
|
:erlang.start_timer(timeout, self(), __MODULE__)
|
||
|
end
|
||
|
|
||
|
defp cancel_backoff(%{backoff: nil} = s), do: s
|
||
|
defp cancel_backoff(%{backoff: backoff} = s) do
|
||
|
case :erlang.cancel_timer(backoff) do
|
||
|
false ->
|
||
|
flush_backoff(backoff)
|
||
|
_ ->
|
||
|
:ok
|
||
|
end
|
||
|
%{s | backoff: nil}
|
||
|
end
|
||
|
|
||
|
defp flush_backoff(backoff) do
|
||
|
receive do
|
||
|
{:timeout, ^backoff, __MODULE__} ->
|
||
|
:ok
|
||
|
after
|
||
|
0 ->
|
||
|
:ok
|
||
|
end
|
||
|
end
|
||
|
|
||
|
## GenServer helpers
|
||
|
|
||
|
defp connect(info, mod_state, %{mod: mod} = s) do
|
||
|
s = cancel_backoff(s)
|
||
|
try do
|
||
|
apply(mod, :connect, [info, mod_state])
|
||
|
catch
|
||
|
class, reason ->
|
||
|
stack = __STACKTRACE__
|
||
|
callback_stop(class, reason, stack, %{s | mod_state: mod_state})
|
||
|
else
|
||
|
{:ok, mod_state} ->
|
||
|
{:noreply, %{s | mod_state: mod_state}}
|
||
|
{:ok, mod_state, timeout} ->
|
||
|
{:noreply, %{s | mod_state: mod_state}, timeout}
|
||
|
{:backoff, backoff_timeout, mod_state} ->
|
||
|
backoff = start_backoff(backoff_timeout)
|
||
|
{:noreply, %{s | backoff: backoff, mod_state: mod_state}}
|
||
|
{:backoff, backoff_timeout, mod_state, timeout} ->
|
||
|
backoff = start_backoff(backoff_timeout)
|
||
|
{:noreply, %{s | backoff: backoff, mod_state: mod_state}, timeout}
|
||
|
{:stop, _, mod_state} = stop ->
|
||
|
put_elem(stop, 2, %{s | mod_state: mod_state})
|
||
|
other ->
|
||
|
{:stop, {:bad_return_value, other}, %{s | mod_state: mod_state}}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defp disconnect(info, mod_state, %{mod: mod} = s) do
|
||
|
s = cancel_backoff(s)
|
||
|
try do
|
||
|
apply(mod, :disconnect, [info, mod_state])
|
||
|
catch
|
||
|
class, reason ->
|
||
|
stack = __STACKTRACE__
|
||
|
callback_stop(class, reason, stack, %{s | mod_state: mod_state})
|
||
|
else
|
||
|
{:connect, info, mod_state} ->
|
||
|
connect(info, mod_state, s)
|
||
|
{:noconnect, mod_state} ->
|
||
|
{:noreply, %{s | mod_state: mod_state}}
|
||
|
{:noconnect, mod_state, timeout} ->
|
||
|
{:noreply, %{s | mod_state: mod_state}, timeout}
|
||
|
{:backoff, backoff_timeout, mod_state} ->
|
||
|
backoff = start_backoff(backoff_timeout)
|
||
|
{:noreply, %{s | backoff: backoff, mod_state: mod_state}}
|
||
|
{:backoff, backoff_timeout, mod_state, timeout} ->
|
||
|
backoff = start_backoff(backoff_timeout)
|
||
|
{:noreply, %{s | backoff: backoff, mod_state: mod_state}, timeout}
|
||
|
{:stop, _, mod_state} = stop ->
|
||
|
put_elem(stop, 2, %{s | mod_state: mod_state})
|
||
|
other ->
|
||
|
{:stop, {:bad_return_value, other}, %{s | mod_state: mod_state}}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# In order to have new mod_state in terminate/2 must return the exit reason.
|
||
|
# However to get the correct GenServer report (exit with stacktrace),
|
||
|
# include stacktrace in reason and re-raise after calling mod.terminate/2 if
|
||
|
# it does not raise.
|
||
|
|
||
|
defp callback_stop(:throw, value, stack, s) do
|
||
|
callback_stop(:error, {:nocatch, value}, stack, s)
|
||
|
end
|
||
|
defp callback_stop(class, reason, stack, s) do
|
||
|
raise = {class, reason, stack}
|
||
|
{:stop, stop_reason(class, reason, stack), %{s | raise: raise}}
|
||
|
end
|
||
|
|
||
|
defp stop_reason(:error, reason, stack), do: {reason, stack}
|
||
|
defp stop_reason(:exit, reason, _), do: reason
|
||
|
|
||
|
defp handle_async(fun, msg, %{mod: mod, mod_state: mod_state} = s) do
|
||
|
try do
|
||
|
apply(mod, fun, [msg, mod_state])
|
||
|
catch
|
||
|
:throw, value ->
|
||
|
:erlang.raise(:error, {:nocatch, value}, __STACKTRACE__)
|
||
|
else
|
||
|
{:noreply, mod_state} = noreply ->
|
||
|
put_elem(noreply, 1, %{s | mod_state: mod_state})
|
||
|
{:noreply, mod_state, _} = noreply ->
|
||
|
put_elem(noreply, 1, %{s | mod_state: mod_state})
|
||
|
{:connect, info, mod_state} ->
|
||
|
connect(info, mod_state, s)
|
||
|
{:disconnect, info, mod_state} ->
|
||
|
disconnect(info, mod_state, s)
|
||
|
{:stop, _, mod_state} = stop ->
|
||
|
put_elem(stop, 2, %{s | mod_state: mod_state})
|
||
|
other ->
|
||
|
{:stop, {:bad_return_value, other}, %{s | mod_state: mod_state}}
|
||
|
end
|
||
|
end
|
||
|
end
|