update
This commit is contained in:
286
phoenix/deps/thousand_island/lib/thousand_island.ex
Normal file
286
phoenix/deps/thousand_island/lib/thousand_island.ex
Normal file
@@ -0,0 +1,286 @@
|
||||
defmodule ThousandIsland do
|
||||
@moduledoc """
|
||||
Thousand Island is a modern, pure Elixir socket server, inspired heavily by
|
||||
[ranch](https://github.com/ninenines/ranch). It aims to be easy to understand
|
||||
& reason about, while also being at least as stable and performant as alternatives.
|
||||
|
||||
Thousand Island is implemented as a supervision tree which is intended to be hosted
|
||||
inside a host application, often as a dependency embedded within a higher-level
|
||||
protocol library such as [Bandit](https://github.com/mtrudel/bandit). Aside from
|
||||
supervising the Thousand Island process tree, applications interact with Thousand
|
||||
Island primarily via the `ThousandIsland.Handler` behaviour.
|
||||
|
||||
## Handlers
|
||||
|
||||
The `ThousandIsland.Handler` behaviour defines the interface that Thousand Island
|
||||
uses to pass `ThousandIsland.Socket`s up to the application level; together they
|
||||
form the primary interface that most applications will have with Thousand Island.
|
||||
Thousand Island comes with a few simple protocol handlers to serve as examples;
|
||||
these can be found in the [examples](https://github.com/mtrudel/thousand_island/tree/main/examples)
|
||||
folder of this project. A simple implementation would look like this:
|
||||
|
||||
```elixir
|
||||
defmodule Echo do
|
||||
use ThousandIsland.Handler
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_data(data, socket, state) do
|
||||
ThousandIsland.Socket.send(socket, data)
|
||||
{:continue, state}
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, pid} = ThousandIsland.start_link(port: 1234, handler_module: Echo)
|
||||
```
|
||||
|
||||
For more information, please consult the `ThousandIsland.Handler` documentation.
|
||||
|
||||
## Starting a Thousand Island Server
|
||||
|
||||
A typical use of `ThousandIsland` might look like the following:
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Supervisor do
|
||||
# ... other Supervisor boilerplate
|
||||
|
||||
def init(config) do
|
||||
children = [
|
||||
# ... other children as dictated by your app
|
||||
{ThousandIsland, port: 1234, handler_module: MyApp.ConnectionHandler}
|
||||
]
|
||||
|
||||
Supervisor.init(children, strategy: :one_for_one)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
You can also start servers directly via the `start_link/1` function:
|
||||
|
||||
```elixir
|
||||
{:ok, pid} = ThousandIsland.start_link(port: 1234, handler_module: MyApp.ConnectionHandler)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
A number of options are defined when starting a server. The complete list is
|
||||
defined by the `t:ThousandIsland.options/0` type.
|
||||
|
||||
## Connection Draining & Shutdown
|
||||
|
||||
`ThousandIsland` instances are just a process tree consisting of standard
|
||||
`Supervisor`, `GenServer` and `Task` modules, and so the usual rules regarding
|
||||
shutdown and shutdown timeouts apply. Immediately upon beginning the shutdown
|
||||
sequence the ThousandIsland.ShutdownListener process will cause the listening
|
||||
socket to shut down, which in turn will cause all of the
|
||||
ThousandIsland.Acceptor processes to shut down as well. At this point all that
|
||||
is left in the supervision tree are several layers of Supervisors and whatever
|
||||
`Handler` processes were in progress when shutdown was initiated. At this
|
||||
point, standard `Supervisor` shutdown timeout semantics give existing
|
||||
connections a chance to finish things up. `Handler` processes trap exit, so
|
||||
they continue running beyond shutdown until they either complete or are
|
||||
`:brutal_kill`ed after their shutdown timeout expires.
|
||||
|
||||
## Logging & Telemetry
|
||||
|
||||
As a low-level library, Thousand Island purposely does not do any inline
|
||||
logging of any kind. The `ThousandIsland.Logger` module defines a number of
|
||||
functions to aid in tracing connections at various log levels, and such logging
|
||||
can be dynamically enabled and disabled against an already running server. This
|
||||
logging is backed by telemetry events internally.
|
||||
|
||||
Thousand Island emits a rich set of telemetry events including spans for each
|
||||
server, acceptor process, and individual client connection. These telemetry
|
||||
events are documented in the `ThousandIsland.Telemetry` module.
|
||||
"""
|
||||
|
||||
@typedoc """
|
||||
Possible options to configure a server. Valid option values are as follows:
|
||||
|
||||
* `handler_module`: The name of the module used to handle connections to this server.
|
||||
The module is expected to implement the `ThousandIsland.Handler` behaviour. Required
|
||||
* `handler_options`: A term which is passed as the initial state value to
|
||||
`c:ThousandIsland.Handler.handle_connection/2` calls. Optional, defaulting to nil
|
||||
* `port`: The TCP port number to listen on. If not specified this defaults to 4000.
|
||||
If a port number of `0` is given, the server will dynamically assign a port number
|
||||
which can then be obtained via `ThousandIsland.listener_info/1` or
|
||||
`ThousandIsland.Socket.sockname/1`
|
||||
* `transport_module`: The name of the module which provides basic socket functions.
|
||||
Thousand Island provides `ThousandIsland.Transports.TCP` and `ThousandIsland.Transports.SSL`,
|
||||
which provide clear and TLS encrypted TCP sockets respectively. If not specified this
|
||||
defaults to `ThousandIsland.Transports.TCP`
|
||||
* `transport_options`: A keyword list of options to be passed to the transport module's
|
||||
`c:ThousandIsland.Transport.listen/2` function. Valid values depend on the transport
|
||||
module specified in `transport_module` and can be found in the documentation for the
|
||||
`ThousandIsland.Transports.TCP` and `ThousandIsland.Transports.SSL` modules. Any options
|
||||
in terms of interfaces to listen to / certificates and keys to use for SSL connections
|
||||
will be passed in via this option
|
||||
* `genserver_options`: A term which is passed as the option value to the handler module's
|
||||
underlying `GenServer.start_link/3` call. Optional, defaulting to `[]`
|
||||
* `supervisor_options`: A term which is passed as the option value to this server's top-level
|
||||
supervisor's `Supervisor.start_link/3` call. Useful for setting the `name` for this server.
|
||||
Optional, defaulting to `[]`
|
||||
* `num_acceptors`: The number of acceptor processes to run. Defaults to 100
|
||||
* `num_listen_sockets`: The number of listener sockets to create. When set to a value greater
|
||||
than 1, multiple listener sockets will be created to distribute incoming connections across
|
||||
multiple sockets for improved performance on multi-core systems. This requires setting either
|
||||
`reuseport: true` or `reuseport_lb: true` in the `transport_options`, and will only work on
|
||||
systems that support such socket functionality (most modern Unix-like systems). If the system
|
||||
does not support the required socket options, server startup will fail. This value must be
|
||||
less than or equal to `num_acceptors`. Defaults to 1
|
||||
* `num_connections`: The maximum number of concurrent connections which each acceptor will
|
||||
accept before throttling connections. Connections will be throttled by having the acceptor
|
||||
process wait `max_connections_retry_wait` milliseconds, up to `max_connections_retry_count`
|
||||
times for existing connections to terminate & make room for this new connection. If there is
|
||||
still no room for this new connection after this interval, the acceptor will close the client
|
||||
connection and emit a `[:thousand_island, :acceptor, :spawn_error]` telemetry event. This number
|
||||
is expressed per-acceptor, so the total number of maximum connections for a Thousand Island
|
||||
server is `num_acceptors * num_connections`. Defaults to `16_384`
|
||||
* `max_connections_retry_wait`: How long to wait during each iteration as described in
|
||||
`num_connectors` above, in milliseconds. Defaults to `1000`
|
||||
* `max_connections_retry_count`: How many iterations to wait as described in `num_connectors`
|
||||
above. Defaults to `5`
|
||||
* `read_timeout`: How long to wait for client data before closing the connection, in
|
||||
milliseconds. Defaults to 60_000
|
||||
* `shutdown_timeout`: How long to wait for existing client connections to complete before
|
||||
forcibly shutting those connections down at server shutdown time, in milliseconds. Defaults to
|
||||
15_000. May also be `:infinity` or `:brutal_kill` as described in the `Supervisor`
|
||||
documentation
|
||||
* `silent_terminate_on_error`: Whether to silently ignore errors returned by the handler or to
|
||||
surface them to the runtime via an abnormal termination result. This only applies to errors
|
||||
returned via `{:error, reason, state}` responses; exceptions raised within a handler are always
|
||||
logged regardless of this value. Note also that telemetry events will always be sent for errors
|
||||
regardless of this value. Defaults to false
|
||||
"""
|
||||
@type options :: [
|
||||
handler_module: module(),
|
||||
handler_options: term(),
|
||||
genserver_options: GenServer.options(),
|
||||
supervisor_options: [Supervisor.option()],
|
||||
port: :inet.port_number(),
|
||||
transport_module: module(),
|
||||
transport_options: transport_options(),
|
||||
num_acceptors: pos_integer(),
|
||||
num_listen_sockets: pos_integer(),
|
||||
num_connections: non_neg_integer() | :infinity,
|
||||
max_connections_retry_count: non_neg_integer(),
|
||||
max_connections_retry_wait: timeout(),
|
||||
read_timeout: timeout(),
|
||||
shutdown_timeout: timeout(),
|
||||
silent_terminate_on_error: boolean()
|
||||
]
|
||||
|
||||
@typedoc "A module implementing `ThousandIsland.Transport` behaviour"
|
||||
@type transport_module :: ThousandIsland.Transports.TCP | ThousandIsland.Transports.SSL
|
||||
|
||||
@typedoc "A keyword list of options to be passed to the transport module's `listen/2` function"
|
||||
@type transport_options() ::
|
||||
ThousandIsland.Transports.TCP.options() | ThousandIsland.Transports.SSL.options()
|
||||
|
||||
@doc false
|
||||
@spec child_spec(options()) :: Supervisor.child_spec()
|
||||
def child_spec(opts) do
|
||||
%{
|
||||
id: {__MODULE__, make_ref()},
|
||||
start: {__MODULE__, :start_link, [opts]},
|
||||
type: :supervisor,
|
||||
restart: :permanent
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts a `ThousandIsland` instance with the given options. Returns a pid
|
||||
that can be used to further manipulate the server via other functions defined on
|
||||
this module in the case of success, or an error tuple describing the reason the
|
||||
server was unable to start in the case of failure.
|
||||
"""
|
||||
@spec start_link(options()) :: Supervisor.on_start()
|
||||
def start_link(opts \\ []) do
|
||||
opts
|
||||
|> ThousandIsland.ServerConfig.new()
|
||||
|> ThousandIsland.Server.start_link()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns information about the address and port that the server is listening on
|
||||
"""
|
||||
@spec listener_info(Supervisor.supervisor()) ::
|
||||
{:ok, ThousandIsland.Transport.socket_info()} | :error
|
||||
def listener_info(supervisor) do
|
||||
case ThousandIsland.Server.listener_pid(supervisor) do
|
||||
nil -> :error
|
||||
pid -> {:ok, ThousandIsland.Listener.listener_info(pid)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a list of active connection processes. This is inherently a bit of a leaky notion in the
|
||||
face of concurrency, as there may be connections coming and going during the period that this
|
||||
function takes to run. Callers should account for the possibility that new connections may have
|
||||
been made since / during this call, and that processes returned by this call may have since
|
||||
completed. The order that connection processes are returned in is not specified
|
||||
"""
|
||||
@spec connection_pids(Supervisor.supervisor()) :: {:ok, [pid()]} | :error
|
||||
def connection_pids(supervisor) do
|
||||
case ThousandIsland.Server.acceptor_pool_supervisor_pid(supervisor) do
|
||||
nil -> :error
|
||||
acceptor_pool_pid -> {:ok, collect_connection_pids(acceptor_pool_pid)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Suspend the server. This will close the listening port, and will stop the acceptance of new
|
||||
connections. Existing connections will stay connected and will continue to be processed.
|
||||
|
||||
The server can later be resumed by calling `resume/1`, or shut down via standard supervision
|
||||
patterns.
|
||||
|
||||
If this function returns `:error`, it is unlikely that the server is in a useable state
|
||||
|
||||
Note that if you do not explicitly set a port (or if you set port to `0`), then the server will
|
||||
bind to a different port when you resume it. This new port can be obtained as usual via the
|
||||
`listener_info/1` function. This is not a concern if you explicitly set a port value when first
|
||||
instantiating the server
|
||||
"""
|
||||
defdelegate suspend(supervisor), to: ThousandIsland.Server
|
||||
|
||||
@doc """
|
||||
Resume a suspended server. This will reopen the listening port, and resume the acceptance of new
|
||||
connections
|
||||
"""
|
||||
defdelegate resume(supervisor), to: ThousandIsland.Server
|
||||
|
||||
defp collect_connection_pids(acceptor_pool_pid) do
|
||||
acceptor_pool_pid
|
||||
|> ThousandIsland.AcceptorPoolSupervisor.acceptor_supervisor_pids()
|
||||
|> Enum.reduce([], fn acceptor_sup_pid, acc ->
|
||||
case ThousandIsland.AcceptorSupervisor.connection_sup_pid(acceptor_sup_pid) do
|
||||
nil -> acc
|
||||
connection_sup_pid -> connection_pids(connection_sup_pid, acc)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp connection_pids(connection_sup_pid, acc) do
|
||||
connection_sup_pid
|
||||
|> DynamicSupervisor.which_children()
|
||||
|> Enum.reduce(acc, fn
|
||||
{_, connection_pid, _, _}, acc when is_pid(connection_pid) ->
|
||||
[connection_pid | acc]
|
||||
|
||||
_, acc ->
|
||||
acc
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Synchronously stops the given server, waiting up to the given number of milliseconds
|
||||
for existing connections to finish up. Immediately upon calling this function,
|
||||
the server stops listening for new connections, and then proceeds to wait until
|
||||
either all existing connections have completed or the specified timeout has
|
||||
elapsed.
|
||||
"""
|
||||
@spec stop(Supervisor.supervisor(), timeout()) :: :ok
|
||||
def stop(supervisor, connection_wait \\ 15_000) do
|
||||
Supervisor.stop(supervisor, :normal, connection_wait)
|
||||
end
|
||||
end
|
||||
77
phoenix/deps/thousand_island/lib/thousand_island/acceptor.ex
Normal file
77
phoenix/deps/thousand_island/lib/thousand_island/acceptor.ex
Normal file
@@ -0,0 +1,77 @@
|
||||
defmodule ThousandIsland.Acceptor do
|
||||
@moduledoc false
|
||||
|
||||
use Task, restart: :transient
|
||||
|
||||
@spec start_link(
|
||||
{server :: Supervisor.supervisor(), parent :: Supervisor.supervisor(), pos_integer(),
|
||||
ThousandIsland.ServerConfig.t()}
|
||||
) :: {:ok, pid()}
|
||||
def start_link(arg), do: Task.start_link(__MODULE__, :run, [arg])
|
||||
|
||||
@spec run(
|
||||
{server :: Supervisor.supervisor(), parent :: Supervisor.supervisor(), pos_integer(),
|
||||
ThousandIsland.ServerConfig.t()}
|
||||
) :: no_return
|
||||
def run({server_pid, parent_pid, acceptor_id, %ThousandIsland.ServerConfig{} = server_config}) do
|
||||
ThousandIsland.ProcessLabel.set(:acceptor, server_config, acceptor_id)
|
||||
|
||||
listener_pid = ThousandIsland.Server.listener_pid(server_pid)
|
||||
|
||||
{listener_socket, listener_span} =
|
||||
ThousandIsland.Listener.acceptor_info(listener_pid, acceptor_id)
|
||||
|
||||
connection_sup_pid = ThousandIsland.AcceptorSupervisor.connection_sup_pid(parent_pid)
|
||||
acceptor_span = ThousandIsland.Telemetry.start_child_span(listener_span, :acceptor)
|
||||
|
||||
# Pre-create the handler config once to avoid Map.take on every connection (hot path)
|
||||
handler_config = ThousandIsland.HandlerConfig.from_server_config(server_config)
|
||||
|
||||
accept(listener_socket, connection_sup_pid, server_config, handler_config, acceptor_span, 0)
|
||||
end
|
||||
|
||||
defp accept(listener_socket, connection_sup_pid, server_config, handler_config, span, count) do
|
||||
with {:ok, socket} <- server_config.transport_module.accept(listener_socket),
|
||||
:ok <-
|
||||
ThousandIsland.Connection.start(
|
||||
connection_sup_pid,
|
||||
socket,
|
||||
server_config,
|
||||
handler_config,
|
||||
span
|
||||
) do
|
||||
accept(listener_socket, connection_sup_pid, server_config, handler_config, span, count + 1)
|
||||
else
|
||||
{:error, :too_many_connections} ->
|
||||
ThousandIsland.Telemetry.span_event(span, :spawn_error)
|
||||
|
||||
accept(
|
||||
listener_socket,
|
||||
connection_sup_pid,
|
||||
server_config,
|
||||
handler_config,
|
||||
span,
|
||||
count + 1
|
||||
)
|
||||
|
||||
{:error, reason} when reason in [:econnaborted, :einval] ->
|
||||
ThousandIsland.Telemetry.span_event(span, reason)
|
||||
|
||||
accept(
|
||||
listener_socket,
|
||||
connection_sup_pid,
|
||||
server_config,
|
||||
handler_config,
|
||||
span,
|
||||
count + 1
|
||||
)
|
||||
|
||||
{:error, :closed} ->
|
||||
ThousandIsland.Telemetry.stop_span(span, %{connections: count})
|
||||
|
||||
{:error, reason} ->
|
||||
ThousandIsland.Telemetry.stop_span(span, %{connections: count}, %{error: reason})
|
||||
raise "Unexpected error in accept: #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,54 @@
|
||||
defmodule ThousandIsland.AcceptorPoolSupervisor do
|
||||
@moduledoc false
|
||||
|
||||
use Supervisor
|
||||
|
||||
@spec start_link({server_pid :: pid, ThousandIsland.ServerConfig.t()}) :: Supervisor.on_start()
|
||||
def start_link(arg) do
|
||||
Supervisor.start_link(__MODULE__, arg)
|
||||
end
|
||||
|
||||
@spec acceptor_supervisor_pids(Supervisor.supervisor()) :: [pid()]
|
||||
def acceptor_supervisor_pids(supervisor) do
|
||||
supervisor
|
||||
|> Supervisor.which_children()
|
||||
|> Enum.reduce([], fn
|
||||
{_, acceptor_pid, _, _}, acc when is_pid(acceptor_pid) -> [acceptor_pid | acc]
|
||||
_, acc -> acc
|
||||
end)
|
||||
end
|
||||
|
||||
@spec suspend(Supervisor.supervisor()) :: :ok | :error
|
||||
def suspend(pid) do
|
||||
pid
|
||||
|> acceptor_supervisor_pids()
|
||||
|> Enum.map(&ThousandIsland.AcceptorSupervisor.suspend/1)
|
||||
|> Enum.all?(&(&1 == :ok))
|
||||
|> if(do: :ok, else: :error)
|
||||
end
|
||||
|
||||
@spec resume(Supervisor.supervisor()) :: :ok | :error
|
||||
def resume(pid) do
|
||||
pid
|
||||
|> acceptor_supervisor_pids()
|
||||
|> Enum.map(&ThousandIsland.AcceptorSupervisor.resume/1)
|
||||
|> Enum.all?(&(&1 == :ok))
|
||||
|> if(do: :ok, else: :error)
|
||||
end
|
||||
|
||||
@impl Supervisor
|
||||
@spec init({server_pid :: pid, ThousandIsland.ServerConfig.t()}) ::
|
||||
{:ok,
|
||||
{Supervisor.sup_flags(),
|
||||
[Supervisor.child_spec() | (old_erlang_child_spec :: :supervisor.child_spec())]}}
|
||||
def init({server_pid, %ThousandIsland.ServerConfig{num_acceptors: num_acceptors} = config}) do
|
||||
ThousandIsland.ProcessLabel.set(:acceptor_pool_supervisor, config)
|
||||
|
||||
1..num_acceptors
|
||||
|> Enum.map(fn acceptor_id ->
|
||||
child_spec = {ThousandIsland.AcceptorSupervisor, {server_pid, acceptor_id, config}}
|
||||
Supervisor.child_spec(child_spec, id: "acceptor-#{acceptor_id}")
|
||||
end)
|
||||
|> Supervisor.init(strategy: :one_for_one)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,59 @@
|
||||
defmodule ThousandIsland.AcceptorSupervisor do
|
||||
@moduledoc false
|
||||
|
||||
use Supervisor
|
||||
|
||||
@spec start_link({server_pid :: pid, pos_integer(), ThousandIsland.ServerConfig.t()}) ::
|
||||
Supervisor.on_start()
|
||||
def start_link(arg) do
|
||||
Supervisor.start_link(__MODULE__, arg)
|
||||
end
|
||||
|
||||
@spec connection_sup_pid(Supervisor.supervisor()) :: pid() | nil
|
||||
def connection_sup_pid(supervisor) do
|
||||
supervisor
|
||||
|> Supervisor.which_children()
|
||||
|> Enum.find_value(fn
|
||||
{:connection_sup, connection_sup_pid, _, _} when is_pid(connection_sup_pid) ->
|
||||
connection_sup_pid
|
||||
|
||||
_ ->
|
||||
false
|
||||
end)
|
||||
end
|
||||
|
||||
@spec suspend(Supervisor.supervisor()) :: :ok | :error
|
||||
def suspend(pid) do
|
||||
case Supervisor.terminate_child(pid, :acceptor) do
|
||||
:ok -> :ok
|
||||
{:error, :not_found} -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@spec resume(Supervisor.supervisor()) :: :ok | :error
|
||||
def resume(pid) do
|
||||
case Supervisor.restart_child(pid, :acceptor) do
|
||||
{:ok, _child} -> :ok
|
||||
{:error, reason} when reason in [:running, :restarting] -> :ok
|
||||
{:error, _reason} -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@impl Supervisor
|
||||
@spec init({server_pid :: pid, pos_integer(), ThousandIsland.ServerConfig.t()}) ::
|
||||
{:ok,
|
||||
{Supervisor.sup_flags(),
|
||||
[Supervisor.child_spec() | (old_erlang_child_spec :: :supervisor.child_spec())]}}
|
||||
def init({server_pid, acceptor_id, %ThousandIsland.ServerConfig{} = config}) do
|
||||
ThousandIsland.ProcessLabel.set(:acceptor_supervisor, config)
|
||||
|
||||
children = [
|
||||
{DynamicSupervisor, strategy: :one_for_one, max_children: config.num_connections}
|
||||
|> Supervisor.child_spec(id: :connection_sup),
|
||||
{ThousandIsland.Acceptor, {server_pid, self(), acceptor_id, config}}
|
||||
|> Supervisor.child_spec(id: :acceptor)
|
||||
]
|
||||
|
||||
Supervisor.init(children, strategy: :rest_for_one)
|
||||
end
|
||||
end
|
||||
102
phoenix/deps/thousand_island/lib/thousand_island/connection.ex
Normal file
102
phoenix/deps/thousand_island/lib/thousand_island/connection.ex
Normal file
@@ -0,0 +1,102 @@
|
||||
defmodule ThousandIsland.Connection do
|
||||
@moduledoc false
|
||||
|
||||
@spec start(
|
||||
Supervisor.supervisor(),
|
||||
ThousandIsland.Transport.socket(),
|
||||
ThousandIsland.ServerConfig.t(),
|
||||
ThousandIsland.HandlerConfig.t(),
|
||||
ThousandIsland.Telemetry.t()
|
||||
) ::
|
||||
:ignore
|
||||
| :ok
|
||||
| {:ok, pid, info :: term}
|
||||
| {:error, :too_many_connections | {:already_started, pid} | term}
|
||||
def start(
|
||||
sup_pid,
|
||||
raw_socket,
|
||||
%ThousandIsland.ServerConfig{} = server_config,
|
||||
%ThousandIsland.HandlerConfig{} = handler_config,
|
||||
acceptor_span
|
||||
) do
|
||||
# This is a multi-step process since we need to do a bit of work from within
|
||||
# the process which owns the socket (us, at this point).
|
||||
|
||||
# First, capture the start time for telemetry purposes
|
||||
start_time = ThousandIsland.Telemetry.monotonic_time()
|
||||
|
||||
# Start by defining the worker process which will eventually handle this socket
|
||||
child_spec =
|
||||
{server_config.handler_module,
|
||||
{server_config.handler_options, server_config.genserver_options}}
|
||||
|> Supervisor.child_spec(shutdown: server_config.shutdown_timeout)
|
||||
|
||||
# Then try to create it
|
||||
do_start(
|
||||
sup_pid,
|
||||
child_spec,
|
||||
raw_socket,
|
||||
server_config,
|
||||
handler_config,
|
||||
acceptor_span,
|
||||
start_time,
|
||||
server_config.max_connections_retry_count
|
||||
)
|
||||
end
|
||||
|
||||
defp do_start(
|
||||
sup_pid,
|
||||
child_spec,
|
||||
raw_socket,
|
||||
server_config,
|
||||
handler_config,
|
||||
acceptor_span,
|
||||
start_time,
|
||||
retries
|
||||
) do
|
||||
case DynamicSupervisor.start_child(sup_pid, child_spec) do
|
||||
{:ok, pid} ->
|
||||
# Since this process owns the socket at this point, it needs to be the
|
||||
# one to make this call. connection_pid is sitting and waiting for the
|
||||
# word from us to start processing, in order to ensure that we've made
|
||||
# the following call. Note that we purposefully do not match on the
|
||||
# return from this function; if there's an error the connection process
|
||||
# will see it, but it's no longer our problem if that's the case
|
||||
_ = handler_config.transport_module.controlling_process(raw_socket, pid)
|
||||
|
||||
# Now that we have transferred ownership over to the new process, send a message to the
|
||||
# new process with all the info it needs to start working with the socket (note that the
|
||||
# new process will still need to handshake with the remote end). handler_config was
|
||||
# pre-created by the acceptor to avoid Map.take on every connection.
|
||||
send(pid, {:thousand_island_ready, raw_socket, handler_config, acceptor_span, start_time})
|
||||
|
||||
:ok
|
||||
|
||||
{:error, :max_children} when retries > 0 ->
|
||||
# We're in a tricky spot here; we have a client connection in hand, but no room to put it
|
||||
# into the connection supervisor. We try to wait a maximum number of times to see if any
|
||||
# room opens up before we give up
|
||||
Process.sleep(server_config.max_connections_retry_wait)
|
||||
|
||||
do_start(
|
||||
sup_pid,
|
||||
child_spec,
|
||||
raw_socket,
|
||||
server_config,
|
||||
handler_config,
|
||||
acceptor_span,
|
||||
start_time,
|
||||
retries - 1
|
||||
)
|
||||
|
||||
{:error, :max_children} ->
|
||||
# We gave up trying to find room for this connection in our supervisor.
|
||||
# Close the raw socket here and let the acceptor process handle propagating the error
|
||||
handler_config.transport_module.close(raw_socket)
|
||||
{:error, :too_many_connections}
|
||||
|
||||
other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
end
|
||||
600
phoenix/deps/thousand_island/lib/thousand_island/handler.ex
Normal file
600
phoenix/deps/thousand_island/lib/thousand_island/handler.ex
Normal file
@@ -0,0 +1,600 @@
|
||||
defmodule ThousandIsland.Handler do
|
||||
@moduledoc """
|
||||
`ThousandIsland.Handler` defines the behaviour required of the application layer of a Thousand Island server. When starting a
|
||||
Thousand Island server, you must pass the name of a module implementing this behaviour as the `handler_module` parameter.
|
||||
Thousand Island will then use the specified module to handle each connection that is made to the server.
|
||||
|
||||
The lifecycle of a Handler instance is as follows:
|
||||
|
||||
1. After a client connection to a Thousand Island server is made, Thousand Island will complete the initial setup of the
|
||||
connection (performing a TLS handshake, for example), and then call `c:handle_connection/2`.
|
||||
|
||||
2. A handler implementation may choose to process a client connection within the `c:handle_connection/2` callback by
|
||||
calling functions against the passed `ThousandIsland.Socket`. In many cases, this may be all that may be required of
|
||||
an implementation & the value `{:close, state}` can be returned which will cause Thousand Island to close the connection
|
||||
to the client.
|
||||
|
||||
3. In cases where the server wishes to keep the connection open and wait for subsequent requests from the client on the
|
||||
same socket, it may elect to return `{:continue, state}`. This will cause Thousand Island to wait for client data
|
||||
asynchronously; `c:handle_data/3` will be invoked when the client sends more data.
|
||||
|
||||
4. In the meantime, the process which is hosting connection is idle & able to receive messages sent from elsewhere in your
|
||||
application as needed. The implementation included in the `use ThousandIsland.Handler` macro uses a `GenServer` structure,
|
||||
so you may implement such behaviour via standard `GenServer` patterns. Note that in these cases that state is provided (and
|
||||
must be returned) in a `{socket, state}` format, where the second tuple is the same state value that is passed to the various `handle_*` callbacks
|
||||
defined on this behaviour. It also critical to maintain the socket's `read_timeout` value by
|
||||
ensuring the relevant timeout value is returned as your callback's final argument. Both of these
|
||||
concerns are illustrated in the following example:
|
||||
|
||||
```elixir
|
||||
defmodule ExampleHandler do
|
||||
use ThousandIsland.Handler
|
||||
|
||||
# ...handle_data and other Handler callbacks
|
||||
|
||||
@impl GenServer
|
||||
def handle_call(msg, from, {socket, state}) do
|
||||
# Do whatever you'd like with msg & from
|
||||
{:reply, :ok, {socket, state}, socket.read_timeout}
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_cast(msg, {socket, state}) do
|
||||
# Do whatever you'd like with msg
|
||||
{:noreply, {socket, state}, socket.read_timeout}
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_info(msg, {socket, state}) do
|
||||
# Do whatever you'd like with msg
|
||||
{:noreply, {socket, state}, socket.read_timeout}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
It is fully supported to intermix synchronous `ThousandIsland.Socket.recv` calls with async return values from `c:handle_connection/2`
|
||||
and `c:handle_data/3` callbacks.
|
||||
|
||||
# Example
|
||||
|
||||
A simple example of a Hello World server is as follows:
|
||||
|
||||
```elixir
|
||||
defmodule HelloWorld do
|
||||
use ThousandIsland.Handler
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_connection(socket, state) do
|
||||
ThousandIsland.Socket.send(socket, "Hello, World")
|
||||
{:close, state}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Another example of a server that echoes back all data sent to it is as follows:
|
||||
|
||||
```elixir
|
||||
defmodule Echo do
|
||||
use ThousandIsland.Handler
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_data(data, socket, state) do
|
||||
ThousandIsland.Socket.send(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:
|
||||
|
||||
```elixir
|
||||
defmodule Messenger do
|
||||
use ThousandIsland.Handler
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_data(msg, _socket, state) do
|
||||
IO.puts(msg)
|
||||
{:continue, state}
|
||||
end
|
||||
|
||||
def handle_info({:send, msg}, {socket, state}) do
|
||||
ThousandIsland.Socket.send(socket, msg)
|
||||
{:noreply, {socket, state}, socket.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 `ThousandIsland.start_link/1`
|
||||
containing `t:GenServer.options/0` to be passed to the last argument of `GenServer.start_link/3`.
|
||||
|
||||
Please note that you should not pass the `name` `t: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 `ThousandIsland.Socket.peername/1`.
|
||||
|
||||
For example, using a custom process registry via `Registry`:
|
||||
|
||||
```elixir
|
||||
|
||||
defmodule Messenger do
|
||||
use ThousandIsland.Handler
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_connection(socket, state) do
|
||||
{:ok, {ip, port}} = ThousandIsland.Socket.peername(socket)
|
||||
{:ok, _pid} = Registry.register(MessengerRegistry, {state[:my_key], address}, nil)
|
||||
{:continue, state}
|
||||
end
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_data(data, socket, state) do
|
||||
ThousandIsland.Socket.send(socket, 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 ThousandIsland.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
|
||||
`ThousandIsland.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:
|
||||
|
||||
1. Thousand Island calls `start_link/1` on the configured `handler_module`, passing in a tuple
|
||||
consisting of the configured handler and genserver opts. This function is expected to return a
|
||||
conventional `GenServer.on_start()` style tuple. Note that this newly created process is not
|
||||
passed the connection socket immediately.
|
||||
2. The raw `t:ThousandIsland.Transport.socket()` socket will be passed to the new process via a
|
||||
message of the form `{:thousand_island_ready, raw_socket, handler_config, acceptor_span,
|
||||
start_time}`.
|
||||
3. Your implementation must turn this into a `to:ThousandIsland.Socket.t()` socket by using the
|
||||
`ThousandIsland.Socket.new/3` call.
|
||||
4. Your implementation must then call `ThousandIsland.Socket.handshake/1` with the socket as the
|
||||
sole argument in order to finalize the setup of the socket.
|
||||
5. 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 Thousand Island 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 Thousand Island server instance.
|
||||
|
||||
* Some of the `:connection` family of telemetry span events are emitted by the
|
||||
`ThousandIsland.Handler` implementation. If you use your own implementation in its place it is
|
||||
likely that such spans will not behave as expected.
|
||||
"""
|
||||
|
||||
@typedoc "The possible ways to indicate a timeout when returning values to Thousand Island"
|
||||
@type timeout_options :: timeout() | {:persistent, timeout()}
|
||||
|
||||
@typedoc "The value returned by `c:handle_connection/2` and `c:handle_data/3`"
|
||||
@type handler_result ::
|
||||
{:continue, state :: term()}
|
||||
| {:continue, state :: term(), timeout_options() | {:continue, term()}}
|
||||
| {:switch_transport, {module(), upgrade_opts :: [term()]}, state :: term()}
|
||||
| {:switch_transport, {module(), upgrade_opts :: [term()]}, state :: term(),
|
||||
timeout_options() | {:continue, term()}}
|
||||
| {:close, state :: term()}
|
||||
| {:error, term(), state :: term()}
|
||||
|
||||
@doc """
|
||||
This callback is called shortly after a client connection has been made, immediately after the socket handshake process has
|
||||
completed. It is called with the server's configured `handler_options` value as initial state. Handlers may choose to
|
||||
interact synchronously with the socket in this callback via calls to various `ThousandIsland.Socket` functions.
|
||||
|
||||
The value returned by this callback causes Thousand Island to proceed in one of several ways:
|
||||
|
||||
* Returning `{:close, state}` will cause Thousand Island to close the socket & call the `c:handle_close/2` callback to
|
||||
allow final cleanup to be done.
|
||||
* Returning `{:continue, state}` will cause Thousand Island 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), Thousand Island will call
|
||||
`c: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. If `timeout` milliseconds passes with no data being received or messages
|
||||
being sent to the process, the socket will be closed and `c: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 of `read_timeout` that was set at server startup), a value of
|
||||
`{:persistent, timeout}` may be returned.
|
||||
* Returning `{:continue, state, {:continue, continue}}` is identical to the previous case with the
|
||||
addition of a `c:GenServer.handle_continue/2` callback being made immediately after, in line with
|
||||
similar behaviour on `GenServer` callbacks.
|
||||
* Returning `{:switch_transport, {module, opts}, state}` will cause Thousand Island to try switching the transport of the
|
||||
current socket. The `module` should be an Elixir module that implements the `ThousandIsland.Transport` behaviour.
|
||||
Thousand Island will call `c:ThousandIsland.Transport.upgrade/2` for the given module to upgrade the transport in-place.
|
||||
After a successful upgrade Thousand Island will switch the socket to an asynchronous mode, as if `{:continue, state}`
|
||||
was returned. As with `:continue` return values, there are also timeout-specifying variants of
|
||||
this return value.
|
||||
* Returning `{:error, reason, state}` will cause Thousand Island to close the socket & call the `c:handle_error/3` callback to
|
||||
allow final cleanup to be done.
|
||||
"""
|
||||
@callback handle_connection(socket :: ThousandIsland.Socket.t(), state :: term()) ::
|
||||
handler_result()
|
||||
|
||||
@doc """
|
||||
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 `ThousandIsland.Socket` functions.
|
||||
|
||||
The value returned by this callback causes Thousand Island to proceed in one of several ways:
|
||||
|
||||
* Returning `{:close, state}` will cause Thousand Island to close the socket & call the `c:handle_close/2` callback to
|
||||
allow final cleanup to be done.
|
||||
* Returning `{:continue, state}` will cause Thousand Island 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), Thousand Island will call
|
||||
`c: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. If `timeout` milliseconds passes with no data being received or messages
|
||||
being sent to the process, the socket will be closed and `c: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 of `read_timeout` that was set at server startup), a value of
|
||||
`{:persistent, timeout}` may be returned.
|
||||
* Returning `{:continue, state, {:continue, continue}}` is identical to the previous case with the
|
||||
addition of a `c:GenServer.handle_continue/2` callback being made immediately after, in line with
|
||||
similar behaviour on `GenServer` callbacks.
|
||||
* Returning `{:error, reason, state}` will cause Thousand Island to close the socket & call the `c:handle_error/3` callback to
|
||||
allow final cleanup to be done.
|
||||
"""
|
||||
@callback handle_data(data :: binary(), socket :: ThousandIsland.Socket.t(), state :: term()) ::
|
||||
handler_result()
|
||||
|
||||
@doc """
|
||||
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 `ThousandIsland.Socket.close/1`, however it
|
||||
will be called in cases where `handle_connection/2` or `handle_data/3` return a `{:close, state}` tuple.
|
||||
"""
|
||||
@callback handle_close(socket :: ThousandIsland.Socket.t(), state :: term()) :: term()
|
||||
|
||||
@doc """
|
||||
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.
|
||||
"""
|
||||
@callback handle_error(reason :: any(), socket :: ThousandIsland.Socket.t(), state :: term()) ::
|
||||
term()
|
||||
|
||||
@doc """
|
||||
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 `c:GenServer.terminate/2`.
|
||||
"""
|
||||
@callback handle_shutdown(socket :: ThousandIsland.Socket.t(), state :: term()) :: term()
|
||||
|
||||
@doc """
|
||||
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 `ThousandIsland.Socket.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.
|
||||
"""
|
||||
@callback handle_timeout(socket :: ThousandIsland.Socket.t(), state :: term()) :: term()
|
||||
|
||||
@optional_callbacks handle_connection: 2,
|
||||
handle_data: 3,
|
||||
handle_close: 2,
|
||||
handle_error: 3,
|
||||
handle_shutdown: 2,
|
||||
handle_timeout: 2
|
||||
|
||||
@spec __using__(any) :: Macro.t()
|
||||
defmacro __using__(_opts) do
|
||||
quote location: :keep do
|
||||
@behaviour ThousandIsland.Handler
|
||||
|
||||
use GenServer, restart: :temporary
|
||||
|
||||
@spec start_link({handler_options :: term(), GenServer.options()}) :: GenServer.on_start()
|
||||
def start_link({handler_options, genserver_options}) do
|
||||
GenServer.start_link(__MODULE__, handler_options, genserver_options)
|
||||
end
|
||||
|
||||
unquote(genserver_impl())
|
||||
unquote(handler_impl())
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
defmacro add_handle_info_fallback(_module) do
|
||||
quote do
|
||||
def handle_info({msg, _raw_socket, _data}, _state) when msg in [:tcp, :ssl] do
|
||||
raise """
|
||||
The callback's `state` doesn't match the expected `{socket, state}` form.
|
||||
Please ensure that you are returning a `{socket, state}` tuple from any
|
||||
`GenServer.handle_*` callbacks you have implemented
|
||||
"""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
|
||||
def genserver_impl do
|
||||
quote do
|
||||
@impl true
|
||||
def init(handler_options) do
|
||||
Process.flag(:trap_exit, true)
|
||||
{:ok, {nil, handler_options}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(
|
||||
{:thousand_island_ready, raw_socket, handler_config, acceptor_span, start_time},
|
||||
{nil, state}
|
||||
) do
|
||||
{ip, port} =
|
||||
case handler_config.transport_module.peername(raw_socket) do
|
||||
{:ok, remote_info} ->
|
||||
remote_info
|
||||
|
||||
{:error, reason} ->
|
||||
# the socket has been prematurely closed by the client, we can't do anything with it
|
||||
# so we just close the socket, stop the GenServer with the error reason and move on.
|
||||
_ = handler_config.transport_module.close(raw_socket)
|
||||
throw({:stop, {:shutdown, {:premature_conn_closing, reason}}, {raw_socket, state}})
|
||||
end
|
||||
|
||||
ThousandIsland.ProcessLabel.set(:connection, handler_config, {ip, port})
|
||||
|
||||
span_meta = %{remote_address: ip, remote_port: port}
|
||||
|
||||
connection_span =
|
||||
ThousandIsland.Telemetry.start_child_span(
|
||||
acceptor_span,
|
||||
:connection,
|
||||
%{monotonic_time: start_time},
|
||||
span_meta
|
||||
)
|
||||
|
||||
socket = ThousandIsland.Socket.new(raw_socket, handler_config, connection_span)
|
||||
ThousandIsland.Telemetry.span_event(connection_span, :ready)
|
||||
|
||||
case ThousandIsland.Socket.handshake(socket) do
|
||||
{:ok, socket} -> {:noreply, {socket, state}, {:continue, :handle_connection}}
|
||||
{:error, reason} -> {:stop, {:shutdown, {:handshake, reason}}, {socket, state}}
|
||||
end
|
||||
catch
|
||||
{:stop, _, _} = stop -> stop
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{msg, raw_socket, data},
|
||||
{%ThousandIsland.Socket{socket: raw_socket} = socket, state}
|
||||
)
|
||||
when msg in [:tcp, :ssl] do
|
||||
ThousandIsland.Telemetry.untimed_span_event(socket.span, :async_recv, %{data: data})
|
||||
|
||||
__MODULE__.handle_data(data, socket, state)
|
||||
|> ThousandIsland.Handler.handle_continuation(socket)
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{msg, raw_socket},
|
||||
{%ThousandIsland.Socket{socket: raw_socket} = socket, state}
|
||||
)
|
||||
when msg in [:tcp_closed, :ssl_closed] do
|
||||
{:stop, {:shutdown, :peer_closed}, {socket, state}}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{msg, raw_socket, reason},
|
||||
{%ThousandIsland.Socket{socket: raw_socket} = socket, state}
|
||||
)
|
||||
when msg in [:tcp_error, :ssl_error] do
|
||||
{:stop, reason, {socket, state}}
|
||||
end
|
||||
|
||||
def handle_info(:timeout, {%ThousandIsland.Socket{} = socket, state}) do
|
||||
{:stop, {:shutdown, :timeout}, {socket, state}}
|
||||
end
|
||||
|
||||
@before_compile {ThousandIsland.Handler, :add_handle_info_fallback}
|
||||
|
||||
# Use a continue pattern here so that we have committed the socket
|
||||
# to state in case the `c:handle_connection/2` callback raises an error.
|
||||
# This ensures that the `c:terminate/2` calls below are able to properly
|
||||
# close down the process
|
||||
@impl true
|
||||
def handle_continue(:handle_connection, {%ThousandIsland.Socket{} = socket, state}) do
|
||||
__MODULE__.handle_connection(socket, state)
|
||||
|> ThousandIsland.Handler.handle_continuation(socket)
|
||||
end
|
||||
|
||||
# Called if the remote end closed the connection before we could initialize it
|
||||
@impl true
|
||||
def terminate({:shutdown, {:premature_conn_closing, _reason}}, {_raw_socket, _state}) do
|
||||
:ok
|
||||
end
|
||||
|
||||
# Called by GenServer if we hit our read_timeout. Socket is still open
|
||||
def terminate({:shutdown, :timeout}, {%ThousandIsland.Socket{} = socket, state}) do
|
||||
_ = __MODULE__.handle_timeout(socket, state)
|
||||
ThousandIsland.Handler.do_socket_close(socket, :timeout)
|
||||
end
|
||||
|
||||
# Called if we're being shutdown in an orderly manner. Socket is still open
|
||||
def terminate(:shutdown, {%ThousandIsland.Socket{} = socket, state}) do
|
||||
_ = __MODULE__.handle_shutdown(socket, state)
|
||||
ThousandIsland.Handler.do_socket_close(socket, :shutdown)
|
||||
end
|
||||
|
||||
# Called if the socket encountered an error during handshaking
|
||||
def terminate({:shutdown, {:handshake, reason}}, {%ThousandIsland.Socket{} = socket, state}) do
|
||||
_ = __MODULE__.handle_error(reason, socket, state)
|
||||
ThousandIsland.Handler.do_socket_close(socket, reason)
|
||||
end
|
||||
|
||||
# Called if the socket encountered an error and we are configured to shutdown silently.
|
||||
# Socket is closed
|
||||
def terminate(
|
||||
{:shutdown, {:silent_termination, reason}},
|
||||
{%ThousandIsland.Socket{} = socket, state}
|
||||
) do
|
||||
_ = __MODULE__.handle_error(reason, socket, state)
|
||||
ThousandIsland.Handler.do_socket_close(socket, reason)
|
||||
end
|
||||
|
||||
# Called if the socket encountered an error during upgrading
|
||||
def terminate({:shutdown, {:upgrade, reason}}, {socket, state}) do
|
||||
_ = __MODULE__.handle_error(reason, socket, state)
|
||||
ThousandIsland.Handler.do_socket_close(socket, reason)
|
||||
end
|
||||
|
||||
# Called if the remote end shut down the connection, or if the local end closed the
|
||||
# connection by returning a `{:close,...}` tuple (in which case the socket will be open)
|
||||
def terminate({:shutdown, reason}, {%ThousandIsland.Socket{} = socket, state}) do
|
||||
_ = __MODULE__.handle_close(socket, state)
|
||||
ThousandIsland.Handler.do_socket_close(socket, reason)
|
||||
end
|
||||
|
||||
# Called if the socket encountered an error. Socket is closed
|
||||
def terminate(reason, {%ThousandIsland.Socket{} = socket, state}) do
|
||||
_ = __MODULE__.handle_error(reason, socket, state)
|
||||
ThousandIsland.Handler.do_socket_close(socket, reason)
|
||||
end
|
||||
|
||||
# This clause could happen if we do not have a socket defined in state (either because the
|
||||
# process crashed before setting it up, or because the user sent an invalid state)
|
||||
def terminate(_reason, _state) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handler_impl do
|
||||
quote do
|
||||
@impl true
|
||||
def handle_connection(_socket, state), do: {:continue, state}
|
||||
|
||||
@impl true
|
||||
def handle_data(_data, _socket, state), do: {:continue, state}
|
||||
|
||||
@impl true
|
||||
def handle_close(_socket, _state), do: :ok
|
||||
|
||||
@impl true
|
||||
def handle_error(_error, _socket, _state), do: :ok
|
||||
|
||||
@impl true
|
||||
def handle_shutdown(_socket, _state), do: :ok
|
||||
|
||||
@impl true
|
||||
def handle_timeout(_socket, _state), do: :ok
|
||||
|
||||
defoverridable ThousandIsland.Handler
|
||||
end
|
||||
end
|
||||
|
||||
@spec do_socket_close(
|
||||
ThousandIsland.Socket.t(),
|
||||
reason :: :shutdown | :local_closed | term()
|
||||
) :: :ok
|
||||
@doc false
|
||||
def do_socket_close(socket, reason) do
|
||||
measurements =
|
||||
case ThousandIsland.Socket.getstat(socket) do
|
||||
{:ok, stats} ->
|
||||
%{
|
||||
send_oct: stats[:send_oct],
|
||||
send_cnt: stats[:send_cnt],
|
||||
recv_oct: stats[:recv_oct],
|
||||
recv_cnt: stats[:recv_cnt]
|
||||
}
|
||||
|
||||
_ ->
|
||||
%{}
|
||||
end
|
||||
|
||||
metadata =
|
||||
if reason in [:shutdown, :local_closed, :peer_closed], do: %{}, else: %{error: reason}
|
||||
|
||||
_ = ThousandIsland.Socket.close(socket)
|
||||
ThousandIsland.Telemetry.stop_span(socket.span, measurements, metadata)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_continuation(continuation, socket) do
|
||||
case continuation do
|
||||
{:continue, state} ->
|
||||
_ = ThousandIsland.Socket.setopts(socket, active: :once)
|
||||
{:noreply, {socket, state}, socket.read_timeout}
|
||||
|
||||
{:continue, state, {:continue, continue}} ->
|
||||
_ = ThousandIsland.Socket.setopts(socket, active: :once)
|
||||
{:noreply, {socket, state}, {:continue, continue}}
|
||||
|
||||
{:continue, state, {:persistent, timeout}} ->
|
||||
socket = %{socket | read_timeout: timeout}
|
||||
_ = ThousandIsland.Socket.setopts(socket, active: :once)
|
||||
{:noreply, {socket, state}, timeout}
|
||||
|
||||
{:continue, state, timeout} ->
|
||||
_ = ThousandIsland.Socket.setopts(socket, active: :once)
|
||||
{:noreply, {socket, state}, timeout}
|
||||
|
||||
{:switch_transport, {module, upgrade_opts}, state} ->
|
||||
handle_switch_continuation(socket, module, upgrade_opts, state, socket.read_timeout)
|
||||
|
||||
{:switch_transport, {module, upgrade_opts}, state, {:continue, continue}} ->
|
||||
handle_switch_continuation(socket, module, upgrade_opts, state, {:continue, continue})
|
||||
|
||||
{:switch_transport, {module, upgrade_opts}, state, {:persistent, timeout}} ->
|
||||
socket = %{socket | read_timeout: timeout}
|
||||
handle_switch_continuation(socket, module, upgrade_opts, state, timeout)
|
||||
|
||||
{:switch_transport, {module, upgrade_opts}, state, timeout} ->
|
||||
handle_switch_continuation(socket, module, upgrade_opts, state, timeout)
|
||||
|
||||
{:close, state} ->
|
||||
{:stop, {:shutdown, :local_closed}, {socket, state}}
|
||||
|
||||
{:error, :timeout, state} ->
|
||||
{:stop, {:shutdown, :timeout}, {socket, state}}
|
||||
|
||||
{:error, reason, state} ->
|
||||
if socket.silent_terminate_on_error do
|
||||
{:stop, {:shutdown, {:silent_termination, reason}}, {socket, state}}
|
||||
else
|
||||
{:stop, reason, {socket, state}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_switch_continuation(socket, module, upgrade_opts, state, timeout_or_continue) do
|
||||
case ThousandIsland.Socket.upgrade(socket, module, upgrade_opts) do
|
||||
{:ok, socket} ->
|
||||
_ = ThousandIsland.Socket.setopts(socket, active: :once)
|
||||
{:noreply, {socket, state}, timeout_or_continue}
|
||||
|
||||
{:error, reason} ->
|
||||
{:stop, {:shutdown, {:upgrade, reason}}, {socket, state}}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
defmodule ThousandIsland.HandlerConfig do
|
||||
@moduledoc """
|
||||
A minimal config struct containing only the fields needed by connection handlers.
|
||||
|
||||
This is used internally by `ThousandIsland.Handler`
|
||||
"""
|
||||
|
||||
@enforce_keys [:handler_module, :transport_module, :read_timeout, :silent_terminate_on_error]
|
||||
defstruct @enforce_keys
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
handler_module: nil,
|
||||
transport_module: module(),
|
||||
read_timeout: timeout(),
|
||||
silent_terminate_on_error: boolean()
|
||||
}
|
||||
|
||||
@doc """
|
||||
Creates a HandlerConfig from a ServerConfig, extracting only the fields needed
|
||||
by connection handlers. This should be called once per acceptor at initialization.
|
||||
"""
|
||||
@spec from_server_config(ThousandIsland.ServerConfig.t()) :: t()
|
||||
def from_server_config(%ThousandIsland.ServerConfig{} = config) do
|
||||
%__MODULE__{
|
||||
handler_module: config.handler_module,
|
||||
transport_module: config.transport_module,
|
||||
read_timeout: config.read_timeout,
|
||||
silent_terminate_on_error: config.silent_terminate_on_error
|
||||
}
|
||||
end
|
||||
end
|
||||
109
phoenix/deps/thousand_island/lib/thousand_island/listener.ex
Normal file
109
phoenix/deps/thousand_island/lib/thousand_island/listener.ex
Normal file
@@ -0,0 +1,109 @@
|
||||
defmodule ThousandIsland.Listener do
|
||||
@moduledoc false
|
||||
|
||||
use GenServer, restart: :transient
|
||||
|
||||
@type state :: %{
|
||||
listener_sockets: [{pos_integer(), ThousandIsland.Transport.listener_socket()}],
|
||||
listener_span: ThousandIsland.Telemetry.t(),
|
||||
local_info: ThousandIsland.Transport.socket_info()
|
||||
}
|
||||
|
||||
@spec start_link(ThousandIsland.ServerConfig.t()) :: GenServer.on_start()
|
||||
def start_link(config), do: GenServer.start_link(__MODULE__, config)
|
||||
|
||||
@spec stop(GenServer.server()) :: :ok
|
||||
def stop(server), do: GenServer.stop(server)
|
||||
|
||||
@spec listener_info(GenServer.server()) :: ThousandIsland.Transport.socket_info()
|
||||
def listener_info(server), do: GenServer.call(server, :listener_info)
|
||||
|
||||
@spec acceptor_info(GenServer.server(), pos_integer()) ::
|
||||
{ThousandIsland.Transport.listener_socket(), ThousandIsland.Telemetry.t()}
|
||||
def acceptor_info(server, acceptor_id),
|
||||
do: GenServer.call(server, {:acceptor_info, acceptor_id})
|
||||
|
||||
@impl GenServer
|
||||
@spec init(ThousandIsland.ServerConfig.t()) :: {:ok, state} | {:stop, reason :: term}
|
||||
def init(%ThousandIsland.ServerConfig{} = server_config) do
|
||||
case start_listen_sockets(server_config) do
|
||||
{:ok, listener_sockets, local_info} ->
|
||||
ThousandIsland.ProcessLabel.set(:listener, server_config)
|
||||
|
||||
span_metadata = %{
|
||||
handler: server_config.handler_module,
|
||||
local_address: elem(local_info, 0),
|
||||
local_port: elem(local_info, 1),
|
||||
transport_module: server_config.transport_module,
|
||||
transport_options: server_config.transport_options
|
||||
}
|
||||
|
||||
listener_span = ThousandIsland.Telemetry.start_span(:listener, %{}, span_metadata)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
listener_sockets: listener_sockets,
|
||||
local_info: local_info,
|
||||
listener_span: listener_span
|
||||
}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:stop, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp start_listen_sockets(%ThousandIsland.ServerConfig{} = server_config) do
|
||||
num_sockets = server_config.num_listen_sockets
|
||||
|
||||
sockets =
|
||||
for socket_id <- 1..num_sockets do
|
||||
case server_config.transport_module.listen(
|
||||
server_config.port,
|
||||
server_config.transport_options
|
||||
) do
|
||||
{:ok, socket} -> {socket_id, socket}
|
||||
{:error, reason} -> throw({:error, reason})
|
||||
end
|
||||
end
|
||||
|
||||
# Get local info from first socket
|
||||
{1, first_socket} = List.keyfind(sockets, 1, 0)
|
||||
|
||||
case server_config.transport_module.sockname(first_socket) do
|
||||
{:ok, {ip, port}} ->
|
||||
{:ok, sockets, {ip, port}}
|
||||
|
||||
{:error, reason} ->
|
||||
# Cleanup all sockets on error
|
||||
Enum.each(sockets, fn {_, socket} ->
|
||||
server_config.transport_module.close(socket)
|
||||
end)
|
||||
|
||||
{:error, reason}
|
||||
end
|
||||
catch
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
@spec handle_call(:listener_info | {:acceptor_info, pos_integer()}, any, state) ::
|
||||
{:reply,
|
||||
ThousandIsland.Transport.socket_info()
|
||||
| {ThousandIsland.Transport.listener_socket(), ThousandIsland.Telemetry.t()}, state}
|
||||
def handle_call(:listener_info, _from, state), do: {:reply, state.local_info, state}
|
||||
|
||||
def handle_call({:acceptor_info, acceptor_id}, _from, state) do
|
||||
num_listen_sockets = length(state.listener_sockets)
|
||||
socket_id = rem(acceptor_id - 1, num_listen_sockets) + 1
|
||||
{^socket_id, listener_socket} = List.keyfind(state.listener_sockets, socket_id, 0)
|
||||
{:reply, {listener_socket, state.listener_span}, state}
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
@spec terminate(reason, state) :: :ok
|
||||
when reason: :normal | :shutdown | {:shutdown, term} | term
|
||||
def terminate(_reason, state) do
|
||||
ThousandIsland.Telemetry.stop_span(state.listener_span)
|
||||
end
|
||||
end
|
||||
149
phoenix/deps/thousand_island/lib/thousand_island/logger.ex
Normal file
149
phoenix/deps/thousand_island/lib/thousand_island/logger.ex
Normal file
@@ -0,0 +1,149 @@
|
||||
defmodule ThousandIsland.Logger do
|
||||
@moduledoc """
|
||||
Logging conveniences for Thousand Island servers
|
||||
|
||||
Allows dynamically adding and altering the log level used to trace connections
|
||||
within a Thousand Island server via the use of telemetry hooks. Should you wish
|
||||
to do your own logging or tracking of these events, a complete list of the
|
||||
telemetry events emitted by Thousand Island is described in the module
|
||||
documentation for `ThousandIsland.Telemetry`.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
@typedoc "Supported log levels"
|
||||
@type log_level :: :error | :info | :debug | :trace
|
||||
|
||||
@doc """
|
||||
Start logging Thousand Island at the specified log level. Valid values for log
|
||||
level are `:error`, `:info`, `:debug`, and `:trace`. Enabling a given log
|
||||
level implicitly enables all higher log levels as well.
|
||||
"""
|
||||
@spec attach_logger(log_level()) :: :ok | {:error, :already_exists}
|
||||
def attach_logger(:error) do
|
||||
events = [
|
||||
[:thousand_island, :acceptor, :spawn_error],
|
||||
[:thousand_island, :acceptor, :econnaborted]
|
||||
]
|
||||
|
||||
:telemetry.attach_many("#{__MODULE__}.error", events, &__MODULE__.log_error/4, nil)
|
||||
end
|
||||
|
||||
def attach_logger(:info) do
|
||||
_ = attach_logger(:error)
|
||||
|
||||
events = [
|
||||
[:thousand_island, :listener, :start],
|
||||
[:thousand_island, :listener, :stop]
|
||||
]
|
||||
|
||||
:telemetry.attach_many("#{__MODULE__}.info", events, &__MODULE__.log_info/4, nil)
|
||||
end
|
||||
|
||||
def attach_logger(:debug) do
|
||||
_ = attach_logger(:info)
|
||||
|
||||
events = [
|
||||
[:thousand_island, :acceptor, :start],
|
||||
[:thousand_island, :acceptor, :stop],
|
||||
[:thousand_island, :connection, :start],
|
||||
[:thousand_island, :connection, :stop]
|
||||
]
|
||||
|
||||
:telemetry.attach_many("#{__MODULE__}.debug", events, &__MODULE__.log_debug/4, nil)
|
||||
end
|
||||
|
||||
def attach_logger(:trace) do
|
||||
_ = attach_logger(:debug)
|
||||
|
||||
events = [
|
||||
[:thousand_island, :connection, :ready],
|
||||
[:thousand_island, :connection, :async_recv],
|
||||
[:thousand_island, :connection, :recv],
|
||||
[:thousand_island, :connection, :recv_error],
|
||||
[:thousand_island, :connection, :send],
|
||||
[:thousand_island, :connection, :send_error],
|
||||
[:thousand_island, :connection, :sendfile],
|
||||
[:thousand_island, :connection, :sendfile_error],
|
||||
[:thousand_island, :connection, :socket_shutdown]
|
||||
]
|
||||
|
||||
:telemetry.attach_many("#{__MODULE__}.trace", events, &__MODULE__.log_trace/4, nil)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stop logging Thousand Island at the specified log level. Disabling a given log
|
||||
level implicitly disables all lower log levels as well.
|
||||
"""
|
||||
@spec detach_logger(log_level()) :: :ok | {:error, :not_found}
|
||||
def detach_logger(:error) do
|
||||
_ = detach_logger(:info)
|
||||
:telemetry.detach("#{__MODULE__}.error")
|
||||
end
|
||||
|
||||
def detach_logger(:info) do
|
||||
_ = detach_logger(:debug)
|
||||
:telemetry.detach("#{__MODULE__}.info")
|
||||
end
|
||||
|
||||
def detach_logger(:debug) do
|
||||
_ = detach_logger(:trace)
|
||||
:telemetry.detach("#{__MODULE__}.debug")
|
||||
end
|
||||
|
||||
def detach_logger(:trace) do
|
||||
:telemetry.detach("#{__MODULE__}.trace")
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec log_error(
|
||||
:telemetry.event_name(),
|
||||
:telemetry.event_measurements(),
|
||||
:telemetry.event_metadata(),
|
||||
:telemetry.handler_config()
|
||||
) :: :ok
|
||||
def log_error(event, measurements, metadata, _config) do
|
||||
Logger.error(
|
||||
"#{inspect(event)} metadata: #{inspect(metadata)}, measurements: #{inspect(measurements)}"
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec log_info(
|
||||
:telemetry.event_name(),
|
||||
:telemetry.event_measurements(),
|
||||
:telemetry.event_metadata(),
|
||||
:telemetry.handler_config()
|
||||
) :: :ok
|
||||
def log_info(event, measurements, metadata, _config) do
|
||||
Logger.info(
|
||||
"#{inspect(event)} metadata: #{inspect(metadata)}, measurements: #{inspect(measurements)}"
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec log_debug(
|
||||
:telemetry.event_name(),
|
||||
:telemetry.event_measurements(),
|
||||
:telemetry.event_metadata(),
|
||||
:telemetry.handler_config()
|
||||
) :: :ok
|
||||
def log_debug(event, measurements, metadata, _config) do
|
||||
Logger.debug(
|
||||
"#{inspect(event)} metadata: #{inspect(metadata)}, measurements: #{inspect(measurements)}"
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec log_trace(
|
||||
:telemetry.event_name(),
|
||||
:telemetry.event_measurements(),
|
||||
:telemetry.event_metadata(),
|
||||
:telemetry.handler_config()
|
||||
) :: :ok
|
||||
def log_trace(event, measurements, metadata, _config) do
|
||||
Logger.debug(
|
||||
"#{inspect(event)} metadata: #{inspect(metadata)}, measurements: #{inspect(measurements)}"
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
defmodule ThousandIsland.ProcessLabel do
|
||||
@moduledoc false
|
||||
# Provides compile-time conditional support for Process.set_label/1
|
||||
# which was introduced in Elixir 1.17.0 and OTP 27.
|
||||
@supports_labels Version.match?(System.version(), ">= 1.17.0") and
|
||||
String.to_integer(System.otp_release()) >= 27
|
||||
|
||||
@type config() :: ThousandIsland.ServerConfig.t() | ThousandIsland.HandlerConfig.t()
|
||||
|
||||
if @supports_labels do
|
||||
@doc """
|
||||
Sets a process label if the current Elixir version supports it (>= 1.17).
|
||||
"""
|
||||
@spec set(atom(), config(), term()) ::
|
||||
:ok
|
||||
def set(name, %ThousandIsland.ServerConfig{} = config, state) when is_atom(name) do
|
||||
Process.set_label({:thousand_island, name, {{config.port, config.handler_module}, state}})
|
||||
end
|
||||
|
||||
def set(name, %ThousandIsland.HandlerConfig{} = config, state) when is_atom(name) do
|
||||
Process.set_label({:thousand_island, name, {config.handler_module, state}})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets a process label if the current Elixir version supports it (>= 1.17).
|
||||
"""
|
||||
@spec set(atom(), term()) :: :ok
|
||||
def set(name, %ThousandIsland.ServerConfig{} = config) when is_atom(name) do
|
||||
Process.set_label({:thousand_island, name, {config.port, config.handler_module}})
|
||||
end
|
||||
|
||||
def set(name, state) when is_atom(name) do
|
||||
Process.set_label({:thousand_island, name, state})
|
||||
end
|
||||
else
|
||||
@doc """
|
||||
No-op on Elixir versions < 1.17 that don't support Process.set_label/1.
|
||||
"""
|
||||
@spec set(atom(), config(), term()) :: :ok
|
||||
def set(_, _, _) do
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
No-op on Elixir versions < 1.17 that don't support Process.set_label/1.
|
||||
"""
|
||||
@spec set(atom(), term()) :: :ok
|
||||
def set(_, _) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
88
phoenix/deps/thousand_island/lib/thousand_island/server.ex
Normal file
88
phoenix/deps/thousand_island/lib/thousand_island/server.ex
Normal file
@@ -0,0 +1,88 @@
|
||||
defmodule ThousandIsland.Server do
|
||||
@moduledoc false
|
||||
|
||||
use Supervisor
|
||||
|
||||
@spec start_link(ThousandIsland.ServerConfig.t()) :: Supervisor.on_start()
|
||||
def start_link(%ThousandIsland.ServerConfig{} = config) do
|
||||
Supervisor.start_link(__MODULE__, config, config.supervisor_options)
|
||||
end
|
||||
|
||||
@spec listener_pid(Supervisor.supervisor()) :: pid() | nil
|
||||
def listener_pid(supervisor) do
|
||||
supervisor
|
||||
|> Supervisor.which_children()
|
||||
|> Enum.find_value(fn
|
||||
{:listener, listener_pid, _, _} when is_pid(listener_pid) ->
|
||||
listener_pid
|
||||
|
||||
_ ->
|
||||
false
|
||||
end)
|
||||
end
|
||||
|
||||
@spec acceptor_pool_supervisor_pid(Supervisor.supervisor()) :: pid() | nil
|
||||
def acceptor_pool_supervisor_pid(supervisor) do
|
||||
supervisor
|
||||
|> Supervisor.which_children()
|
||||
|> Enum.find_value(fn
|
||||
{:acceptor_pool_supervisor, acceptor_pool_sup_pid, _, _}
|
||||
when is_pid(acceptor_pool_sup_pid) ->
|
||||
acceptor_pool_sup_pid
|
||||
|
||||
_ ->
|
||||
false
|
||||
end)
|
||||
end
|
||||
|
||||
@spec suspend(Supervisor.supervisor()) :: :ok | :error
|
||||
def suspend(pid) do
|
||||
with pool_sup_pid when is_pid(pool_sup_pid) <- acceptor_pool_supervisor_pid(pid),
|
||||
:ok <- ThousandIsland.AcceptorPoolSupervisor.suspend(pool_sup_pid),
|
||||
:ok <- Supervisor.terminate_child(pid, :shutdown_listener),
|
||||
:ok <- Supervisor.terminate_child(pid, :listener) do
|
||||
:ok
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@spec resume(Supervisor.supervisor()) :: :ok | :error
|
||||
def resume(pid) do
|
||||
with :ok <- wrap_restart_child(pid, :listener),
|
||||
:ok <- wrap_restart_child(pid, :shutdown_listener),
|
||||
pool_sup_pid when is_pid(pool_sup_pid) <- acceptor_pool_supervisor_pid(pid),
|
||||
:ok <- ThousandIsland.AcceptorPoolSupervisor.resume(pool_sup_pid) do
|
||||
:ok
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp wrap_restart_child(pid, id) do
|
||||
case Supervisor.restart_child(pid, id) do
|
||||
{:ok, _child} -> :ok
|
||||
{:error, reason} when reason in [:running, :restarting] -> :ok
|
||||
{:error, _reason} -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@impl Supervisor
|
||||
@spec init(ThousandIsland.ServerConfig.t()) ::
|
||||
{:ok,
|
||||
{Supervisor.sup_flags(),
|
||||
[Supervisor.child_spec() | (old_erlang_child_spec :: :supervisor.child_spec())]}}
|
||||
def init(config) do
|
||||
ThousandIsland.ProcessLabel.set(:server, config)
|
||||
|
||||
children = [
|
||||
{ThousandIsland.Listener, config} |> Supervisor.child_spec(id: :listener),
|
||||
{ThousandIsland.AcceptorPoolSupervisor, {self(), config}}
|
||||
|> Supervisor.child_spec(id: :acceptor_pool_supervisor),
|
||||
{ThousandIsland.ShutdownListener, {self(), config}}
|
||||
|> Supervisor.child_spec(id: :shutdown_listener)
|
||||
]
|
||||
|
||||
Supervisor.init(children, strategy: :rest_for_one)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,77 @@
|
||||
defmodule ThousandIsland.ServerConfig do
|
||||
@moduledoc """
|
||||
Encapsulates the configuration of a ThousandIsland server instance
|
||||
|
||||
This is used internally by `ThousandIsland.Handler`
|
||||
"""
|
||||
|
||||
@typedoc "A set of configuration parameters for a ThousandIsland server instance"
|
||||
@type t :: %__MODULE__{
|
||||
port: :inet.port_number(),
|
||||
transport_module: ThousandIsland.transport_module(),
|
||||
transport_options: ThousandIsland.transport_options(),
|
||||
handler_module: module(),
|
||||
handler_options: term(),
|
||||
genserver_options: GenServer.options(),
|
||||
supervisor_options: [Supervisor.option()],
|
||||
num_acceptors: pos_integer(),
|
||||
num_listen_sockets: pos_integer(),
|
||||
num_connections: non_neg_integer() | :infinity,
|
||||
max_connections_retry_count: non_neg_integer(),
|
||||
max_connections_retry_wait: timeout(),
|
||||
read_timeout: timeout(),
|
||||
shutdown_timeout: timeout(),
|
||||
silent_terminate_on_error: boolean()
|
||||
}
|
||||
|
||||
defstruct port: 4000,
|
||||
transport_module: ThousandIsland.Transports.TCP,
|
||||
transport_options: [],
|
||||
handler_module: nil,
|
||||
handler_options: [],
|
||||
genserver_options: [],
|
||||
supervisor_options: [],
|
||||
num_acceptors: 100,
|
||||
num_listen_sockets: 1,
|
||||
num_connections: 16_384,
|
||||
max_connections_retry_count: 5,
|
||||
max_connections_retry_wait: 1000,
|
||||
read_timeout: 60_000,
|
||||
shutdown_timeout: 15_000,
|
||||
silent_terminate_on_error: false
|
||||
|
||||
@spec new(ThousandIsland.options()) :: t()
|
||||
def new(opts \\ []) do
|
||||
config = struct!(__MODULE__, opts)
|
||||
validate_handler_module!(config)
|
||||
validate_num_sockets!(config)
|
||||
validate_reuseport_options!(config)
|
||||
config
|
||||
end
|
||||
|
||||
defp validate_handler_module!(config) do
|
||||
if !config.handler_module do
|
||||
raise("No handler_module defined in server configuration")
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_num_sockets!(config) do
|
||||
if config.num_listen_sockets > config.num_acceptors do
|
||||
raise(
|
||||
"num_listen_sockets (#{config.num_listen_sockets}) must be less than or equal to num_acceptors (#{config.num_acceptors})"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_reuseport_options!(config) do
|
||||
num_listen_sockets = config.num_listen_sockets
|
||||
transport_options = config.transport_options
|
||||
has_reuseport = :proplists.get_value(:reuseport, transport_options, false)
|
||||
has_reuseport_lb = :proplists.get_value(:reuseport_lb, transport_options, false)
|
||||
|
||||
unless num_listen_sockets <= 1 or has_reuseport or has_reuseport_lb do
|
||||
raise ArgumentError,
|
||||
"reuseport: true or reuseport_lb: true must be set in transport_options when using num_listen_sockets > 1"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
defmodule ThousandIsland.ShutdownListener do
|
||||
@moduledoc false
|
||||
|
||||
# Used as part of the `ThousandIsland.Server` supervision tree to facilitate
|
||||
# stopping the server's listener process early in the shutdown process, in order
|
||||
# to allow existing connections to drain without accepting new ones
|
||||
|
||||
use GenServer
|
||||
|
||||
@type state :: %{
|
||||
optional(:server_pid) => pid(),
|
||||
optional(:listener_pid) => pid() | nil
|
||||
}
|
||||
|
||||
@doc false
|
||||
@spec start_link({pid(), any()}) :: :ignore | {:error, any} | {:ok, pid}
|
||||
def start_link({server_pid, key}) do
|
||||
GenServer.start_link(__MODULE__, {server_pid, key})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl GenServer
|
||||
@spec init({pid(), any()}) :: {:ok, state, {:continue, :setup_listener_pid}}
|
||||
def init({server_pid, key}) do
|
||||
Process.flag(:trap_exit, true)
|
||||
ThousandIsland.ProcessLabel.set(:shutdown_listener, key)
|
||||
{:ok, %{server_pid: server_pid}, {:continue, :setup_listener_pid}}
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl GenServer
|
||||
@spec handle_continue(:setup_listener_pid, state) :: {:noreply, state}
|
||||
def handle_continue(:setup_listener_pid, %{server_pid: server_pid}) do
|
||||
listener_pid = ThousandIsland.Server.listener_pid(server_pid)
|
||||
{:noreply, %{listener_pid: listener_pid}}
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl GenServer
|
||||
@spec terminate(reason, state) :: :ok
|
||||
when reason: :normal | :shutdown | {:shutdown, term} | term
|
||||
def terminate(_reason, %{listener_pid: listener_pid}) do
|
||||
ThousandIsland.Listener.stop(listener_pid)
|
||||
end
|
||||
|
||||
def terminate(_reason, _state), do: :ok
|
||||
end
|
||||
236
phoenix/deps/thousand_island/lib/thousand_island/socket.ex
Normal file
236
phoenix/deps/thousand_island/lib/thousand_island/socket.ex
Normal file
@@ -0,0 +1,236 @@
|
||||
defmodule ThousandIsland.Socket do
|
||||
@moduledoc """
|
||||
Encapsulates a client connection's underlying socket, providing a facility to
|
||||
read, write, and otherwise manipulate a connection from a client.
|
||||
"""
|
||||
|
||||
@enforce_keys [:socket, :transport_module, :read_timeout, :silent_terminate_on_error, :span]
|
||||
defstruct @enforce_keys
|
||||
|
||||
@typedoc "A reference to a socket along with metadata describing how to use it"
|
||||
@type t :: %__MODULE__{
|
||||
socket: ThousandIsland.Transport.socket(),
|
||||
transport_module: module(),
|
||||
read_timeout: timeout(),
|
||||
silent_terminate_on_error: boolean(),
|
||||
span: ThousandIsland.Telemetry.t()
|
||||
}
|
||||
|
||||
@doc """
|
||||
Creates a new socket struct based on the passed parameters.
|
||||
|
||||
This is normally called internally by `ThousandIsland.Handler` and does not need to be
|
||||
called by implementations which are based on `ThousandIsland.Handler`
|
||||
"""
|
||||
@spec new(
|
||||
ThousandIsland.Transport.socket(),
|
||||
ThousandIsland.HandlerConfig.t(),
|
||||
ThousandIsland.Telemetry.t()
|
||||
) :: t()
|
||||
def new(raw_socket, handler_config, span) do
|
||||
%__MODULE__{
|
||||
socket: raw_socket,
|
||||
transport_module: handler_config.transport_module,
|
||||
read_timeout: handler_config.read_timeout,
|
||||
silent_terminate_on_error: handler_config.silent_terminate_on_error,
|
||||
span: span
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handshakes the underlying socket if it is required (as in the case of SSL sockets, for example).
|
||||
|
||||
This is normally called internally by `ThousandIsland.Handler` and does not need to be
|
||||
called by implementations which are based on `ThousandIsland.Handler`
|
||||
"""
|
||||
@spec handshake(t()) :: ThousandIsland.Transport.on_handshake()
|
||||
def handshake(%__MODULE__{} = socket) do
|
||||
case socket.transport_module.handshake(socket.socket) do
|
||||
{:ok, inner_socket} ->
|
||||
{:ok, %{socket | socket: inner_socket}}
|
||||
|
||||
{:error, reason} = err ->
|
||||
ThousandIsland.Telemetry.stop_span(socket.span, %{}, %{error: reason})
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Upgrades the transport of the socket to use the specified transport module, performing any client
|
||||
handshaking that may be required. The passed options are blindly passed through to the new
|
||||
transport module.
|
||||
|
||||
This is normally called internally by `ThousandIsland.Handler` and does not need to be
|
||||
called by implementations which are based on `ThousandIsland.Handler`
|
||||
"""
|
||||
@spec upgrade(t(), module(), term()) :: ThousandIsland.Transport.on_upgrade()
|
||||
def upgrade(%__MODULE__{} = socket, module, opts) when is_atom(module) do
|
||||
case module.upgrade(socket.socket, opts) do
|
||||
{:ok, updated_socket} ->
|
||||
{:ok, %{socket | socket: updated_socket, transport_module: module}}
|
||||
|
||||
{:error, reason} = err ->
|
||||
ThousandIsland.Telemetry.stop_span(socket.span, %{}, %{error: reason})
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns available bytes on the given socket. Up to `length` bytes will be
|
||||
returned (0 can be passed in to get the next 'available' bytes, typically the
|
||||
next packet). If insufficient bytes are available, the function can wait `timeout`
|
||||
milliseconds for data to arrive.
|
||||
"""
|
||||
@spec recv(t(), non_neg_integer(), timeout() | nil) :: ThousandIsland.Transport.on_recv()
|
||||
def recv(%__MODULE__{} = socket, length \\ 0, timeout \\ nil) do
|
||||
case socket.transport_module.recv(socket.socket, length, timeout || socket.read_timeout) do
|
||||
{:ok, data} = ok ->
|
||||
ThousandIsland.Telemetry.untimed_span_event(socket.span, :recv, %{data: data})
|
||||
ok
|
||||
|
||||
{:error, reason} = err ->
|
||||
ThousandIsland.Telemetry.span_event(socket.span, :recv_error, %{error: reason})
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends the given data (specified as a binary or an IO list) on the given socket.
|
||||
"""
|
||||
@spec send(t(), iodata()) :: ThousandIsland.Transport.on_send()
|
||||
def send(%__MODULE__{} = socket, data) do
|
||||
case socket.transport_module.send(socket.socket, data) do
|
||||
:ok ->
|
||||
ThousandIsland.Telemetry.untimed_span_event(socket.span, :send, %{data: data})
|
||||
:ok
|
||||
|
||||
{:error, reason} = err ->
|
||||
ThousandIsland.Telemetry.span_event(socket.span, :send_error, %{data: data, error: reason})
|
||||
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends the contents of the given file based on the provided offset & length
|
||||
"""
|
||||
@spec sendfile(t(), String.t(), non_neg_integer(), non_neg_integer()) ::
|
||||
ThousandIsland.Transport.on_sendfile()
|
||||
def sendfile(%__MODULE__{} = socket, filename, offset, length) do
|
||||
case socket.transport_module.sendfile(socket.socket, filename, offset, length) do
|
||||
{:ok, bytes_written} = ok ->
|
||||
measurements = %{filename: filename, offset: offset, bytes_written: bytes_written}
|
||||
ThousandIsland.Telemetry.untimed_span_event(socket.span, :sendfile, measurements)
|
||||
ok
|
||||
|
||||
{:error, reason} = err ->
|
||||
measurements = %{filename: filename, offset: offset, length: length, error: reason}
|
||||
ThousandIsland.Telemetry.span_event(socket.span, :sendfile_error, measurements)
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Shuts down the socket in the given direction.
|
||||
"""
|
||||
@spec shutdown(t(), ThousandIsland.Transport.way()) :: ThousandIsland.Transport.on_shutdown()
|
||||
def shutdown(%__MODULE__{} = socket, way) do
|
||||
ThousandIsland.Telemetry.span_event(socket.span, :socket_shutdown, %{way: way})
|
||||
socket.transport_module.shutdown(socket.socket, way)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Closes the given socket. Note that a socket is automatically closed when the handler
|
||||
process which owns it terminates
|
||||
"""
|
||||
@spec close(t()) :: ThousandIsland.Transport.on_close()
|
||||
def close(%__MODULE__{} = socket) do
|
||||
socket.transport_module.close(socket.socket)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the given flags on the socket
|
||||
|
||||
Errors are usually from :inet.posix(), however, SSL module defines return type as any()
|
||||
"""
|
||||
@spec getopts(t(), ThousandIsland.Transport.socket_get_options()) ::
|
||||
ThousandIsland.Transport.on_getopts()
|
||||
def getopts(%__MODULE__{} = socket, options) do
|
||||
socket.transport_module.getopts(socket.socket, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets the given flags on the socket
|
||||
|
||||
Errors are usually from :inet.posix(), however, SSL module defines return type as any()
|
||||
"""
|
||||
@spec setopts(t(), ThousandIsland.Transport.socket_set_options()) ::
|
||||
ThousandIsland.Transport.on_setopts()
|
||||
def setopts(%__MODULE__{} = socket, options) do
|
||||
socket.transport_module.setopts(socket.socket, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns information in the form of `t:ThousandIsland.Transport.socket_info()` about the local end of the socket.
|
||||
"""
|
||||
@spec sockname(t()) :: ThousandIsland.Transport.on_sockname()
|
||||
def sockname(%__MODULE__{} = socket) do
|
||||
socket.transport_module.sockname(socket.socket)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns information in the form of `t:ThousandIsland.Transport.socket_info()` about the remote end of the socket.
|
||||
"""
|
||||
@spec peername(t()) :: ThousandIsland.Transport.on_peername()
|
||||
def peername(%__MODULE__{} = socket) do
|
||||
socket.transport_module.peername(socket.socket)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns information in the form of `t:public_key.der_encoded()` about the peer certificate of the socket.
|
||||
"""
|
||||
@spec peercert(t()) :: ThousandIsland.Transport.on_peercert()
|
||||
def peercert(%__MODULE__{} = socket) do
|
||||
socket.transport_module.peercert(socket.socket)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns whether or not this protocol is secure.
|
||||
"""
|
||||
@spec secure?(t()) :: boolean()
|
||||
def secure?(%__MODULE__{} = socket) do
|
||||
socket.transport_module.secure?()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns statistics about the connection.
|
||||
"""
|
||||
@spec getstat(t()) :: ThousandIsland.Transport.socket_stats()
|
||||
def getstat(%__MODULE__{} = socket) do
|
||||
socket.transport_module.getstat(socket.socket)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns information about the protocol negotiated during transport handshaking (if any).
|
||||
"""
|
||||
@spec negotiated_protocol(t()) :: ThousandIsland.Transport.on_negotiated_protocol()
|
||||
def negotiated_protocol(%__MODULE__{} = socket) do
|
||||
socket.transport_module.negotiated_protocol(socket.socket)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns information about the SSL connection info, if transport is SSL.
|
||||
"""
|
||||
@spec connection_information(t()) :: ThousandIsland.Transport.on_connection_information()
|
||||
def connection_information(%__MODULE__{} = socket) do
|
||||
socket.transport_module.connection_information(socket.socket)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the telemetry span representing the lifetime of this socket
|
||||
"""
|
||||
@spec telemetry_span(t()) :: ThousandIsland.Telemetry.t()
|
||||
def telemetry_span(%__MODULE__{} = socket) do
|
||||
socket.span
|
||||
end
|
||||
end
|
||||
400
phoenix/deps/thousand_island/lib/thousand_island/telemetry.ex
Normal file
400
phoenix/deps/thousand_island/lib/thousand_island/telemetry.ex
Normal file
@@ -0,0 +1,400 @@
|
||||
defmodule ThousandIsland.Telemetry do
|
||||
@moduledoc """
|
||||
The following telemetry spans are emitted by thousand_island
|
||||
|
||||
## `[:thousand_island, :listener, *]`
|
||||
|
||||
Represents a Thousand Island server listening to a port
|
||||
|
||||
This span is started by the following event:
|
||||
|
||||
* `[:thousand_island, :listener, :start]`
|
||||
|
||||
Represents the start of the span
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `monotonic_time`: The time of this event, in `:native` units
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
* `local_address`: The IP address that the listener is bound to
|
||||
* `local_port`: The port that the listener is bound to
|
||||
* `transport_module`: The transport module in use
|
||||
* `transport_options`: Options passed to the transport module at startup
|
||||
|
||||
|
||||
This span is ended by the following event:
|
||||
|
||||
* `[:thousand_island, :listener, :stop]`
|
||||
|
||||
Represents the end of the span
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `monotonic_time`: The time of this event, in `:native` units
|
||||
* `duration`: The span duration, in `:native` units
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
* `local_address`: The IP address that the listener is bound to
|
||||
* `local_port`: The port that the listener is bound to
|
||||
* `transport_module`: The transport module in use
|
||||
* `transport_options`: Options passed to the transport module at startup
|
||||
|
||||
## `[:thousand_island, :acceptor, *]`
|
||||
|
||||
Represents a Thousand Island acceptor process listening for connections
|
||||
|
||||
This span is started by the following event:
|
||||
|
||||
* `[:thousand_island, :acceptor, :start]`
|
||||
|
||||
Represents the start of the span
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `monotonic_time`: The time of this event, in `:native` units
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
* `parent_telemetry_span_context`: The span context of the `:listener` which created this acceptor
|
||||
|
||||
This span is ended by the following event:
|
||||
|
||||
* `[:thousand_island, :acceptor, :stop]`
|
||||
|
||||
Represents the end of the span
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `monotonic_time`: The time of this event, in `:native` units
|
||||
* `duration`: The span duration, in `:native` units
|
||||
* `connections`: The number of client requests that the acceptor handled
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
* `parent_telemetry_span_context`: The span context of the `:listener` which created this acceptor
|
||||
* `error`: The error that caused the span to end, if it ended in error
|
||||
|
||||
The following events may be emitted within this span:
|
||||
|
||||
* `[:thousand_island, :acceptor, :spawn_error]`
|
||||
|
||||
Thousand Island was unable to spawn a process to handle a connection. This occurs when too
|
||||
many connections are in progress; you may want to look at increasing the `num_connections`
|
||||
configuration parameter
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `monotonic_time`: The time of this event, in `:native` units
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
|
||||
* `[:thousand_island, :acceptor, :econnaborted]`
|
||||
|
||||
Thousand Island was unable to spawn a process to handle a connection since the remote end
|
||||
closed before we could accept it. This usually occurs when it takes too long for your server
|
||||
to start processing a connection; you may want to look at tuning OS-level TCP parameters or
|
||||
adding more server capacity.
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `monotonic_time`: The time of this event, in `:native` units
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
|
||||
## `[:thousand_island, :connection, *]`
|
||||
|
||||
Represents Thousand Island handling a specific client request
|
||||
|
||||
This span is started by the following event:
|
||||
|
||||
* `[:thousand_island, :connection, :start]`
|
||||
|
||||
Represents the start of the span
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `monotonic_time`: The time of this event, in `:native` units
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
* `parent_telemetry_span_context`: The span context of the `:acceptor` span which accepted
|
||||
this connection
|
||||
* `remote_address`: The IP address of the connected client
|
||||
* `remote_port`: The port of the connected client
|
||||
|
||||
This span is ended by the following event:
|
||||
|
||||
* `[:thousand_island, :connection, :stop]`
|
||||
|
||||
Represents the end of the span
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `monotonic_time`: The time of this event, in `:native` units
|
||||
* `duration`: The span duration, in `:native` units
|
||||
* `send_oct`: The number of octets sent on the connection
|
||||
* `send_cnt`: The number of packets sent on the connection
|
||||
* `recv_oct`: The number of octets received on the connection
|
||||
* `recv_cnt`: The number of packets received on the connection
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
* `parent_telemetry_span_context`: The span context of the `:acceptor` span which accepted
|
||||
this connection
|
||||
* `remote_address`: The IP address of the connected client
|
||||
* `remote_port`: The port of the connected client
|
||||
* `error`: The error that caused the span to end, if it ended in error
|
||||
|
||||
The following events may be emitted within this span:
|
||||
|
||||
* `[:thousand_island, :connection, :ready]`
|
||||
|
||||
Thousand Island has completed setting up the client connection
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `monotonic_time`: The time of this event, in `:native` units
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
|
||||
* `[:thousand_island, :connection, :async_recv]`
|
||||
|
||||
Thousand Island has asynchronously received data from the client
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `data`: The data received from the client
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
|
||||
* `[:thousand_island, :connection, :recv]`
|
||||
|
||||
Thousand Island has synchronously received data from the client
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `data`: The data received from the client
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
|
||||
* `[:thousand_island, :connection, :recv_error]`
|
||||
|
||||
Thousand Island encountered an error reading data from the client
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `error`: A description of the error
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
|
||||
* `[:thousand_island, :connection, :send]`
|
||||
|
||||
Thousand Island has sent data to the client
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `data`: The data sent to the client
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
|
||||
* `[:thousand_island, :connection, :send_error]`
|
||||
|
||||
Thousand Island encountered an error sending data to the client
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `data`: The data that was being sent to the client
|
||||
* `error`: A description of the error
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
|
||||
* `[:thousand_island, :connection, :sendfile]`
|
||||
|
||||
Thousand Island has sent a file to the client
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `filename`: The filename containing data sent to the client
|
||||
* `offset`: The offset (in bytes) within the file sending started from
|
||||
* `bytes_written`: The number of bytes written
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
|
||||
* `[:thousand_island, :connection, :sendfile_error]`
|
||||
|
||||
Thousand Island encountered an error sending a file to the client
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `filename`: The filename containing data that was being sent to the client
|
||||
* `offset`: The offset (in bytes) within the file where sending started from
|
||||
* `length`: The number of bytes that were attempted to send
|
||||
* `error`: A description of the error
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
|
||||
* `[:thousand_island, :connection, :socket_shutdown]`
|
||||
|
||||
Thousand Island has shutdown the client connection
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `monotonic_time`: The time of this event, in `:native` units
|
||||
* `way`: The direction in which the socket was shut down
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
"""
|
||||
|
||||
@enforce_keys [
|
||||
:span_name,
|
||||
:telemetry_span_context,
|
||||
:start_time,
|
||||
:start_metadata,
|
||||
:handler,
|
||||
:span_metadata
|
||||
]
|
||||
defstruct @enforce_keys
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
span_name: span_name(),
|
||||
telemetry_span_context: reference(),
|
||||
start_time: integer(),
|
||||
start_metadata: metadata(),
|
||||
handler: module(),
|
||||
span_metadata: metadata()
|
||||
}
|
||||
|
||||
@type span_name :: :listener | :acceptor | :connection
|
||||
@type metadata :: :telemetry.event_metadata()
|
||||
|
||||
@typedoc false
|
||||
@type measurements :: :telemetry.event_measurements()
|
||||
|
||||
@typedoc false
|
||||
@type event_name ::
|
||||
:ready
|
||||
| :spawn_error
|
||||
| :econnaborted
|
||||
| :recv_error
|
||||
| :send_error
|
||||
| :sendfile_error
|
||||
| :socket_shutdown
|
||||
|
||||
@typedoc false
|
||||
@type untimed_event_name ::
|
||||
:async_recv
|
||||
| :stop
|
||||
| :recv
|
||||
| :send
|
||||
| :sendfile
|
||||
|
||||
@app_name :thousand_island
|
||||
|
||||
@doc false
|
||||
@spec start_span(span_name(), measurements(), metadata()) :: t()
|
||||
def start_span(span_name, measurements, metadata) do
|
||||
measurements = Map.put_new_lazy(measurements, :monotonic_time, &monotonic_time/0)
|
||||
telemetry_span_context = make_ref()
|
||||
metadata = Map.put(metadata, :telemetry_span_context, telemetry_span_context)
|
||||
_ = event([span_name, :start], measurements, metadata)
|
||||
|
||||
handler = metadata.handler
|
||||
|
||||
# Pre-build the metadata that will be used for all events in this span
|
||||
span_metadata = %{
|
||||
telemetry_span_context: telemetry_span_context,
|
||||
handler: handler
|
||||
}
|
||||
|
||||
%__MODULE__{
|
||||
span_name: span_name,
|
||||
telemetry_span_context: telemetry_span_context,
|
||||
start_time: measurements[:monotonic_time],
|
||||
start_metadata: metadata,
|
||||
handler: handler,
|
||||
span_metadata: span_metadata
|
||||
}
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec start_child_span(t(), span_name(), measurements(), metadata()) :: t()
|
||||
def start_child_span(parent_span, span_name, measurements \\ %{}, metadata \\ %{}) do
|
||||
metadata =
|
||||
Map.merge(metadata, %{
|
||||
parent_telemetry_span_context: parent_span.telemetry_span_context,
|
||||
handler: parent_span.handler
|
||||
})
|
||||
|
||||
start_span(span_name, measurements, metadata)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec stop_span(t(), measurements(), metadata()) :: :ok
|
||||
def stop_span(span, measurements \\ %{}, metadata \\ %{}) do
|
||||
monotonic_time = measurements[:monotonic_time] || monotonic_time()
|
||||
|
||||
measurements =
|
||||
Map.merge(measurements, %{
|
||||
monotonic_time: monotonic_time,
|
||||
duration: monotonic_time - span.start_time
|
||||
})
|
||||
|
||||
metadata = Map.merge(span.start_metadata, metadata)
|
||||
|
||||
untimed_span_event(span, :stop, measurements, metadata)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec span_event(t(), event_name(), measurements(), metadata()) :: :ok
|
||||
def span_event(span, name, measurements \\ %{}, metadata \\ %{}) do
|
||||
measurements = Map.put_new_lazy(measurements, :monotonic_time, &monotonic_time/0)
|
||||
untimed_span_event(span, name, measurements, metadata)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec untimed_span_event(t(), event_name() | untimed_event_name(), measurements(), metadata()) ::
|
||||
:ok
|
||||
def untimed_span_event(span, name, measurements \\ %{}, metadata \\ %{}) do
|
||||
metadata = Map.merge(metadata, span.span_metadata)
|
||||
|
||||
event([span.span_name, name], measurements, metadata)
|
||||
end
|
||||
|
||||
@spec monotonic_time() :: integer
|
||||
defdelegate monotonic_time, to: System
|
||||
|
||||
defp event(suffix, measurements, metadata) do
|
||||
:telemetry.execute([@app_name | suffix], measurements, metadata)
|
||||
end
|
||||
end
|
||||
220
phoenix/deps/thousand_island/lib/thousand_island/transport.ex
Normal file
220
phoenix/deps/thousand_island/lib/thousand_island/transport.ex
Normal file
@@ -0,0 +1,220 @@
|
||||
defmodule ThousandIsland.Transport do
|
||||
@moduledoc """
|
||||
This module describes the behaviour required for Thousand Island to interact
|
||||
with low-level sockets. It is largely internal to Thousand Island, however users
|
||||
are free to implement their own versions of this behaviour backed by whatever
|
||||
underlying transport they choose. Such a module can be used in Thousand Island
|
||||
by passing its name as the `transport_module` option when starting up a server,
|
||||
as described in `ThousandIsland`.
|
||||
"""
|
||||
|
||||
@typedoc "A listener socket used to wait for connections"
|
||||
@type listener_socket() :: :inet.socket() | :ssl.sslsocket()
|
||||
|
||||
@typedoc "A listener socket options"
|
||||
@type listen_options() ::
|
||||
[:inet.inet_backend() | :gen_tcp.listen_option()] | [:ssl.tls_server_option()]
|
||||
|
||||
@typedoc "A socket representing a client connection"
|
||||
@type socket() :: :inet.socket() | :ssl.sslsocket()
|
||||
|
||||
@typedoc "Information about an endpoint, either remote ('peer') or local"
|
||||
@type socket_info() ::
|
||||
{:inet.ip_address(), :inet.port_number()} | :inet.returned_non_ip_address()
|
||||
|
||||
@typedoc "A socket address"
|
||||
@type address ::
|
||||
:inet.ip_address()
|
||||
| :inet.local_address()
|
||||
| {:local, binary()}
|
||||
| :unspec
|
||||
| {:undefined, any()}
|
||||
@typedoc "Connection statistics for a given socket"
|
||||
@type socket_stats() :: {:ok, [{:inet.stat_option(), integer()}]} | {:error, :inet.posix()}
|
||||
|
||||
@typedoc "Options which can be set on a socket via setopts/2 (or returned from getopts/1)"
|
||||
@type socket_get_options() :: [:inet.socket_getopt()]
|
||||
|
||||
@typedoc "Options which can be set on a socket via setopts/2 (or returned from getopts/1)"
|
||||
@type socket_set_options() :: [:inet.socket_setopt()]
|
||||
|
||||
@typedoc "The direction in which to shutdown a connection in advance of closing it"
|
||||
@type way() :: :read | :write | :read_write
|
||||
|
||||
@typedoc "The return value from a listen/2 call"
|
||||
@type on_listen() ::
|
||||
{:ok, listener_socket()} | {:error, :system_limit} | {:error, :inet.posix()}
|
||||
|
||||
@typedoc "The return value from an accept/1 call"
|
||||
@type on_accept() :: {:ok, socket()} | {:error, on_accept_tcp_error() | on_accept_ssl_error()}
|
||||
|
||||
@type on_accept_tcp_error() :: :closed | :system_limit | :inet.posix()
|
||||
@type on_accept_ssl_error() :: :closed | :timeout | :ssl.error_alert()
|
||||
|
||||
@typedoc "The return value from a controlling_process/2 call"
|
||||
@type on_controlling_process() :: :ok | {:error, :closed | :not_owner | :badarg | :inet.posix()}
|
||||
|
||||
@typedoc "The return value from a handshake/1 call"
|
||||
@type on_handshake() :: {:ok, socket()} | {:error, on_handshake_ssl_error()}
|
||||
|
||||
@type on_handshake_ssl_error() :: :closed | :timeout | :ssl.error_alert()
|
||||
|
||||
@typedoc "The return value from a upgrade/2 call"
|
||||
@type on_upgrade() :: {:ok, socket()} | {:error, term()}
|
||||
|
||||
@typedoc "The return value from a shutdown/2 call"
|
||||
@type on_shutdown() :: :ok | {:error, :inet.posix()}
|
||||
|
||||
@typedoc "The return value from a close/1 call"
|
||||
@type on_close() :: :ok | {:error, any()}
|
||||
|
||||
@typedoc "The return value from a recv/3 call"
|
||||
@type on_recv() :: {:ok, binary()} | {:error, :closed | :timeout | :inet.posix()}
|
||||
|
||||
@typedoc "The return value from a send/2 call"
|
||||
@type on_send() :: :ok | {:error, :closed | {:timeout, rest_data :: binary()} | :inet.posix()}
|
||||
|
||||
@typedoc "The return value from a sendfile/4 call"
|
||||
@type on_sendfile() ::
|
||||
{:ok, non_neg_integer()}
|
||||
| {:error, :inet.posix() | :closed | :badarg | :not_owner | :eof}
|
||||
|
||||
@typedoc "The return value from a getopts/2 call"
|
||||
@type on_getopts() :: {:ok, [:inet.socket_optval()]} | {:error, :inet.posix()}
|
||||
|
||||
@typedoc "The return value from a setopts/2 call"
|
||||
@type on_setopts() :: :ok | {:error, :inet.posix()}
|
||||
|
||||
@typedoc "The return value from a sockname/1 call"
|
||||
@type on_sockname() :: {:ok, socket_info()} | {:error, :inet.posix()}
|
||||
|
||||
@typedoc "The return value from a peername/1 call"
|
||||
@type on_peername() :: {:ok, socket_info()} | {:error, :inet.posix()}
|
||||
|
||||
@typedoc "The return value from a peercert/1 call"
|
||||
@type on_peercert() :: {:ok, :public_key.der_encoded()} | {:error, reason :: any()}
|
||||
|
||||
@typedoc "The return value from a connection_information/1 call"
|
||||
@type on_connection_information() :: {:ok, :ssl.connection_info()} | {:error, reason :: any()}
|
||||
|
||||
@typedoc "The return value from a negotiated_protocol/1 call"
|
||||
@type on_negotiated_protocol() ::
|
||||
{:ok, binary()} | {:error, :protocol_not_negotiated | :closed}
|
||||
|
||||
@doc """
|
||||
Create and return a listener socket bound to the given port and configured per
|
||||
the provided options.
|
||||
"""
|
||||
@callback listen(:inet.port_number(), listen_options()) ::
|
||||
{:ok, listener_socket()} | {:error, any()}
|
||||
|
||||
@doc """
|
||||
Wait for a client connection on the given listener socket. This call blocks until
|
||||
such a connection arrives, or an error occurs (such as the listener socket being
|
||||
closed).
|
||||
"""
|
||||
@callback accept(listener_socket()) :: on_accept()
|
||||
|
||||
@doc """
|
||||
Performs an initial handshake on a new client connection (such as that done
|
||||
when negotiating an SSL connection). Transports which do not have such a
|
||||
handshake can simply pass the socket through unchanged.
|
||||
"""
|
||||
@callback handshake(socket()) :: on_handshake()
|
||||
|
||||
@doc """
|
||||
Performs an upgrade of an existing client connection (for example upgrading
|
||||
an already-established connection to SSL). Transports which do not support upgrading can return
|
||||
`{:error, :unsupported_upgrade}`.
|
||||
"""
|
||||
@callback upgrade(socket(), term()) :: on_upgrade()
|
||||
|
||||
@doc """
|
||||
Transfers ownership of the given socket to the given process. This will always
|
||||
be called by the process which currently owns the socket.
|
||||
"""
|
||||
@callback controlling_process(socket(), pid()) :: on_controlling_process()
|
||||
|
||||
@doc """
|
||||
Returns available bytes on the given socket. Up to `num_bytes` bytes will be
|
||||
returned (0 can be passed in to get the next 'available' bytes, typically the
|
||||
next packet). If insufficient bytes are available, the function can wait `timeout`
|
||||
milliseconds for data to arrive.
|
||||
"""
|
||||
@callback recv(socket(), num_bytes :: non_neg_integer(), timeout :: timeout()) :: on_recv()
|
||||
|
||||
@doc """
|
||||
Sends the given data (specified as a binary or an IO list) on the given socket.
|
||||
"""
|
||||
@callback send(socket(), data :: iodata()) :: on_send()
|
||||
|
||||
@doc """
|
||||
Sends the contents of the given file based on the provided offset & length
|
||||
"""
|
||||
@callback sendfile(
|
||||
socket(),
|
||||
filename :: String.t(),
|
||||
offset :: non_neg_integer(),
|
||||
length :: non_neg_integer()
|
||||
) :: on_sendfile()
|
||||
|
||||
@doc """
|
||||
Gets the given options on the socket.
|
||||
"""
|
||||
@callback getopts(socket(), socket_get_options()) :: on_getopts()
|
||||
|
||||
@doc """
|
||||
Sets the given options on the socket. Should disallow setting of options which
|
||||
are not compatible with Thousand Island
|
||||
"""
|
||||
@callback setopts(socket(), socket_set_options()) :: on_setopts()
|
||||
|
||||
@doc """
|
||||
Shuts down the socket in the given direction.
|
||||
"""
|
||||
@callback shutdown(socket(), way()) :: on_shutdown()
|
||||
|
||||
@doc """
|
||||
Closes the given socket.
|
||||
"""
|
||||
@callback close(socket() | listener_socket()) :: on_close()
|
||||
|
||||
@doc """
|
||||
Returns information in the form of `t:socket_info()` about the local end of the socket.
|
||||
"""
|
||||
@callback sockname(socket() | listener_socket()) :: on_sockname()
|
||||
|
||||
@doc """
|
||||
Returns information in the form of `t:socket_info()` about the remote end of the socket.
|
||||
"""
|
||||
@callback peername(socket()) :: on_peername()
|
||||
|
||||
@doc """
|
||||
Returns the peer certificate for the given socket in the form of `t:public_key.der_encoded()`.
|
||||
If the socket is not secure, `{:error, :not_secure}` is returned.
|
||||
"""
|
||||
@callback peercert(socket()) :: on_peercert()
|
||||
|
||||
@doc """
|
||||
Returns whether or not this protocol is secure.
|
||||
"""
|
||||
@callback secure?() :: boolean()
|
||||
|
||||
@doc """
|
||||
Returns stats about the connection on the socket.
|
||||
"""
|
||||
@callback getstat(socket()) :: socket_stats()
|
||||
|
||||
@doc """
|
||||
Returns the protocol negotiated as part of handshaking. Most typically this is via TLS'
|
||||
ALPN or NPN extensions. If the underlying transport does not support protocol negotiation
|
||||
(or if one was not negotiated), `{:error, :protocol_not_negotiated}` is returned
|
||||
"""
|
||||
@callback negotiated_protocol(socket()) :: on_negotiated_protocol()
|
||||
|
||||
@doc """
|
||||
Returns the SSL connection_info for the given socket. If the socket is not secure,
|
||||
`{:error, :not_secure}` is returned.
|
||||
"""
|
||||
@callback connection_information(socket()) :: on_connection_information()
|
||||
end
|
||||
@@ -0,0 +1,221 @@
|
||||
defmodule ThousandIsland.Transports.SSL do
|
||||
@moduledoc """
|
||||
Defines a `ThousandIsland.Transport` implementation based on TCP SSL sockets
|
||||
as provided by Erlang's `:ssl` module. For the most part, users of Thousand
|
||||
Island will only ever need to deal with this module via `transport_options`
|
||||
passed to `ThousandIsland` at startup time. A complete list of such options
|
||||
is defined via the `t::ssl.tls_server_option/0` type. This list can be somewhat
|
||||
difficult to decipher; by far the most common values to pass to this transport
|
||||
are the following:
|
||||
|
||||
* `keyfile`: The path to a PEM encoded key to use for SSL
|
||||
* `certfile`: The path to a PEM encoded cert to use for SSL
|
||||
* `ip`: The IP to listen on. Can be specified as:
|
||||
* `{1, 2, 3, 4}` for IPv4 addresses
|
||||
* `{1, 2, 3, 4, 5, 6, 7, 8}` for IPv6 addresses
|
||||
* `:loopback` for local loopback
|
||||
* `:any` for all interfaces (ie: `0.0.0.0`)
|
||||
* `{:local, "/path/to/socket"}` for a Unix domain socket. If this option is used, the `port`
|
||||
option *must* be set to `0`.
|
||||
|
||||
Unless overridden, this module uses the following default options:
|
||||
|
||||
```elixir
|
||||
backlog: 1024,
|
||||
nodelay: true,
|
||||
send_timeout: 30_000,
|
||||
send_timeout_close: true,
|
||||
reuseaddr: true
|
||||
```
|
||||
|
||||
The following options are required for the proper operation of Thousand Island
|
||||
and cannot be overridden:
|
||||
|
||||
```elixir
|
||||
mode: :binary,
|
||||
active: false
|
||||
```
|
||||
"""
|
||||
|
||||
@type options() :: [:ssl.tls_server_option()]
|
||||
@type listener_socket() :: :ssl.sslsocket()
|
||||
@type socket() :: :ssl.sslsocket()
|
||||
|
||||
@behaviour ThousandIsland.Transport
|
||||
|
||||
@hardcoded_options [mode: :binary, active: false]
|
||||
|
||||
# Default chunk size: 8MB - balances memory usage vs syscall overhead
|
||||
@sendfile_chunk_size 8 * 1024 * 1024
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec listen(:inet.port_number(), [:ssl.tls_server_option()]) ::
|
||||
ThousandIsland.Transport.on_listen()
|
||||
def listen(port, user_options) do
|
||||
default_options = [
|
||||
backlog: 1024,
|
||||
nodelay: true,
|
||||
send_timeout: 30_000,
|
||||
send_timeout_close: true,
|
||||
reuseaddr: true
|
||||
]
|
||||
|
||||
# We can't use Keyword functions here because :ssl accepts non-keyword style options
|
||||
resolved_options =
|
||||
Enum.uniq_by(
|
||||
@hardcoded_options ++ user_options ++ default_options,
|
||||
fn
|
||||
{key, _} when is_atom(key) -> key
|
||||
key when is_atom(key) -> key
|
||||
end
|
||||
)
|
||||
|
||||
if not Enum.any?(
|
||||
[:certs_keys, :keyfile, :key, :sni_hosts, :sni_fun],
|
||||
&:proplists.is_defined(&1, resolved_options)
|
||||
) do
|
||||
raise "transport_options must include one of keyfile, key, sni_hosts or sni_fun"
|
||||
end
|
||||
|
||||
if not Enum.any?(
|
||||
[:certs_keys, :certfile, :cert, :sni_hosts, :sni_fun],
|
||||
&:proplists.is_defined(&1, resolved_options)
|
||||
) do
|
||||
raise "transport_options must include one of certfile, cert, sni_hosts or sni_fun"
|
||||
end
|
||||
|
||||
:ssl.listen(port, resolved_options)
|
||||
end
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec accept(listener_socket()) :: ThousandIsland.Transport.on_accept()
|
||||
defdelegate accept(listener_socket), to: :ssl, as: :transport_accept
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec handshake(socket()) :: ThousandIsland.Transport.on_handshake()
|
||||
def handshake(socket) do
|
||||
case :ssl.handshake(socket) do
|
||||
{:ok, socket, _protocol_extensions} -> {:ok, socket}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec upgrade(socket(), options()) :: ThousandIsland.Transport.on_upgrade()
|
||||
def upgrade(socket, opts) do
|
||||
case :ssl.handshake(socket, opts) do
|
||||
{:ok, socket, _protocol_extensions} -> {:ok, socket}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec controlling_process(socket(), pid()) :: ThousandIsland.Transport.on_controlling_process()
|
||||
defdelegate controlling_process(socket, pid), to: :ssl
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec recv(socket(), non_neg_integer(), timeout()) :: ThousandIsland.Transport.on_recv()
|
||||
defdelegate recv(socket, length, timeout), to: :ssl
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec send(socket(), iodata()) :: ThousandIsland.Transport.on_send()
|
||||
defdelegate send(socket, data), to: :ssl
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec sendfile(
|
||||
socket(),
|
||||
filename :: String.t(),
|
||||
offset :: non_neg_integer(),
|
||||
length :: non_neg_integer()
|
||||
) :: ThousandIsland.Transport.on_sendfile()
|
||||
def sendfile(socket, filename, offset, length) do
|
||||
# We can't use :file.sendfile here since it works on clear sockets, not ssl sockets.
|
||||
# Build our own version with chunking for large files.
|
||||
case :file.open(filename, [:read, :raw, :binary]) do
|
||||
{:ok, fd} ->
|
||||
try do
|
||||
sendfile_loop(socket, fd, offset, length, 0)
|
||||
after
|
||||
:file.close(fd)
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp sendfile_loop(_socket, _fd, _offset, sent, sent) when 0 != sent do
|
||||
{:ok, sent}
|
||||
end
|
||||
|
||||
defp sendfile_loop(socket, fd, offset, length, sent) do
|
||||
with read_size <- chunk_size(length, sent, @sendfile_chunk_size),
|
||||
{:ok, data} <- :file.pread(fd, offset, read_size),
|
||||
:ok <- :ssl.send(socket, data) do
|
||||
now_sent = byte_size(data)
|
||||
sendfile_loop(socket, fd, offset + now_sent, length, sent + now_sent)
|
||||
else
|
||||
:eof ->
|
||||
{:ok, sent}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp chunk_size(0, _sent, chunk_size), do: chunk_size
|
||||
defp chunk_size(length, sent, chunk), do: min(length - sent, chunk)
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec getopts(socket(), ThousandIsland.Transport.socket_get_options()) ::
|
||||
ThousandIsland.Transport.on_getopts()
|
||||
defdelegate getopts(socket, options), to: :ssl
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec setopts(socket(), ThousandIsland.Transport.socket_set_options()) ::
|
||||
ThousandIsland.Transport.on_setopts()
|
||||
defdelegate setopts(socket, options), to: :ssl
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec shutdown(socket(), ThousandIsland.Transport.way()) ::
|
||||
ThousandIsland.Transport.on_shutdown()
|
||||
defdelegate shutdown(socket, way), to: :ssl
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec close(socket() | listener_socket()) :: ThousandIsland.Transport.on_close()
|
||||
defdelegate close(socket), to: :ssl
|
||||
|
||||
# :ssl.sockname/1's typespec is incorrect
|
||||
@dialyzer {:no_match, sockname: 1}
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec sockname(socket() | listener_socket()) :: ThousandIsland.Transport.on_sockname()
|
||||
defdelegate sockname(socket), to: :ssl
|
||||
|
||||
# :ssl.peername/1's typespec is incorrect
|
||||
@dialyzer {:no_match, peername: 1}
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec peername(socket()) :: ThousandIsland.Transport.on_peername()
|
||||
defdelegate peername(socket), to: :ssl
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec peercert(socket()) :: ThousandIsland.Transport.on_peercert()
|
||||
defdelegate peercert(socket), to: :ssl
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec secure?() :: true
|
||||
def secure?, do: true
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec getstat(socket()) :: ThousandIsland.Transport.socket_stats()
|
||||
defdelegate getstat(socket), to: :ssl
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec negotiated_protocol(socket()) :: ThousandIsland.Transport.on_negotiated_protocol()
|
||||
defdelegate negotiated_protocol(socket), to: :ssl
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec connection_information(socket()) :: ThousandIsland.Transport.on_connection_information()
|
||||
defdelegate connection_information(socket), to: :ssl
|
||||
end
|
||||
@@ -0,0 +1,169 @@
|
||||
defmodule ThousandIsland.Transports.TCP do
|
||||
@moduledoc """
|
||||
Defines a `ThousandIsland.Transport` implementation based on clear TCP sockets
|
||||
as provided by Erlang's `:gen_tcp` module. For the most part, users of Thousand
|
||||
Island will only ever need to deal with this module via `transport_options`
|
||||
passed to `ThousandIsland` at startup time. A complete list of such options
|
||||
is defined via the `t::gen_tcp.listen_option/0` type. This list can be somewhat
|
||||
difficult to decipher; by far the most common value to pass to this transport
|
||||
is the following:
|
||||
|
||||
* `ip`: The IP to listen on. Can be specified as:
|
||||
* `{1, 2, 3, 4}` for IPv4 addresses
|
||||
* `{1, 2, 3, 4, 5, 6, 7, 8}` for IPv6 addresses
|
||||
* `:loopback` for local loopback
|
||||
* `:any` for all interfaces (i.e.: `0.0.0.0`)
|
||||
* `{:local, "/path/to/socket"}` for a Unix domain socket. If this option is used,
|
||||
the `port` option *must* be set to `0`
|
||||
|
||||
Unless overridden, this module uses the following default options:
|
||||
|
||||
```elixir
|
||||
backlog: 1024,
|
||||
nodelay: true,
|
||||
send_timeout: 30_000,
|
||||
send_timeout_close: true,
|
||||
reuseaddr: true
|
||||
```
|
||||
|
||||
The following options are required for the proper operation of Thousand Island
|
||||
and cannot be overridden:
|
||||
|
||||
```elixir
|
||||
mode: :binary,
|
||||
active: false
|
||||
```
|
||||
"""
|
||||
|
||||
@type options() :: [:gen_tcp.listen_option()]
|
||||
@type listener_socket() :: :inet.socket()
|
||||
@type socket() :: :inet.socket()
|
||||
|
||||
@behaviour ThousandIsland.Transport
|
||||
|
||||
@hardcoded_options [mode: :binary, active: false]
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec listen(:inet.port_number(), [:inet.inet_backend() | :gen_tcp.listen_option()]) ::
|
||||
ThousandIsland.Transport.on_listen()
|
||||
def listen(port, user_options) do
|
||||
default_options = [
|
||||
backlog: 1024,
|
||||
nodelay: true,
|
||||
send_timeout: 30_000,
|
||||
send_timeout_close: true,
|
||||
reuseaddr: true
|
||||
]
|
||||
|
||||
# We can't use Keyword functions here because :gen_tcp accepts non-keyword style options
|
||||
resolved_options =
|
||||
Enum.uniq_by(
|
||||
@hardcoded_options ++ user_options ++ default_options,
|
||||
fn
|
||||
{key, _} when is_atom(key) -> key
|
||||
key when is_atom(key) -> key
|
||||
end
|
||||
)
|
||||
|
||||
# `inet_backend`, if present, needs to be the first option
|
||||
sorted_options =
|
||||
Enum.sort(resolved_options, fn
|
||||
_, {:inet_backend, _} -> false
|
||||
_, _ -> true
|
||||
end)
|
||||
|
||||
:gen_tcp.listen(port, sorted_options)
|
||||
end
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec accept(listener_socket()) :: ThousandIsland.Transport.on_accept()
|
||||
defdelegate accept(listener_socket), to: :gen_tcp
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec handshake(socket()) :: ThousandIsland.Transport.on_handshake()
|
||||
def handshake(socket), do: {:ok, socket}
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec upgrade(socket(), options()) :: ThousandIsland.Transport.on_upgrade()
|
||||
def upgrade(_, _), do: {:error, :unsupported_upgrade}
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec controlling_process(socket(), pid()) :: ThousandIsland.Transport.on_controlling_process()
|
||||
defdelegate controlling_process(socket, pid), to: :gen_tcp
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec recv(socket(), non_neg_integer(), timeout()) :: ThousandIsland.Transport.on_recv()
|
||||
defdelegate recv(socket, length, timeout), to: :gen_tcp
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec send(socket(), iodata()) :: ThousandIsland.Transport.on_send()
|
||||
defdelegate send(socket, data), to: :gen_tcp
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec sendfile(
|
||||
socket(),
|
||||
filename :: String.t(),
|
||||
offset :: non_neg_integer(),
|
||||
length :: non_neg_integer()
|
||||
) :: ThousandIsland.Transport.on_sendfile()
|
||||
def sendfile(socket, filename, offset, length) do
|
||||
case :file.open(filename, [:read, :raw, :binary]) do
|
||||
{:ok, fd} ->
|
||||
try do
|
||||
:file.sendfile(fd, socket, offset, length, [])
|
||||
after
|
||||
:file.close(fd)
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec getopts(socket(), ThousandIsland.Transport.socket_get_options()) ::
|
||||
ThousandIsland.Transport.on_getopts()
|
||||
defdelegate getopts(socket, options), to: :inet
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec setopts(socket(), ThousandIsland.Transport.socket_set_options()) ::
|
||||
ThousandIsland.Transport.on_setopts()
|
||||
defdelegate setopts(socket, options), to: :inet
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec shutdown(socket(), ThousandIsland.Transport.way()) ::
|
||||
ThousandIsland.Transport.on_shutdown()
|
||||
defdelegate shutdown(socket, way), to: :gen_tcp
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec close(socket() | listener_socket()) :: :ok
|
||||
defdelegate close(socket), to: :gen_tcp
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec sockname(socket() | listener_socket()) :: ThousandIsland.Transport.on_sockname()
|
||||
defdelegate sockname(socket), to: :inet
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec peername(socket()) :: ThousandIsland.Transport.on_peername()
|
||||
defdelegate peername(socket), to: :inet
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec peercert(socket()) :: ThousandIsland.Transport.on_peercert()
|
||||
def peercert(_socket), do: {:error, :not_secure}
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec secure?() :: false
|
||||
def secure?, do: false
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec getstat(socket()) :: ThousandIsland.Transport.socket_stats()
|
||||
defdelegate getstat(socket), to: :inet
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec negotiated_protocol(socket()) :: ThousandIsland.Transport.on_negotiated_protocol()
|
||||
def negotiated_protocol(_socket), do: {:error, :protocol_not_negotiated}
|
||||
|
||||
@impl ThousandIsland.Transport
|
||||
@spec connection_information(socket()) :: ThousandIsland.Transport.on_connection_information()
|
||||
def connection_information(_socket), do: {:error, :not_secure}
|
||||
end
|
||||
Reference in New Issue
Block a user