Abyss.Handler behaviour (Abyss v0.3.0)
View SourceAbyss.Handler
defines the behaviour required of the application layer of a Abyss server.
Example
Another example of a server that echoes back all data sent to it is as follows:
defmodule Echo do
use Abyss.Handler
@impl Abyss.Handler
def handle_data(data, state) do
Abyss.Transport.UDP.send(state.socket, data)
{:continue, state}
end
end
Note that in this example there is no c:handle_connection/2
callback defined. The default implementation of this
callback will simply return {:continue, state}
, which is appropriate for cases where the client is the first
party to communicate.
Another example of a server which can send and receive messages asynchronously is as follows:
defmodule Messenger do
use Abyss.Handler
@impl Abyss.Handler
def handle_data(msg, state) do
IO.inspect(msg)
{:continue, state}
end
def handle_info({:udp, socket, ip, port, data}, state) do
Abyss.Transport.UDP.send(socket, ip, port, msg)
{:noreply, state, state.read_timeout}
end
end
Note that in this example we make use of the fact that the handler process is really just a GenServer to send it messages which are able to make use of the underlying socket. This allows for bidirectional sending and receiving of messages in an asynchronous manner.
You can pass options to the default handler underlying GenServer
by passing a genserver_options
key to Abyss.start_link/1
containing GenServer.options/0
to be passed to the last argument of GenServer.start_link/3
.
Please note that you should not pass the name
GenServer.option/0
. If you need to register handler processes for
later lookup and use, you should perform process registration in handle_connection/2
, ensuring the handler process is
registered only after the underlying connection is established and you have access to the connection socket and metadata
via Abyss.Transport.UDP.peername/1
.
For example, using a custom process registry via Registry
:
defmodule Messenger do
use Abyss.Handler
@impl Abyss.Handler
def handle_data(recv_data, state) do
{ip, port, data} = recv_data
Abyss.Transport.UDP.send(state.socket, ip, port, data)
{:continue, state}
end
end
This example assumes you have started a Registry
and registered it under the name MessengerRegistry
.
When Handler Isn't Enough
The use Abyss.Handler
implementation should be flexible enough to power just about any handler, however if
this should not be the case for you, there is an escape hatch available. If you require more flexibility than the
Abyss.Handler
behaviour provides, you are free to specify any module which implements start_link/1
as the
handler_module
parameter. The process of getting from this new process to a ready-to-use socket is somewhat
delicate, however. The steps required are as follows:
- Abyss calls
start_link/1
on the configuredhandler_module
, passing in a tuple consisting of the configured handler and genserver opts. This function is expected to return a conventionalGenServer.on_start()
style tuple. Note that this newly created process is not passed the connection socket immediately. - The raw
t:Abyss.Transport.socket()
socket will be passed to the new process via a message of the form{:abyss_received, listener_socket, server_config, acceptor_span, start_time}
. - Your implenentation must turn this into a
to::inet.socket()
socket by using theAbyss.Transport.UDP.new/3
call. - Your implementation must then call
Abyss.Transport.UDP.handshake/1
with the socket as the sole argument in order to finalize the setup of the socket. - The socket is now ready to use.
In addition to this process, there are several other considerations to be aware of:
The underlying socket is closed automatically when the handler process ends.
Handler processes should have a restart strategy of
:temporary
to ensure that Abyss does not attempt to restart crashed handlers.Handler processes should trap exit if possible so that existing connections can be given a chance to cleanly shut down when shutting down a Abyss server instance.
Some of the
:connection
family of telemetry span events are emitted by theAbyss.Handler
implementation. If you use your own implementation in its place it is likely that such spans will not behave as expected.
Summary
Types
The value returned by c:handle_connection/2
and c:handle_data/3
The possible ways to indicate a timeout when returning values to Abyss
Callbacks
This callback is called when the underlying socket is closed by the remote end; it should perform any cleanup required as it is the last callback called before the process backing this connection is terminated. The underlying socket has already been closed by the time this callback is called. The return value is ignored.
This callback is called whenever client data is received after c:handle_connection/2
or c:handle_data/3
have returned an
{:continue, state}
tuple. The data received is passed as the first argument, and handlers may choose to interact
synchronously with the socket in this callback via calls to various Abyss.Transport.UDP
functions.
This callback is called when the underlying socket encounters an error; it should perform any cleanup required as it is the last callback called before the process backing this connection is terminated. The underlying socket has already been closed by the time this callback is called. The return value is ignored.
This callback is called when the server process itself is being shut down; it should perform any cleanup required as it is the last callback called before the process backing this connection is terminated. The underlying socket has NOT been closed by the time this callback is called. The return value is ignored.
This callback is called when a handler process has gone more than timeout
ms without receiving
either remote data or a local message. The value used for timeout
defaults to the
read_timeout
value specified at server startup, and may be overridden on a one-shot or
persistent basis based on values returned from c:handle_connection/2
or c:handle_data/3
calls. Note that it is NOT called on explicit Abyss.Transport.UDP.recv/3
calls as they have
their own timeout semantics. The underlying socket has NOT been closed by the time this callback
is called. The return value is ignored.
Types
@type handler_result() :: {:continue, state :: term()} | {:continue, state :: term(), timeout_options()} | {:close, state :: term()} | {:error, term(), state :: term()}
The value returned by c:handle_connection/2
and c:handle_data/3
The possible ways to indicate a timeout when returning values to Abyss
Callbacks
This callback is called when the underlying socket is closed by the remote end; it should perform any cleanup required as it is the last callback called before the process backing this connection is terminated. The underlying socket has already been closed by the time this callback is called. The return value is ignored.
This callback is not called if the connection is explicitly closed via Abyss.Transport.UDP.close/1
, however it
will be called in cases where handle_connection/2
or handle_data/3
return a {:close, state}
tuple.
@callback handle_data(data :: Abyss.Transport.recv_data(), state :: term()) :: handler_result()
This callback is called whenever client data is received after c:handle_connection/2
or c:handle_data/3
have returned an
{:continue, state}
tuple. The data received is passed as the first argument, and handlers may choose to interact
synchronously with the socket in this callback via calls to various Abyss.Transport.UDP
functions.
The value returned by this callback causes Abyss to proceed in one of several ways:
- Returning
{:close, state}
will cause Abyss to close the socket & call thec:handle_close/2
callback to allow final cleanup to be done. - Returning
{:continue, state}
will cause Abyss to switch the socket to an asynchronous mode. When the client subsequently sends data (or if there is already unread data waiting from the client), Abyss will callc:handle_data/3
to allow this data to be processed. - Returning
{:continue, state, timeout}
is identical to the previous case with the addition of a timeout. Iftimeout
milliseconds passes with no data being received or messages being sent to the process, the socket will be closed andc:handle_timeout/2
will be called. Note that this timeout is not persistent; it applies only to the interval until the next message is received. In order to set a persistent timeout for all future messages (essentially overwriting the value ofread_timeout
that was set at server startup), a value of{:persistent, timeout}
may be returned. - Returning
{:error, reason, state}
will cause Abyss to close the socket & call thec:handle_error/3
callback to allow final cleanup to be done.
This callback is called when the underlying socket encounters an error; it should perform any cleanup required as it is the last callback called before the process backing this connection is terminated. The underlying socket has already been closed by the time this callback is called. The return value is ignored.
In addition to socket level errors, this callback is also called in cases where handle_connection/2
or handle_data/3
return a {:error, reason, state}
tuple, or when connection handshaking (typically TLS
negotiation) fails.
This callback is called when the server process itself is being shut down; it should perform any cleanup required as it is the last callback called before the process backing this connection is terminated. The underlying socket has NOT been closed by the time this callback is called. The return value is ignored.
This callback is only called when the shutdown reason is :normal
, and is subject to the same caveats described
in GenServer.terminate/2
.
This callback is called when a handler process has gone more than timeout
ms without receiving
either remote data or a local message. The value used for timeout
defaults to the
read_timeout
value specified at server startup, and may be overridden on a one-shot or
persistent basis based on values returned from c:handle_connection/2
or c:handle_data/3
calls. Note that it is NOT called on explicit Abyss.Transport.UDP.recv/3
calls as they have
their own timeout semantics. The underlying socket has NOT been closed by the time this callback
is called. The return value is ignored.