cat-bookmarker/deps/connection/lib/connection.ex

830 lines
29 KiB
Elixir
Raw Normal View History

2024-03-10 18:52:04 +00:00
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