update
This commit is contained in:
754
phoenix/deps/finch/lib/finch.ex
Normal file
754
phoenix/deps/finch/lib/finch.ex
Normal file
@@ -0,0 +1,754 @@
|
||||
defmodule Finch do
|
||||
@external_resource "README.md"
|
||||
@moduledoc "README.md"
|
||||
|> File.read!()
|
||||
|> String.split("<!-- MDOC !-->")
|
||||
|> Enum.fetch!(1)
|
||||
|
||||
alias Finch.{PoolManager, Request, Response}
|
||||
require Finch.Pool
|
||||
|
||||
use Supervisor
|
||||
|
||||
@default_pool_size 50
|
||||
@default_pool_count 1
|
||||
|
||||
@default_connect_timeout 5_000
|
||||
|
||||
@pool_config_schema [
|
||||
protocol: [
|
||||
type: {:in, [:http2, :http1]},
|
||||
deprecated: "Use `:protocols` instead."
|
||||
],
|
||||
protocols: [
|
||||
type: {:list, {:in, [:http1, :http2]}},
|
||||
doc: """
|
||||
The type of connections to support.
|
||||
|
||||
If using `:http1` only, an HTTP1 pool without multiplexing is used. \
|
||||
If using `:http2` only, an HTTP2 pool with multiplexing is used. \
|
||||
If both are listed, then both HTTP1/HTTP2 connections are \
|
||||
supported (via ALPN), but there is no multiplexing.
|
||||
""",
|
||||
default: [:http1]
|
||||
],
|
||||
size: [
|
||||
type: :pos_integer,
|
||||
doc: """
|
||||
Number of connections to maintain in each pool. Used only by HTTP1 pools \
|
||||
since HTTP2 is able to multiplex requests through a single connection. In \
|
||||
other words, for HTTP2, the size is always 1 and the `:count` should be \
|
||||
configured in order to increase capacity.
|
||||
""",
|
||||
default: @default_pool_size
|
||||
],
|
||||
count: [
|
||||
type: :pos_integer,
|
||||
doc: """
|
||||
Number of pools to start. HTTP1 pools are able to re-use connections in the \
|
||||
same pool and establish new ones only when necessary. However, if there is a \
|
||||
high pool count and few requests are made, these requests will be scattered \
|
||||
across pools, reducing connection reuse. It is recommended to increase the pool \
|
||||
count for HTTP1 only if you are experiencing high checkout times.
|
||||
""",
|
||||
default: @default_pool_count
|
||||
],
|
||||
max_idle_time: [
|
||||
type: :timeout,
|
||||
doc: """
|
||||
The maximum number of milliseconds an HTTP1 connection is allowed to be idle \
|
||||
before being closed during a checkout attempt.
|
||||
""",
|
||||
deprecated: "Use :conn_max_idle_time instead."
|
||||
],
|
||||
conn_opts: [
|
||||
type: :keyword_list,
|
||||
doc: """
|
||||
These options are passed to `Mint.HTTP.connect/4` whenever a new connection is established. \
|
||||
`:mode` is not configurable as Finch must control this setting. Typically these options are \
|
||||
used to configure proxying, https settings, or connect timeouts.
|
||||
""",
|
||||
default: []
|
||||
],
|
||||
pool_max_idle_time: [
|
||||
type: :timeout,
|
||||
doc: """
|
||||
The maximum number of milliseconds that a pool can be idle before being terminated, used only by HTTP1 pools. \
|
||||
This options is forwarded to NimblePool and it starts and idle verification cycle that may impact \
|
||||
performance if misused. For instance setting a very low timeout may lead to pool restarts. \
|
||||
For more information see NimblePool's `handle_ping/2` documentation.
|
||||
""",
|
||||
default: :infinity
|
||||
],
|
||||
conn_max_idle_time: [
|
||||
type: :timeout,
|
||||
doc: """
|
||||
The maximum number of milliseconds an HTTP1 connection is allowed to be idle \
|
||||
before being closed during a checkout attempt.
|
||||
""",
|
||||
default: :infinity
|
||||
],
|
||||
start_pool_metrics?: [
|
||||
type: :boolean,
|
||||
doc: "When true, pool metrics will be collected and available through `get_pool_status/2`",
|
||||
default: false
|
||||
]
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
The `:name` provided to Finch in `start_link/1`.
|
||||
"""
|
||||
@type name() :: atom()
|
||||
|
||||
@type scheme() :: :http | :https
|
||||
|
||||
@type scheme_host_port() :: {scheme(), host :: String.t(), port :: :inet.port_number()}
|
||||
|
||||
@typedoc """
|
||||
Pool metrics returned by `get_pool_status/2` for a single pool.
|
||||
"""
|
||||
@type pool_metrics() ::
|
||||
[Finch.HTTP1.PoolMetrics.t()]
|
||||
| [Finch.HTTP2.PoolMetrics.t()]
|
||||
|
||||
@typedoc """
|
||||
Pool metrics grouped by SHP when querying the `:default` configuration.
|
||||
"""
|
||||
@type default_pool_metrics() :: %{required(scheme_host_port()) => pool_metrics()}
|
||||
|
||||
@type request_opt() ::
|
||||
{:pool_timeout, timeout()}
|
||||
| {:receive_timeout, timeout()}
|
||||
| {:request_timeout, timeout()}
|
||||
|
||||
@typedoc """
|
||||
Options used by request functions.
|
||||
"""
|
||||
@type request_opts() :: [request_opt()]
|
||||
|
||||
@typedoc """
|
||||
The reference used to identify a request sent using `async_request/3`.
|
||||
"""
|
||||
@opaque request_ref() :: Finch.Pool.request_ref()
|
||||
|
||||
@typedoc """
|
||||
The stream function given to `stream/5`.
|
||||
"""
|
||||
@type stream(acc) ::
|
||||
({:status, integer}
|
||||
| {:headers, Mint.Types.headers()}
|
||||
| {:data, binary}
|
||||
| {:trailers, Mint.Types.headers()},
|
||||
acc ->
|
||||
acc)
|
||||
|
||||
@typedoc """
|
||||
The stream function given to `stream_while/5`.
|
||||
"""
|
||||
@type stream_while(acc) ::
|
||||
({:status, integer}
|
||||
| {:headers, Mint.Types.headers()}
|
||||
| {:data, binary}
|
||||
| {:trailers, Mint.Types.headers()},
|
||||
acc ->
|
||||
{:cont, acc} | {:halt, acc})
|
||||
|
||||
@doc """
|
||||
Start an instance of Finch.
|
||||
|
||||
## Options
|
||||
|
||||
* `:name` - The name of your Finch instance. This field is required.
|
||||
|
||||
* `:pools` - A map specifying the configuration for your pools. The keys should be URLs
|
||||
provided as binaries, a tuple `{scheme, {:local, unix_socket}}` where `unix_socket` is the path for
|
||||
the socket, or the atom `:default` to provide a catch-all configuration to be used for any
|
||||
unspecified URLs - meaning that new pools for unspecified URLs will be started using the `:default`
|
||||
configuration. See "Pool Configuration Options" below for details on the possible map
|
||||
values. Default value is `%{default: [size: #{@default_pool_size}, count: #{@default_pool_count}]}`.
|
||||
|
||||
### Pool Configuration Options
|
||||
|
||||
#{NimbleOptions.docs(@pool_config_schema)}
|
||||
"""
|
||||
def start_link(opts) do
|
||||
name = finch_name!(opts)
|
||||
pools = Keyword.get(opts, :pools, []) |> pool_options!()
|
||||
{default_pool_config, pools} = Map.pop(pools, :default)
|
||||
|
||||
config = %{
|
||||
registry_name: name,
|
||||
manager_name: manager_name(name),
|
||||
supervisor_name: pool_supervisor_name(name),
|
||||
default_pool_config: default_pool_config,
|
||||
pools: pools
|
||||
}
|
||||
|
||||
Supervisor.start_link(__MODULE__, config, name: supervisor_name(name))
|
||||
end
|
||||
|
||||
def child_spec(opts) do
|
||||
%{
|
||||
id: finch_name!(opts),
|
||||
start: {__MODULE__, :start_link, [opts]}
|
||||
}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(config) do
|
||||
children = [
|
||||
{Registry, [keys: :duplicate, name: config.registry_name, meta: [config: config]]},
|
||||
{DynamicSupervisor, name: config.supervisor_name, strategy: :one_for_one},
|
||||
{PoolManager, config}
|
||||
]
|
||||
|
||||
Supervisor.init(children, strategy: :one_for_all)
|
||||
end
|
||||
|
||||
defp finch_name!(opts) do
|
||||
Keyword.get(opts, :name) || raise(ArgumentError, "must supply a name")
|
||||
end
|
||||
|
||||
defp pool_options!(pools) do
|
||||
{:ok, default} = NimbleOptions.validate([], @pool_config_schema)
|
||||
|
||||
Enum.reduce(pools, %{default: valid_opts_to_map(default)}, fn {destination, opts}, acc ->
|
||||
with {:ok, valid_destination} <- cast_destination(destination),
|
||||
{:ok, valid_pool_opts} <- cast_pool_opts(opts) do
|
||||
Map.put(acc, valid_destination, valid_pool_opts)
|
||||
else
|
||||
{:error, reason} ->
|
||||
raise reason
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp cast_destination(destination) do
|
||||
case destination do
|
||||
:default ->
|
||||
{:ok, destination}
|
||||
|
||||
{scheme, {:local, path}} when is_atom(scheme) and is_binary(path) ->
|
||||
{:ok, {scheme, {:local, path}, 0}}
|
||||
|
||||
url when is_binary(url) ->
|
||||
cast_binary_destination(url)
|
||||
|
||||
_ ->
|
||||
{:error, %ArgumentError{message: "invalid destination: #{inspect(destination)}"}}
|
||||
end
|
||||
end
|
||||
|
||||
defp cast_binary_destination(url) when is_binary(url) do
|
||||
{scheme, host, port, _path, _query} = Finch.Request.parse_url(url)
|
||||
{:ok, {scheme, host, port}}
|
||||
end
|
||||
|
||||
defp cast_pool_opts(opts) do
|
||||
with {:ok, valid} <- NimbleOptions.validate(opts, @pool_config_schema) do
|
||||
{:ok, valid_opts_to_map(valid)}
|
||||
end
|
||||
end
|
||||
|
||||
defp valid_opts_to_map(valid) do
|
||||
# We need to enable keepalive and set the nodelay flag to true by default.
|
||||
transport_opts =
|
||||
valid
|
||||
|> get_in([:conn_opts, :transport_opts])
|
||||
|> List.wrap()
|
||||
|> Keyword.put_new(:timeout, @default_connect_timeout)
|
||||
|> Keyword.put_new(:nodelay, true)
|
||||
|> Keyword.put(:keepalive, true)
|
||||
|
||||
conn_opts = valid[:conn_opts] |> List.wrap()
|
||||
|
||||
# Only relevant to HTTP2, but just gracefully ignored in HTTP1.
|
||||
# Since we cannot handle server push responses, we need to disable the feature.
|
||||
client_settings =
|
||||
conn_opts
|
||||
|> Keyword.get(:client_settings, [])
|
||||
|> Keyword.put(:enable_push, false)
|
||||
|
||||
ssl_key_log_file =
|
||||
Keyword.get(conn_opts, :ssl_key_log_file) || System.get_env("SSLKEYLOGFILE")
|
||||
|
||||
ssl_key_log_file_device = ssl_key_log_file && File.open!(ssl_key_log_file, [:append])
|
||||
|
||||
conn_opts =
|
||||
conn_opts
|
||||
|> Keyword.put(:ssl_key_log_file_device, ssl_key_log_file_device)
|
||||
|> Keyword.put(:transport_opts, transport_opts)
|
||||
|> Keyword.put(:protocols, valid[:protocols])
|
||||
|> Keyword.put(:client_settings, client_settings)
|
||||
|
||||
# TODO: Remove :protocol on v0.18
|
||||
mod =
|
||||
case valid[:protocol] do
|
||||
:http1 ->
|
||||
Finch.HTTP1.Pool
|
||||
|
||||
:http2 ->
|
||||
Finch.HTTP2.Pool
|
||||
|
||||
nil ->
|
||||
if :http1 in valid[:protocols] do
|
||||
Finch.HTTP1.Pool
|
||||
else
|
||||
Finch.HTTP2.Pool
|
||||
end
|
||||
end
|
||||
|
||||
%{
|
||||
mod: mod,
|
||||
size: valid[:size],
|
||||
count: valid[:count],
|
||||
conn_opts: conn_opts,
|
||||
conn_max_idle_time: to_native(valid[:max_idle_time] || valid[:conn_max_idle_time]),
|
||||
pool_max_idle_time: valid[:pool_max_idle_time],
|
||||
start_pool_metrics?: valid[:start_pool_metrics?]
|
||||
}
|
||||
end
|
||||
|
||||
defp to_native(:infinity), do: :infinity
|
||||
defp to_native(time), do: System.convert_time_unit(time, :millisecond, :native)
|
||||
|
||||
defp supervisor_name(name), do: :"#{name}.Supervisor"
|
||||
defp manager_name(name), do: :"#{name}.PoolManager"
|
||||
defp pool_supervisor_name(name), do: :"#{name}.PoolSupervisor"
|
||||
|
||||
defmacrop request_span(request, name, do: block) do
|
||||
quote do
|
||||
start_meta = %{request: unquote(request), name: unquote(name)}
|
||||
|
||||
Finch.Telemetry.span(:request, start_meta, fn ->
|
||||
result = unquote(block)
|
||||
end_meta = Map.put(start_meta, :result, result)
|
||||
{result, end_meta}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds an HTTP request to be sent with `request/3` or `stream/4`.
|
||||
|
||||
It is possible to send the request body in a streaming fashion. In order to do so, the
|
||||
`body` parameter needs to take form of a tuple `{:stream, body_stream}`, where `body_stream`
|
||||
is a `Stream`.
|
||||
"""
|
||||
@spec build(Request.method(), Request.url(), Request.headers(), Request.body(), Keyword.t()) ::
|
||||
Request.t()
|
||||
defdelegate build(method, url, headers \\ [], body \\ nil, opts \\ []), to: Request
|
||||
|
||||
@doc """
|
||||
Streams an HTTP request and returns the accumulator.
|
||||
|
||||
A function of arity 2 is expected as argument. The first argument
|
||||
is a tuple, as listed below, and the second argument is the
|
||||
accumulator. The function must return a potentially updated
|
||||
accumulator.
|
||||
|
||||
See also `stream_while/5`.
|
||||
|
||||
> ### HTTP2 streaming and back-pressure {: .warning}
|
||||
>
|
||||
> At the moment, streaming over HTTP2 connections do not provide
|
||||
> any back-pressure mechanism: this means the response will be
|
||||
> sent to the client as quickly as possible. Therefore, you must
|
||||
> not use streaming over HTTP2 for non-terminating responses or
|
||||
> when streaming large responses which you do not intend to keep
|
||||
> in memory.
|
||||
|
||||
## Stream commands
|
||||
|
||||
* `{:status, status}` - the http response status
|
||||
* `{:headers, headers}` - the http response headers
|
||||
* `{:data, data}` - a streaming section of the http response body
|
||||
* `{:trailers, trailers}` - the http response trailers
|
||||
|
||||
## Options
|
||||
|
||||
Shares options with `request/3`.
|
||||
|
||||
## Examples
|
||||
|
||||
path = "/tmp/archive.zip"
|
||||
file = File.open!(path, [:write, :exclusive])
|
||||
url = "https://example.com/archive.zip"
|
||||
request = Finch.build(:get, url)
|
||||
|
||||
Finch.stream(request, MyFinch, nil, fn
|
||||
{:status, status}, _acc ->
|
||||
IO.inspect(status)
|
||||
|
||||
{:headers, headers}, _acc ->
|
||||
IO.inspect(headers)
|
||||
|
||||
{:data, data}, _acc ->
|
||||
IO.binwrite(file, data)
|
||||
end)
|
||||
|
||||
File.close(file)
|
||||
"""
|
||||
@spec stream(Request.t(), name(), acc, stream(acc), request_opts()) ::
|
||||
{:ok, acc} | {:error, Exception.t(), acc}
|
||||
when acc: term()
|
||||
def stream(%Request{} = req, name, acc, fun, opts \\ []) when is_function(fun, 2) do
|
||||
fun = fn entry, acc ->
|
||||
{:cont, fun.(entry, acc)}
|
||||
end
|
||||
|
||||
stream_while(req, name, acc, fun, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Streams an HTTP request until it finishes or `fun` returns `{:halt, acc}`.
|
||||
|
||||
A function of arity 2 is expected as argument. The first argument
|
||||
is a tuple, as listed below, and the second argument is the
|
||||
accumulator.
|
||||
|
||||
The function must return:
|
||||
|
||||
* `{:cont, acc}` to continue streaming
|
||||
* `{:halt, acc}` to halt streaming
|
||||
|
||||
See also `stream/5`.
|
||||
|
||||
> ### HTTP2 streaming and back-pressure {: .warning}
|
||||
>
|
||||
> At the moment, streaming over HTTP2 connections do not provide
|
||||
> any back-pressure mechanism: this means the response will be
|
||||
> sent to the client as quickly as possible. Therefore, you must
|
||||
> not use streaming over HTTP2 for non-terminating responses or
|
||||
> when streaming large responses which you do not intend to keep
|
||||
> in memory.
|
||||
|
||||
## Stream commands
|
||||
|
||||
* `{:status, status}` - the http response status
|
||||
* `{:headers, headers}` - the http response headers
|
||||
* `{:data, data}` - a streaming section of the http response body
|
||||
* `{:trailers, trailers}` - the http response trailers
|
||||
|
||||
## Options
|
||||
|
||||
Shares options with `request/3`.
|
||||
|
||||
## Examples
|
||||
|
||||
path = "/tmp/archive.zip"
|
||||
file = File.open!(path, [:write, :exclusive])
|
||||
url = "https://example.com/archive.zip"
|
||||
request = Finch.build(:get, url)
|
||||
|
||||
Finch.stream_while(request, MyFinch, nil, fn
|
||||
{:status, status}, acc ->
|
||||
IO.inspect(status)
|
||||
{:cont, acc}
|
||||
|
||||
{:headers, headers}, acc ->
|
||||
IO.inspect(headers)
|
||||
{:cont, acc}
|
||||
|
||||
{:data, data}, acc ->
|
||||
IO.binwrite(file, data)
|
||||
{:cont, acc}
|
||||
end)
|
||||
|
||||
File.close(file)
|
||||
"""
|
||||
@spec stream_while(Request.t(), name(), acc, stream_while(acc), request_opts()) ::
|
||||
{:ok, acc} | {:error, Exception.t(), acc}
|
||||
when acc: term()
|
||||
def stream_while(%Request{} = req, name, acc, fun, opts \\ []) when is_function(fun, 2) do
|
||||
request_span req, name do
|
||||
__stream__(req, name, acc, fun, opts)
|
||||
end
|
||||
end
|
||||
|
||||
defp __stream__(%Request{} = req, name, acc, fun, opts) do
|
||||
{pool, pool_mod} = get_pool(req, name)
|
||||
pool_mod.request(pool, req, acc, fun, name, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends an HTTP request and returns a `Finch.Response` struct.
|
||||
|
||||
It can still raise exceptions if it was not possible to check out a connection in the given `:pool_timeout`.
|
||||
|
||||
## Options
|
||||
|
||||
* `:pool_timeout` - This timeout is applied when we check out a connection from the pool.
|
||||
Default value is `5_000`.
|
||||
|
||||
* `:receive_timeout` - The maximum time to wait for each chunk to be received before returning an error.
|
||||
Default value is `15_000`.
|
||||
|
||||
* `:request_timeout` - The amount of time to wait for a complete response before returning an error.
|
||||
This timeout only applies to HTTP/1, and its current implementation is a best effort timeout,
|
||||
it does not guarantee the call will return precisely when the time has elapsed.
|
||||
Default value is `:infinity`.
|
||||
|
||||
"""
|
||||
@spec request(Request.t(), name(), request_opts()) ::
|
||||
{:ok, Response.t()}
|
||||
| {:error, Exception.t()}
|
||||
def request(req, name, opts \\ [])
|
||||
|
||||
def request(%Request{} = req, name, opts) do
|
||||
request_span req, name do
|
||||
acc = {nil, [], [], []}
|
||||
|
||||
fun = fn
|
||||
{:status, value}, {_, headers, body, trailers} ->
|
||||
{:cont, {value, headers, body, trailers}}
|
||||
|
||||
{:headers, value}, {status, headers, body, trailers} ->
|
||||
{:cont, {status, headers ++ value, body, trailers}}
|
||||
|
||||
{:data, value}, {status, headers, body, trailers} ->
|
||||
{:cont, {status, headers, [body | value], trailers}}
|
||||
|
||||
{:trailers, value}, {status, headers, body, trailers} ->
|
||||
{:cont, {status, headers, body, trailers ++ value}}
|
||||
end
|
||||
|
||||
case __stream__(req, name, acc, fun, opts) do
|
||||
{:ok, {status, headers, body, trailers}} ->
|
||||
{:ok,
|
||||
%Response{
|
||||
status: status,
|
||||
headers: headers,
|
||||
body: IO.iodata_to_binary(body),
|
||||
trailers: trailers
|
||||
}}
|
||||
|
||||
{:error, error, _acc} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Catch-all for backwards compatibility below
|
||||
def request(name, method, url) do
|
||||
request(name, method, url, [])
|
||||
end
|
||||
|
||||
@doc false
|
||||
def request(name, method, url, headers, body \\ nil, opts \\ []) do
|
||||
IO.warn("Finch.request/6 is deprecated, use Finch.build/5 + Finch.request/3 instead")
|
||||
|
||||
build(method, url, headers, body)
|
||||
|> request(name, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends an HTTP request and returns a `Finch.Response` struct
|
||||
or raises an exception in case of failure.
|
||||
|
||||
See `request/3` for more detailed information.
|
||||
"""
|
||||
@spec request!(Request.t(), name(), request_opts()) ::
|
||||
Response.t()
|
||||
def request!(%Request{} = req, name, opts \\ []) do
|
||||
case request(req, name, opts) do
|
||||
{:ok, resp} -> resp
|
||||
{:error, exception} -> raise exception
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends an HTTP request asynchronously, returning a request reference.
|
||||
|
||||
If the request is sent using HTTP1, an extra process is spawned to
|
||||
consume messages from the underlying socket. The messages are sent
|
||||
to the current process as soon as they arrive, as a firehose. If
|
||||
you wish to maximize request rate or have more control over how
|
||||
messages are streamed, a strategy using `request/3` or `stream/5`
|
||||
should be used instead.
|
||||
|
||||
## Receiving the response
|
||||
|
||||
Response information is sent to the calling process as it is received
|
||||
in `{ref, response}` tuples.
|
||||
|
||||
If the calling process exits before the request has completed, the
|
||||
request will be canceled.
|
||||
|
||||
Responses include:
|
||||
|
||||
* `{:status, status}` - HTTP response status
|
||||
* `{:headers, headers}` - HTTP response headers
|
||||
* `{:data, data}` - section of the HTTP response body
|
||||
* `{:error, exception}` - an error occurred during the request
|
||||
* `:done` - request has completed successfully
|
||||
|
||||
On a successful request, a single `:status` message will be followed
|
||||
by a single `:headers` message, after which more than one `:data`
|
||||
messages may be sent. If trailing headers are present, a final
|
||||
`:headers` message may be sent. Any `:done` or `:error` message
|
||||
indicates that the request has succeeded or failed and no further
|
||||
messages are expected.
|
||||
|
||||
## Example
|
||||
|
||||
iex> req = Finch.build(:get, "https://httpbin.org/stream/5")
|
||||
iex> ref = Finch.async_request(req, MyFinch)
|
||||
iex> flush()
|
||||
{ref, {:status, 200}}
|
||||
{ref, {:headers, [...]}}
|
||||
{ref, {:data, "..."}}
|
||||
{ref, :done}
|
||||
|
||||
## Options
|
||||
|
||||
Shares options with `request/3`.
|
||||
"""
|
||||
@spec async_request(Request.t(), name(), request_opts()) :: request_ref()
|
||||
def async_request(%Request{} = req, name, opts \\ []) do
|
||||
{pool, pool_mod} = get_pool(req, name)
|
||||
pool_mod.async_request(pool, req, name, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Cancels a request sent with `async_request/3`.
|
||||
"""
|
||||
@spec cancel_async_request(request_ref()) :: :ok
|
||||
def cancel_async_request(request_ref) when Finch.Pool.is_request_ref(request_ref) do
|
||||
{pool_mod, _cancel_ref} = request_ref
|
||||
pool_mod.cancel_async_request(request_ref)
|
||||
end
|
||||
|
||||
defp get_pool(%Request{scheme: scheme, unix_socket: unix_socket}, name)
|
||||
when is_binary(unix_socket) do
|
||||
PoolManager.get_pool(name, {scheme, {:local, unix_socket}, 0})
|
||||
end
|
||||
|
||||
defp get_pool(%Request{scheme: scheme, host: host, port: port}, name) do
|
||||
PoolManager.get_pool(name, {scheme, host, port})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get pool metrics.
|
||||
|
||||
When given a URL or SHP tuple, this returns the metrics list for that specific
|
||||
pool. The number of items in the metrics list depends on the configured
|
||||
`:count` option and each entry will have a `pool_index` going from 1 to
|
||||
`:count`.
|
||||
|
||||
When `:default` is provided, Finch returns the metrics for all pools started
|
||||
from the `:default` configuration. In this case the return value is a map
|
||||
keyed by each pool's `{scheme, host, port}` tuple with the corresponding
|
||||
metrics list as the value.
|
||||
|
||||
The metrics struct depends on the pool scheme defined in the `:protocols`
|
||||
option: `Finch.HTTP1.PoolMetrics` for `:http1` and `Finch.HTTP2.PoolMetrics`
|
||||
for `:http2`. See the documentation for those modules for more details.
|
||||
|
||||
`{:error, :not_found}` is returned in the following scenarios:
|
||||
|
||||
* There is no pool registered for the given Finch instance and URL/SHP.
|
||||
* The pool has `start_pool_metrics?: false` (the default).
|
||||
* `:default` is provided but no pools have been started from the
|
||||
`:default` configuration (or none have metrics enabled).
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Finch.get_pool_status(MyFinch, "https://httpbin.org")
|
||||
{:ok, [
|
||||
%Finch.HTTP1.PoolMetrics{
|
||||
pool_index: 1,
|
||||
pool_size: 50,
|
||||
available_connections: 43,
|
||||
in_use_connections: 7
|
||||
},
|
||||
%Finch.HTTP1.PoolMetrics{
|
||||
pool_index: 2,
|
||||
pool_size: 50,
|
||||
available_connections: 37,
|
||||
in_use_connections: 13
|
||||
}]
|
||||
}
|
||||
|
||||
iex> Finch.get_pool_status(MyFinch, :default)
|
||||
{:ok,
|
||||
%{
|
||||
{:https, "httpbin.org", 443} => [
|
||||
%Finch.HTTP1.PoolMetrics{
|
||||
pool_index: 1,
|
||||
pool_size: 50,
|
||||
available_connections: 43,
|
||||
in_use_connections: 7
|
||||
}
|
||||
]
|
||||
}}
|
||||
"""
|
||||
@spec get_pool_status(name(), url :: String.t() | scheme_host_port() | :default) ::
|
||||
{:ok, pool_metrics()}
|
||||
| {:ok, default_pool_metrics()}
|
||||
| {:error, :not_found}
|
||||
def get_pool_status(finch_name, url) when is_binary(url) do
|
||||
{s, h, p, _, _} = Request.parse_url(url)
|
||||
get_pool_status(finch_name, {s, h, p})
|
||||
end
|
||||
|
||||
def get_pool_status(finch_name, :default) do
|
||||
finch_name
|
||||
|> PoolManager.get_default_shps()
|
||||
|> Enum.reduce(%{}, fn shp, acc ->
|
||||
case get_pool_status(finch_name, shp) do
|
||||
{:ok, metrics} -> Map.put(acc, shp, metrics)
|
||||
{:error, :not_found} -> acc
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
result when result == %{} -> {:error, :not_found}
|
||||
result -> {:ok, result}
|
||||
end
|
||||
end
|
||||
|
||||
def get_pool_status(finch_name, shp) when is_tuple(shp) do
|
||||
case PoolManager.get_pool(finch_name, shp, auto_start?: false) do
|
||||
{_pool, pool_mod} ->
|
||||
pool_mod.get_pool_status(finch_name, shp)
|
||||
|
||||
:not_found ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stops the pool of processes associated with the given scheme, host, port (aka SHP).
|
||||
|
||||
This function can be invoked to manually stop the pool to the given SHP when you know it's not
|
||||
going to be used anymore.
|
||||
|
||||
Note that this function is not safe with respect to concurrent requests. Invoking it while
|
||||
another request to the same SHP is taking place might result in the failure of that request. It
|
||||
is the responsibility of the client to ensure that no request to the same SHP is taking place
|
||||
while this function is being invoked.
|
||||
"""
|
||||
@spec stop_pool(name(), url :: String.t() | scheme_host_port()) :: :ok | {:error, :not_found}
|
||||
def stop_pool(finch_name, url) when is_binary(url) do
|
||||
{s, h, p, _, _} = Request.parse_url(url)
|
||||
stop_pool(finch_name, {s, h, p})
|
||||
end
|
||||
|
||||
def stop_pool(finch_name, shp) when is_tuple(shp) do
|
||||
case PoolManager.all_pool_instances(finch_name, shp) do
|
||||
[] ->
|
||||
{:error, :not_found}
|
||||
|
||||
children ->
|
||||
Enum.each(
|
||||
children,
|
||||
fn {pid, _module} ->
|
||||
DynamicSupervisor.terminate_child(pool_supervisor_name(finch_name), pid)
|
||||
end
|
||||
)
|
||||
|
||||
PoolManager.maybe_remove_default_shp(finch_name, shp)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
21
phoenix/deps/finch/lib/finch/error.ex
Normal file
21
phoenix/deps/finch/lib/finch/error.ex
Normal file
@@ -0,0 +1,21 @@
|
||||
defmodule Finch.Error do
|
||||
@moduledoc """
|
||||
An HTTP error.
|
||||
|
||||
This exception struct is used to represent errors of all sorts for the HTTP/2 protocol.
|
||||
"""
|
||||
|
||||
@type t() :: %__MODULE__{reason: atom()}
|
||||
|
||||
defexception [:reason]
|
||||
|
||||
@impl true
|
||||
def exception(reason) when is_atom(reason) do
|
||||
%__MODULE__{reason: reason}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def message(%__MODULE__{reason: reason}) do
|
||||
"#{reason}"
|
||||
end
|
||||
end
|
||||
372
phoenix/deps/finch/lib/finch/http1/conn.ex
Normal file
372
phoenix/deps/finch/lib/finch/http1/conn.ex
Normal file
@@ -0,0 +1,372 @@
|
||||
defmodule Finch.HTTP1.Conn do
|
||||
@moduledoc false
|
||||
|
||||
alias Finch.SSL
|
||||
alias Finch.Telemetry
|
||||
|
||||
def new(scheme, host, port, opts, parent) do
|
||||
%{
|
||||
scheme: scheme,
|
||||
host: host,
|
||||
port: port,
|
||||
opts: opts.conn_opts,
|
||||
parent: parent,
|
||||
last_checkin: System.monotonic_time(),
|
||||
max_idle_time: opts.conn_max_idle_time,
|
||||
mint: nil
|
||||
}
|
||||
end
|
||||
|
||||
def connect(%{mint: mint} = conn, name) when not is_nil(mint) do
|
||||
meta = %{
|
||||
scheme: conn.scheme,
|
||||
host: conn.host,
|
||||
port: conn.port,
|
||||
name: name
|
||||
}
|
||||
|
||||
Telemetry.event(:reused_connection, %{}, meta)
|
||||
{:ok, conn}
|
||||
end
|
||||
|
||||
def connect(%{mint: nil} = conn, name) do
|
||||
meta = %{
|
||||
scheme: conn.scheme,
|
||||
host: conn.host,
|
||||
port: conn.port,
|
||||
name: name
|
||||
}
|
||||
|
||||
start_time = Telemetry.start(:connect, meta)
|
||||
|
||||
# By default we force HTTP1, but we allow someone to set
|
||||
# custom protocols in case they don't know if a connection
|
||||
# is HTTP1/HTTP2, but they are fine as treating HTTP2
|
||||
# connections has HTTP2.
|
||||
|
||||
conn_opts =
|
||||
conn.opts
|
||||
|> Keyword.put(:mode, :passive)
|
||||
|> Keyword.put_new(:protocols, [:http1])
|
||||
|
||||
case Mint.HTTP.connect(conn.scheme, conn.host, conn.port, conn_opts) do
|
||||
{:ok, mint} ->
|
||||
Telemetry.stop(:connect, start_time, meta)
|
||||
SSL.maybe_log_secrets(conn.scheme, conn_opts, mint)
|
||||
{:ok, %{conn | mint: mint}}
|
||||
|
||||
{:error, error} ->
|
||||
meta = Map.put(meta, :error, error)
|
||||
Telemetry.stop(:connect, start_time, meta)
|
||||
{:error, conn, error}
|
||||
end
|
||||
end
|
||||
|
||||
def transfer(conn, pid) do
|
||||
case Mint.HTTP.controlling_process(conn.mint, pid) do
|
||||
# Mint.HTTP.controlling_process causes a side-effect, but it doesn't actually
|
||||
# change the conn, so we can ignore the value returned above.
|
||||
{:ok, _} -> {:ok, conn}
|
||||
{:error, error} -> {:error, conn, error}
|
||||
end
|
||||
end
|
||||
|
||||
def open?(%{mint: nil}), do: false
|
||||
def open?(%{mint: mint}), do: Mint.HTTP.open?(mint)
|
||||
|
||||
def idle_time(conn, unit \\ :native) do
|
||||
idle_time = System.monotonic_time() - conn.last_checkin
|
||||
|
||||
System.convert_time_unit(idle_time, :native, unit)
|
||||
end
|
||||
|
||||
def reusable?(%{max_idle_time: :infinity}, _idle_time), do: true
|
||||
def reusable?(%{max_idle_time: max_idle_time}, idle_time), do: idle_time <= max_idle_time
|
||||
|
||||
def set_mode(conn, mode) when mode in [:active, :passive] do
|
||||
case Mint.HTTP.set_mode(conn.mint, mode) do
|
||||
{:ok, mint} -> {:ok, %{conn | mint: mint}}
|
||||
_ -> {:error, "Connection is dead"}
|
||||
end
|
||||
end
|
||||
|
||||
def discard(%{mint: nil}, _), do: :unknown
|
||||
|
||||
def discard(conn, message) do
|
||||
case Mint.HTTP.stream(conn.mint, message) do
|
||||
{:ok, mint, _responses} -> {:ok, %{conn | mint: mint}}
|
||||
{:error, _, reason, _} -> {:error, reason}
|
||||
:unknown -> :unknown
|
||||
end
|
||||
end
|
||||
|
||||
def request(%{mint: nil} = conn, _, _, _, _, _, _, _), do: {:error, conn, "Could not connect"}
|
||||
|
||||
def request(conn, req, acc, fun, name, receive_timeout, request_timeout, idle_time) do
|
||||
full_path = Finch.Request.request_path(req)
|
||||
|
||||
metadata = %{request: req, name: name}
|
||||
|
||||
extra_measurements = %{idle_time: idle_time}
|
||||
|
||||
start_time = Telemetry.start(:send, metadata, extra_measurements)
|
||||
|
||||
try do
|
||||
case Mint.HTTP.request(
|
||||
conn.mint,
|
||||
req.method,
|
||||
full_path,
|
||||
req.headers,
|
||||
stream_or_body(req.body)
|
||||
) do
|
||||
{:ok, mint, ref} ->
|
||||
case maybe_stream_request_body(mint, ref, req.body) do
|
||||
{:ok, mint} ->
|
||||
Telemetry.stop(:send, start_time, metadata, extra_measurements)
|
||||
start_time = Telemetry.start(:recv, metadata, extra_measurements)
|
||||
resp_metadata = %{status: nil, headers: [], trailers: []}
|
||||
timeouts = %{receive_timeout: receive_timeout, request_timeout: request_timeout}
|
||||
|
||||
response =
|
||||
receive_response(
|
||||
[],
|
||||
acc,
|
||||
fun,
|
||||
mint,
|
||||
ref,
|
||||
timeouts,
|
||||
:headers,
|
||||
resp_metadata
|
||||
)
|
||||
|
||||
handle_response(response, conn, metadata, start_time, extra_measurements)
|
||||
|
||||
{:error, mint, error} ->
|
||||
handle_request_error(
|
||||
conn,
|
||||
mint,
|
||||
error,
|
||||
acc,
|
||||
metadata,
|
||||
start_time,
|
||||
extra_measurements
|
||||
)
|
||||
end
|
||||
|
||||
{:error, mint, error} ->
|
||||
handle_request_error(conn, mint, error, acc, metadata, start_time, extra_measurements)
|
||||
end
|
||||
catch
|
||||
kind, error ->
|
||||
close(conn)
|
||||
Telemetry.exception(:recv, start_time, kind, error, __STACKTRACE__, metadata)
|
||||
:erlang.raise(kind, error, __STACKTRACE__)
|
||||
end
|
||||
end
|
||||
|
||||
defp stream_or_body({:stream, _}), do: :stream
|
||||
defp stream_or_body(body), do: body
|
||||
|
||||
defp handle_request_error(conn, mint, error, acc, metadata, start_time, extra_measurements) do
|
||||
metadata = Map.put(metadata, :error, error)
|
||||
Telemetry.stop(:send, start_time, metadata, extra_measurements)
|
||||
{:error, %{conn | mint: mint}, error, acc}
|
||||
end
|
||||
|
||||
defp maybe_stream_request_body(mint, ref, {:stream, stream}) do
|
||||
with {:ok, mint} <- stream_request_body(mint, ref, stream) do
|
||||
Mint.HTTP.stream_request_body(mint, ref, :eof)
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_stream_request_body(mint, _, _), do: {:ok, mint}
|
||||
|
||||
defp stream_request_body(mint, ref, stream) do
|
||||
Enum.reduce_while(stream, {:ok, mint}, fn
|
||||
chunk, {:ok, mint} -> {:cont, Mint.HTTP.stream_request_body(mint, ref, chunk)}
|
||||
_chunk, error -> {:halt, error}
|
||||
end)
|
||||
end
|
||||
|
||||
def close(%{mint: nil} = conn), do: conn
|
||||
|
||||
def close(conn) do
|
||||
{:ok, mint} = Mint.HTTP.close(conn.mint)
|
||||
%{conn | mint: mint}
|
||||
end
|
||||
|
||||
defp handle_response(response, conn, metadata, start_time, extra_measurements) do
|
||||
case response do
|
||||
{:ok, mint, acc, resp_metadata} ->
|
||||
metadata = Map.merge(metadata, resp_metadata)
|
||||
Telemetry.stop(:recv, start_time, metadata, extra_measurements)
|
||||
{:ok, %{conn | mint: mint}, acc}
|
||||
|
||||
{:error, mint, error, acc, resp_metadata} ->
|
||||
metadata = Map.merge(metadata, Map.put(resp_metadata, :error, error))
|
||||
Telemetry.stop(:recv, start_time, metadata, extra_measurements)
|
||||
{:error, %{conn | mint: mint}, error, acc}
|
||||
end
|
||||
end
|
||||
|
||||
defp receive_response(
|
||||
entries,
|
||||
acc,
|
||||
fun,
|
||||
mint,
|
||||
ref,
|
||||
timeouts,
|
||||
fields,
|
||||
resp_metadata
|
||||
)
|
||||
|
||||
defp receive_response(
|
||||
[{:done, ref} | _],
|
||||
acc,
|
||||
_fun,
|
||||
mint,
|
||||
ref,
|
||||
_timeouts,
|
||||
_fields,
|
||||
resp_metadata
|
||||
) do
|
||||
{:ok, mint, acc, resp_metadata}
|
||||
end
|
||||
|
||||
defp receive_response(
|
||||
_,
|
||||
acc,
|
||||
_fun,
|
||||
mint,
|
||||
_ref,
|
||||
timeouts,
|
||||
_fields,
|
||||
resp_metadata
|
||||
)
|
||||
when timeouts.request_timeout < 0 do
|
||||
{:ok, mint} = Mint.HTTP.close(mint)
|
||||
{:error, mint, %Mint.TransportError{reason: :timeout}, acc, resp_metadata}
|
||||
end
|
||||
|
||||
defp receive_response(
|
||||
[],
|
||||
acc,
|
||||
fun,
|
||||
mint,
|
||||
ref,
|
||||
timeouts,
|
||||
fields,
|
||||
resp_metadata
|
||||
) do
|
||||
start_time = System.monotonic_time(:millisecond)
|
||||
|
||||
case Mint.HTTP.recv(mint, 0, timeouts.receive_timeout) do
|
||||
{:ok, mint, entries} ->
|
||||
timeouts =
|
||||
if is_integer(timeouts.request_timeout) do
|
||||
elapsed_time = System.monotonic_time(:millisecond) - start_time
|
||||
update_in(timeouts.request_timeout, &(&1 - elapsed_time))
|
||||
else
|
||||
timeouts
|
||||
end
|
||||
|
||||
receive_response(
|
||||
entries,
|
||||
acc,
|
||||
fun,
|
||||
mint,
|
||||
ref,
|
||||
timeouts,
|
||||
fields,
|
||||
resp_metadata
|
||||
)
|
||||
|
||||
{:error, mint, error, _responses} ->
|
||||
{:error, mint, error, acc, resp_metadata}
|
||||
end
|
||||
end
|
||||
|
||||
defp receive_response(
|
||||
[entry | entries],
|
||||
acc,
|
||||
fun,
|
||||
mint,
|
||||
ref,
|
||||
timeouts,
|
||||
fields,
|
||||
resp_metadata
|
||||
) do
|
||||
case entry do
|
||||
{:status, ^ref, value} ->
|
||||
case fun.({:status, value}, acc) do
|
||||
{:cont, acc} ->
|
||||
receive_response(
|
||||
entries,
|
||||
acc,
|
||||
fun,
|
||||
mint,
|
||||
ref,
|
||||
timeouts,
|
||||
fields,
|
||||
%{resp_metadata | status: value}
|
||||
)
|
||||
|
||||
{:halt, acc} ->
|
||||
{:ok, mint} = Mint.HTTP.close(mint)
|
||||
{:ok, mint, acc, resp_metadata}
|
||||
|
||||
other ->
|
||||
raise ArgumentError, "expected {:cont, acc} or {:halt, acc}, got: #{inspect(other)}"
|
||||
end
|
||||
|
||||
{:headers, ^ref, value} ->
|
||||
resp_metadata = update_in(resp_metadata, [fields], &(&1 ++ value))
|
||||
|
||||
case fun.({fields, value}, acc) do
|
||||
{:cont, acc} ->
|
||||
receive_response(
|
||||
entries,
|
||||
acc,
|
||||
fun,
|
||||
mint,
|
||||
ref,
|
||||
timeouts,
|
||||
fields,
|
||||
resp_metadata
|
||||
)
|
||||
|
||||
{:halt, acc} ->
|
||||
{:ok, mint} = Mint.HTTP.close(mint)
|
||||
{:ok, mint, acc, resp_metadata}
|
||||
|
||||
other ->
|
||||
raise ArgumentError, "expected {:cont, acc} or {:halt, acc}, got: #{inspect(other)}"
|
||||
end
|
||||
|
||||
{:data, ^ref, value} ->
|
||||
case fun.({:data, value}, acc) do
|
||||
{:cont, acc} ->
|
||||
receive_response(
|
||||
entries,
|
||||
acc,
|
||||
fun,
|
||||
mint,
|
||||
ref,
|
||||
timeouts,
|
||||
:trailers,
|
||||
resp_metadata
|
||||
)
|
||||
|
||||
{:halt, acc} ->
|
||||
{:ok, mint} = Mint.HTTP.close(mint)
|
||||
{:ok, mint, acc, resp_metadata}
|
||||
|
||||
other ->
|
||||
raise ArgumentError, "expected {:cont, acc} or {:halt, acc}, got: #{inspect(other)}"
|
||||
end
|
||||
|
||||
{:error, ^ref, error} ->
|
||||
{:error, mint, error, acc, resp_metadata}
|
||||
end
|
||||
end
|
||||
end
|
||||
377
phoenix/deps/finch/lib/finch/http1/pool.ex
Normal file
377
phoenix/deps/finch/lib/finch/http1/pool.ex
Normal file
@@ -0,0 +1,377 @@
|
||||
defmodule Finch.HTTP1.Pool do
|
||||
@moduledoc false
|
||||
@behaviour NimblePool
|
||||
@behaviour Finch.Pool
|
||||
|
||||
defmodule State do
|
||||
@moduledoc false
|
||||
defstruct [
|
||||
:registry,
|
||||
:shp,
|
||||
:pool_idx,
|
||||
:metric_ref,
|
||||
:opts,
|
||||
:activity_info
|
||||
]
|
||||
end
|
||||
|
||||
alias Finch.HTTP1.Conn
|
||||
alias Finch.Telemetry
|
||||
alias Finch.HTTP1.PoolMetrics
|
||||
|
||||
def child_spec(opts) do
|
||||
{
|
||||
_shp,
|
||||
_registry_name,
|
||||
_pool_size,
|
||||
_conn_opts,
|
||||
pool_max_idle_time,
|
||||
_start_pool_metrics?,
|
||||
_pool_idx
|
||||
} = opts
|
||||
|
||||
%{
|
||||
id: __MODULE__,
|
||||
start: {__MODULE__, :start_link, [opts]},
|
||||
restart: restart_option(pool_max_idle_time)
|
||||
}
|
||||
end
|
||||
|
||||
def start_link(
|
||||
{shp, registry_name, pool_size, conn_opts, pool_max_idle_time, start_pool_metrics?,
|
||||
pool_idx}
|
||||
) do
|
||||
NimblePool.start_link(
|
||||
worker:
|
||||
{__MODULE__, {registry_name, shp, pool_idx, pool_size, start_pool_metrics?, conn_opts}},
|
||||
pool_size: pool_size,
|
||||
lazy: true,
|
||||
worker_idle_timeout: pool_idle_timeout(pool_max_idle_time)
|
||||
)
|
||||
end
|
||||
|
||||
@impl Finch.Pool
|
||||
def request(pool, req, acc, fun, name, opts) do
|
||||
pool_timeout = Keyword.get(opts, :pool_timeout, 5_000)
|
||||
receive_timeout = Keyword.get(opts, :receive_timeout, 15_000)
|
||||
request_timeout = Keyword.get(opts, :request_timeout, :infinity)
|
||||
|
||||
metadata = %{request: req, pool: pool, name: name}
|
||||
|
||||
start_time = Telemetry.start(:queue, metadata)
|
||||
|
||||
try do
|
||||
NimblePool.checkout!(
|
||||
pool,
|
||||
:checkout,
|
||||
fn from, {state, conn, idle_time} ->
|
||||
Telemetry.stop(:queue, start_time, metadata, %{idle_time: idle_time})
|
||||
|
||||
case Conn.connect(conn, name) do
|
||||
{:ok, conn} ->
|
||||
Conn.request(conn, req, acc, fun, name, receive_timeout, request_timeout, idle_time)
|
||||
|> case do
|
||||
{:ok, conn, acc} ->
|
||||
{{:ok, acc}, transfer_if_open(conn, state, from)}
|
||||
|
||||
{:error, conn, error, acc} ->
|
||||
{{:error, error, acc}, transfer_if_open(conn, state, from)}
|
||||
end
|
||||
|
||||
{:error, conn, error} ->
|
||||
{{:error, error, acc}, transfer_if_open(conn, state, from)}
|
||||
end
|
||||
end,
|
||||
pool_timeout
|
||||
)
|
||||
catch
|
||||
:exit, data ->
|
||||
Telemetry.exception(:queue, start_time, :exit, data, __STACKTRACE__, metadata)
|
||||
|
||||
# Provide helpful error messages for known errors
|
||||
case data do
|
||||
{:timeout, {NimblePool, :checkout, _affected_pids}} ->
|
||||
reraise(
|
||||
"""
|
||||
Finch was unable to provide a connection within the timeout due to excess queuing \
|
||||
for connections. Consider adjusting the pool size, count, timeout or reducing the \
|
||||
rate of requests if it is possible that the downstream service is unable to keep up \
|
||||
with the current rate.
|
||||
""",
|
||||
__STACKTRACE__
|
||||
)
|
||||
|
||||
_ ->
|
||||
exit(data)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl Finch.Pool
|
||||
def async_request(pool, req, name, opts) do
|
||||
owner = self()
|
||||
|
||||
pid =
|
||||
spawn_link(fn ->
|
||||
monitor = Process.monitor(owner)
|
||||
request_ref = {__MODULE__, self()}
|
||||
|
||||
case request(
|
||||
pool,
|
||||
req,
|
||||
{owner, monitor, request_ref},
|
||||
&send_async_response/2,
|
||||
name,
|
||||
opts
|
||||
) do
|
||||
{:ok, _} -> send(owner, {request_ref, :done})
|
||||
{:error, error, _acc} -> send(owner, {request_ref, {:error, error}})
|
||||
end
|
||||
end)
|
||||
|
||||
{__MODULE__, pid}
|
||||
end
|
||||
|
||||
defp send_async_response(response, {owner, monitor, request_ref}) do
|
||||
if process_down?(monitor) do
|
||||
exit(:shutdown)
|
||||
end
|
||||
|
||||
send(owner, {request_ref, response})
|
||||
{:cont, {owner, monitor, request_ref}}
|
||||
end
|
||||
|
||||
defp process_down?(monitor) do
|
||||
receive do
|
||||
{:DOWN, ^monitor, _, _, _} -> true
|
||||
after
|
||||
0 -> false
|
||||
end
|
||||
end
|
||||
|
||||
@impl Finch.Pool
|
||||
def cancel_async_request({_, pid} = _request_ref) do
|
||||
Process.unlink(pid)
|
||||
Process.exit(pid, :shutdown)
|
||||
:ok
|
||||
end
|
||||
|
||||
@impl Finch.Pool
|
||||
def get_pool_status(finch_name, shp) do
|
||||
case Finch.PoolManager.get_pool_count(finch_name, shp) do
|
||||
nil ->
|
||||
{:error, :not_found}
|
||||
|
||||
count ->
|
||||
1..count
|
||||
|> Enum.map(&PoolMetrics.get_pool_status(finch_name, shp, &1))
|
||||
|> Enum.filter(&match?({:ok, _}, &1))
|
||||
|> Enum.map(&elem(&1, 1))
|
||||
|> case do
|
||||
[] -> {:error, :not_found}
|
||||
result -> {:ok, result}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl NimblePool
|
||||
def init_pool({registry, shp, pool_idx, pool_size, start_pool_metrics?, opts}) do
|
||||
{:ok, metric_ref} =
|
||||
if start_pool_metrics?,
|
||||
do: PoolMetrics.init(registry, shp, pool_idx, pool_size),
|
||||
else: {:ok, nil}
|
||||
|
||||
# Register our pool with our module name as the key. This allows the caller
|
||||
# to determine the correct pool module to use to make the request
|
||||
{:ok, _} = Registry.register(registry, shp, __MODULE__)
|
||||
|
||||
acitivity_info =
|
||||
if opts[:pool_max_idle_time] != :infinity, do: init_activity_info(), else: nil
|
||||
|
||||
state = %__MODULE__.State{
|
||||
registry: registry,
|
||||
shp: shp,
|
||||
pool_idx: pool_idx,
|
||||
metric_ref: metric_ref,
|
||||
opts: opts,
|
||||
activity_info: acitivity_info
|
||||
}
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@impl NimblePool
|
||||
def init_worker(%__MODULE__.State{shp: {scheme, host, port}, opts: opts} = pool_state) do
|
||||
{:ok, Conn.new(scheme, host, port, opts, self()), pool_state}
|
||||
end
|
||||
|
||||
@impl NimblePool
|
||||
def handle_checkout(:checkout, _, %{mint: nil} = conn, %__MODULE__.State{} = pool_state) do
|
||||
idle_time = System.monotonic_time() - conn.last_checkin
|
||||
PoolMetrics.maybe_add(pool_state.metric_ref, in_use_connections: 1)
|
||||
{:ok, {:fresh, conn, idle_time}, conn, pool_state}
|
||||
end
|
||||
|
||||
def handle_checkout(:checkout, _from, conn, %__MODULE__.State{} = pool_state) do
|
||||
idle_time = System.monotonic_time() - conn.last_checkin
|
||||
|
||||
%__MODULE__.State{
|
||||
shp: {scheme, host, port},
|
||||
metric_ref: metric_ref
|
||||
} = pool_state
|
||||
|
||||
with true <- Conn.reusable?(conn, idle_time),
|
||||
{:ok, conn} <- Conn.set_mode(conn, :passive) do
|
||||
PoolMetrics.maybe_add(metric_ref, in_use_connections: 1)
|
||||
{:ok, {:reuse, conn, idle_time}, conn, update_activity_info(:checkout, pool_state)}
|
||||
else
|
||||
false ->
|
||||
meta = %{
|
||||
scheme: scheme,
|
||||
host: host,
|
||||
port: port
|
||||
}
|
||||
|
||||
# Deprecated, remember to delete when we remove the :max_idle_time pool config option!
|
||||
Telemetry.event(:max_idle_time_exceeded, %{idle_time: idle_time}, meta)
|
||||
|
||||
Telemetry.event(:conn_max_idle_time_exceeded, %{idle_time: idle_time}, meta)
|
||||
|
||||
{:remove, :closed, pool_state}
|
||||
|
||||
_ ->
|
||||
{:remove, :closed, pool_state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl NimblePool
|
||||
def handle_checkin(checkin, _from, _old_conn, %__MODULE__.State{} = pool_state) do
|
||||
%__MODULE__.State{metric_ref: metric_ref} = pool_state
|
||||
PoolMetrics.maybe_add(metric_ref, in_use_connections: -1)
|
||||
|
||||
with {:ok, conn} <- checkin,
|
||||
{:ok, conn} <- Conn.set_mode(conn, :active) do
|
||||
{
|
||||
:ok,
|
||||
%{conn | last_checkin: System.monotonic_time()},
|
||||
update_activity_info(:checkin, pool_state)
|
||||
}
|
||||
else
|
||||
_ ->
|
||||
{:remove, :closed, update_activity_info(:checkin, pool_state)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl NimblePool
|
||||
def handle_update(new_conn, _old_conn, %__MODULE__.State{} = pool_state) do
|
||||
{:ok, new_conn, pool_state}
|
||||
end
|
||||
|
||||
@impl NimblePool
|
||||
def handle_info(message, conn) do
|
||||
case Conn.discard(conn, message) do
|
||||
{:ok, conn} -> {:ok, conn}
|
||||
:unknown -> {:ok, conn}
|
||||
{:error, _error} -> {:remove, :closed}
|
||||
end
|
||||
end
|
||||
|
||||
@impl NimblePool
|
||||
def handle_ping(conn, %__MODULE__.State{} = pool_state) do
|
||||
%__MODULE__.State{
|
||||
shp: {scheme, host, port},
|
||||
opts: opts,
|
||||
activity_info: activity_info
|
||||
} = pool_state
|
||||
|
||||
max_idle_time = Map.get(opts, :pool_max_idle_time, :infinity)
|
||||
now = System.monotonic_time(:millisecond)
|
||||
diff_from_last_checkout = now - activity_info.last_checkout_ts
|
||||
|
||||
is_idle? = diff_from_last_checkout > max_idle_time
|
||||
max_idle_time_configured? = is_number(max_idle_time)
|
||||
any_connection_in_use? = activity_info.in_use_count > 0
|
||||
|
||||
cond do
|
||||
not max_idle_time_configured? ->
|
||||
{:ok, conn}
|
||||
|
||||
any_connection_in_use? ->
|
||||
{:ok, conn}
|
||||
|
||||
is_idle? ->
|
||||
meta = %{
|
||||
scheme: scheme,
|
||||
host: host,
|
||||
port: port
|
||||
}
|
||||
|
||||
Telemetry.event(:pool_max_idle_time_exceeded, %{}, meta)
|
||||
{:stop, :idle_timeout}
|
||||
|
||||
true ->
|
||||
{:ok, conn}
|
||||
end
|
||||
end
|
||||
|
||||
@impl NimblePool
|
||||
# On terminate, effectively close it.
|
||||
# This will succeed even if it was already closed or if we don't own it.
|
||||
def terminate_worker(_reason, conn, %__MODULE__.State{} = pool_state) do
|
||||
Conn.close(conn)
|
||||
{:ok, pool_state}
|
||||
end
|
||||
|
||||
@impl NimblePool
|
||||
def handle_cancelled(:checked_out, %__MODULE__.State{} = pool_state) do
|
||||
%__MODULE__.State{metric_ref: metric_ref} = pool_state
|
||||
PoolMetrics.maybe_add(metric_ref, in_use_connections: -1)
|
||||
:ok
|
||||
end
|
||||
|
||||
def handle_cancelled(:queued, _pool_state), do: :ok
|
||||
|
||||
defp transfer_if_open(conn, state, {pid, _} = from) do
|
||||
if Conn.open?(conn) do
|
||||
if state == :fresh do
|
||||
NimblePool.update(from, conn)
|
||||
|
||||
case Conn.transfer(conn, pid) do
|
||||
{:ok, conn} -> {:ok, conn}
|
||||
{:error, _, _} -> :closed
|
||||
end
|
||||
else
|
||||
{:ok, conn}
|
||||
end
|
||||
else
|
||||
:closed
|
||||
end
|
||||
end
|
||||
|
||||
defp restart_option(:infinity), do: :permanent
|
||||
defp restart_option(_pool_max_idle_time), do: :transient
|
||||
|
||||
defp pool_idle_timeout(:infinity), do: nil
|
||||
defp pool_idle_timeout(pool_max_idle_time), do: pool_max_idle_time
|
||||
|
||||
defp init_activity_info() do
|
||||
%{in_use_count: 0, last_checkout_ts: System.monotonic_time(:millisecond)}
|
||||
end
|
||||
|
||||
defp update_activity_info(
|
||||
_checkout_or_checkin,
|
||||
%__MODULE__.State{activity_info: nil} = pool_state
|
||||
) do
|
||||
pool_state
|
||||
end
|
||||
|
||||
defp update_activity_info(:checkout, %__MODULE__.State{} = pool_state) do
|
||||
update_in(pool_state.activity_info, fn %{in_use_count: count} ->
|
||||
%{in_use_count: count + 1, last_checkout_ts: System.monotonic_time(:millisecond)}
|
||||
end)
|
||||
end
|
||||
|
||||
defp update_activity_info(:checkin, %__MODULE__.State{} = pool_state) do
|
||||
update_in(pool_state.activity_info.in_use_count, &max(&1 - 1, 0))
|
||||
end
|
||||
end
|
||||
81
phoenix/deps/finch/lib/finch/http1/pool_metrics.ex
Normal file
81
phoenix/deps/finch/lib/finch/http1/pool_metrics.ex
Normal file
@@ -0,0 +1,81 @@
|
||||
defmodule Finch.HTTP1.PoolMetrics do
|
||||
@moduledoc """
|
||||
HTTP1 Pool metrics.
|
||||
|
||||
Available metrics:
|
||||
|
||||
* `:pool_index` - Index of the pool
|
||||
* `:pool_size` - Total number of connections of the pool
|
||||
* `:available_connections` - Number of available connections
|
||||
* `:in_use_connections` - Number of connections currently in use
|
||||
|
||||
Caveats:
|
||||
|
||||
* A given number X of `available_connections` does not mean that currently
|
||||
exists X connections to the server sitting on the pool. Because Finch uses
|
||||
a lazy strategy for workers initialization, every pool starts with it's
|
||||
size as available connections even if they are not started yet. In practice
|
||||
this means that `available_connections` may be connections sitting on the pool
|
||||
or available space on the pool for a new one if required.
|
||||
|
||||
"""
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
defstruct [
|
||||
:pool_index,
|
||||
:pool_size,
|
||||
:available_connections,
|
||||
:in_use_connections
|
||||
]
|
||||
|
||||
@atomic_idx [
|
||||
pool_idx: 1,
|
||||
pool_size: 2,
|
||||
in_use_connections: 3
|
||||
]
|
||||
|
||||
def init(registry, shp, pool_idx, pool_size) do
|
||||
ref = :atomics.new(length(@atomic_idx), [])
|
||||
:atomics.add(ref, @atomic_idx[:pool_idx], pool_idx)
|
||||
:atomics.add(ref, @atomic_idx[:pool_size], pool_size)
|
||||
|
||||
:persistent_term.put({__MODULE__, registry, shp, pool_idx}, ref)
|
||||
{:ok, ref}
|
||||
end
|
||||
|
||||
def maybe_add(nil, _metrics_list), do: :ok
|
||||
|
||||
def maybe_add(ref, metrics_list) do
|
||||
Enum.each(metrics_list, fn {metric_name, val} ->
|
||||
:atomics.add(ref, @atomic_idx[metric_name], val)
|
||||
end)
|
||||
end
|
||||
|
||||
def get_pool_status(name, shp, pool_idx) do
|
||||
{__MODULE__, name, shp, pool_idx}
|
||||
|> :persistent_term.get(nil)
|
||||
|> get_pool_status()
|
||||
end
|
||||
|
||||
def get_pool_status(nil), do: {:error, :not_found}
|
||||
|
||||
def get_pool_status(ref) do
|
||||
%{
|
||||
pool_idx: pool_idx,
|
||||
pool_size: pool_size,
|
||||
in_use_connections: in_use_connections
|
||||
} =
|
||||
@atomic_idx
|
||||
|> Enum.map(fn {k, idx} -> {k, :atomics.get(ref, idx)} end)
|
||||
|> Map.new()
|
||||
|
||||
result = %__MODULE__{
|
||||
pool_index: pool_idx,
|
||||
pool_size: pool_size,
|
||||
available_connections: pool_size - in_use_connections,
|
||||
in_use_connections: in_use_connections
|
||||
}
|
||||
|
||||
{:ok, result}
|
||||
end
|
||||
end
|
||||
844
phoenix/deps/finch/lib/finch/http2/pool.ex
Normal file
844
phoenix/deps/finch/lib/finch/http2/pool.ex
Normal file
@@ -0,0 +1,844 @@
|
||||
defmodule Finch.HTTP2.Pool do
|
||||
@moduledoc false
|
||||
|
||||
@behaviour :gen_statem
|
||||
@behaviour Finch.Pool
|
||||
|
||||
alias Mint.HTTP2
|
||||
alias Mint.HTTPError
|
||||
alias Finch.Error
|
||||
alias Finch.Telemetry
|
||||
alias Finch.SSL
|
||||
alias Finch.HTTP2.RequestStream
|
||||
|
||||
alias Finch.HTTP2.PoolMetrics
|
||||
|
||||
require Logger
|
||||
|
||||
@default_receive_timeout 15_000
|
||||
|
||||
@impl true
|
||||
def callback_mode(), do: [:state_functions, :state_enter]
|
||||
|
||||
def child_spec(opts) do
|
||||
%{
|
||||
id: __MODULE__,
|
||||
start: {__MODULE__, :start_link, [opts]}
|
||||
}
|
||||
end
|
||||
|
||||
# Call the pool with the request. The pool will multiplex multiple requests
|
||||
# and stream the result set back to the calling process using `send`
|
||||
@impl Finch.Pool
|
||||
def request(pool, request, acc, fun, name, opts) do
|
||||
opts = Keyword.put_new(opts, :receive_timeout, @default_receive_timeout)
|
||||
timeout = opts[:receive_timeout]
|
||||
request_ref = make_request_ref(pool)
|
||||
|
||||
case :gen_statem.call(pool, {:request, request_ref, request, opts}) do
|
||||
{:ok, recv_start} ->
|
||||
monitor = Process.monitor(pool)
|
||||
# If the timeout is an integer, we add a fail-safe "after" clause that fires
|
||||
# after a timeout that is double the original timeout (min 2000ms). This means
|
||||
# that if there are no bugs in our code, then the normal :request_timeout is
|
||||
# returned, but otherwise we have a way to escape this code, raise an error, and
|
||||
# get the process unstuck.
|
||||
fail_safe_timeout = if is_integer(timeout), do: max(2000, timeout * 2), else: :infinity
|
||||
|
||||
try do
|
||||
response_waiting_loop(acc, fun, request_ref, monitor, fail_safe_timeout, :headers)
|
||||
catch
|
||||
kind, error ->
|
||||
metadata = %{request: request, name: name}
|
||||
Telemetry.exception(:recv, recv_start, kind, error, __STACKTRACE__, metadata)
|
||||
|
||||
:ok = :gen_statem.call(pool, {:cancel, request_ref})
|
||||
clean_responses(request_ref)
|
||||
Process.demonitor(monitor)
|
||||
|
||||
:erlang.raise(kind, error, __STACKTRACE__)
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error, acc}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Finch.Pool
|
||||
def async_request(pool, req, _name, opts) do
|
||||
opts = Keyword.put_new(opts, :receive_timeout, @default_receive_timeout)
|
||||
request_ref = make_request_ref(pool)
|
||||
|
||||
:ok = :gen_statem.cast(pool, {:async_request, self(), request_ref, req, opts})
|
||||
|
||||
request_ref
|
||||
end
|
||||
|
||||
@impl Finch.Pool
|
||||
def cancel_async_request({_, {pool, _}} = request_ref) do
|
||||
:ok = :gen_statem.call(pool, {:cancel, request_ref})
|
||||
clean_responses(request_ref)
|
||||
end
|
||||
|
||||
@impl Finch.Pool
|
||||
def get_pool_status(finch_name, shp) do
|
||||
case Finch.PoolManager.get_pool_count(finch_name, shp) do
|
||||
nil ->
|
||||
{:error, :not_found}
|
||||
|
||||
count ->
|
||||
1..count
|
||||
|> Enum.map(&PoolMetrics.get_pool_status(finch_name, shp, &1))
|
||||
|> Enum.filter(&match?({:ok, _}, &1))
|
||||
|> Enum.map(&elem(&1, 1))
|
||||
|> case do
|
||||
[] -> {:error, :not_found}
|
||||
result -> {:ok, result}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp make_request_ref(pool) do
|
||||
{__MODULE__, {pool, make_ref()}}
|
||||
end
|
||||
|
||||
defp response_waiting_loop(acc, fun, request_ref, monitor_ref, fail_safe_timeout, fields)
|
||||
|
||||
defp response_waiting_loop(acc, fun, request_ref, monitor_ref, fail_safe_timeout, fields) do
|
||||
receive do
|
||||
{^request_ref, {:status, value}} ->
|
||||
case fun.({:status, value}, acc) do
|
||||
{:cont, acc} ->
|
||||
response_waiting_loop(
|
||||
acc,
|
||||
fun,
|
||||
request_ref,
|
||||
monitor_ref,
|
||||
fail_safe_timeout,
|
||||
fields
|
||||
)
|
||||
|
||||
{:halt, acc} ->
|
||||
cancel_async_request(request_ref)
|
||||
Process.demonitor(monitor_ref)
|
||||
{:ok, acc}
|
||||
|
||||
other ->
|
||||
raise ArgumentError, "expected {:cont, acc} or {:halt, acc}, got: #{inspect(other)}"
|
||||
end
|
||||
|
||||
{^request_ref, {:headers, value}} ->
|
||||
case fun.({fields, value}, acc) do
|
||||
{:cont, acc} ->
|
||||
response_waiting_loop(
|
||||
acc,
|
||||
fun,
|
||||
request_ref,
|
||||
monitor_ref,
|
||||
fail_safe_timeout,
|
||||
fields
|
||||
)
|
||||
|
||||
{:halt, acc} ->
|
||||
cancel_async_request(request_ref)
|
||||
Process.demonitor(monitor_ref)
|
||||
{:ok, acc}
|
||||
|
||||
other ->
|
||||
raise ArgumentError, "expected {:cont, acc} or {:halt, acc}, got: #{inspect(other)}"
|
||||
end
|
||||
|
||||
{^request_ref, {:data, value}} ->
|
||||
case fun.({:data, value}, acc) do
|
||||
{:cont, acc} ->
|
||||
response_waiting_loop(
|
||||
acc,
|
||||
fun,
|
||||
request_ref,
|
||||
monitor_ref,
|
||||
fail_safe_timeout,
|
||||
:trailers
|
||||
)
|
||||
|
||||
{:halt, acc} ->
|
||||
cancel_async_request(request_ref)
|
||||
Process.demonitor(monitor_ref)
|
||||
{:ok, acc}
|
||||
|
||||
other ->
|
||||
raise ArgumentError, "expected {:cont, acc} or {:halt, acc}, got: #{inspect(other)}"
|
||||
end
|
||||
|
||||
{^request_ref, :done} ->
|
||||
Process.demonitor(monitor_ref)
|
||||
{:ok, acc}
|
||||
|
||||
{^request_ref, {:error, error}} ->
|
||||
Process.demonitor(monitor_ref)
|
||||
{:error, error, acc}
|
||||
|
||||
{:DOWN, ^monitor_ref, _, _, _} ->
|
||||
{:error, :connection_process_went_down}
|
||||
after
|
||||
fail_safe_timeout ->
|
||||
Process.demonitor(monitor_ref)
|
||||
|
||||
raise "no response was received even after waiting #{fail_safe_timeout}ms. " <>
|
||||
"This is likely a bug in Finch, but we're raising so that your system doesn't " <>
|
||||
"get stuck in an infinite receive."
|
||||
end
|
||||
end
|
||||
|
||||
defp clean_responses(request_ref) do
|
||||
receive do
|
||||
{^request_ref, _} -> clean_responses(request_ref)
|
||||
after
|
||||
0 -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
def start_link({_shp, _finch_name, _pool_config, _start_pool_metrics?, _pool_idx} = opts) do
|
||||
:gen_statem.start_link(__MODULE__, opts, [])
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init({{scheme, host, port} = shp, registry, pool_opts, start_pool_metrics?, pool_idx}) do
|
||||
{:ok, metrics_ref} =
|
||||
if start_pool_metrics?,
|
||||
do: PoolMetrics.init(registry, shp, pool_idx),
|
||||
else: {:ok, nil}
|
||||
|
||||
{:ok, _} = Registry.register(registry, shp, __MODULE__)
|
||||
|
||||
data = %{
|
||||
conn: nil,
|
||||
finch_name: registry,
|
||||
scheme: scheme,
|
||||
host: host,
|
||||
port: port,
|
||||
pool_idx: pool_idx,
|
||||
requests: %{},
|
||||
refs: %{},
|
||||
requests_by_pid: %{},
|
||||
backoff_base: 500,
|
||||
backoff_max: 10_000,
|
||||
connect_opts: pool_opts[:conn_opts] || [],
|
||||
metrics_ref: metrics_ref
|
||||
}
|
||||
|
||||
{:ok, :disconnected, data, {:next_event, :internal, {:connect, 0}}}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def disconnected(event, content, data)
|
||||
|
||||
def disconnected(:enter, :disconnected, _) do
|
||||
:keep_state_and_data
|
||||
end
|
||||
|
||||
# When entering a disconnected state we need to fail all of the pending
|
||||
# requests
|
||||
def disconnected(:enter, _, data) do
|
||||
:ok =
|
||||
Enum.each(data.requests, fn {_ref, request} ->
|
||||
send(
|
||||
request.from_pid,
|
||||
{request.request_ref, {:error, Error.exception(:connection_closed)}}
|
||||
)
|
||||
end)
|
||||
|
||||
# It's possible that we're entering this state before we are alerted of the
|
||||
# fact that the socket is closed. This most often happens if we're in a read
|
||||
# only state but have no pending requests to wait on. In this case we can just
|
||||
# close the connection and throw it away.
|
||||
if data.conn do
|
||||
HTTP2.close(data.conn)
|
||||
end
|
||||
|
||||
data =
|
||||
data
|
||||
|> Map.put(:requests, %{})
|
||||
|> Map.put(:conn, nil)
|
||||
|
||||
actions = [{{:timeout, :reconnect}, data.backoff_base, 1}]
|
||||
|
||||
{:keep_state, data, actions}
|
||||
end
|
||||
|
||||
def disconnected(:internal, {:connect, failure_count}, data) do
|
||||
metadata = %{
|
||||
scheme: data.scheme,
|
||||
host: data.host,
|
||||
port: data.port,
|
||||
name: data.finch_name
|
||||
}
|
||||
|
||||
start = Telemetry.start(:connect, metadata)
|
||||
|
||||
case HTTP2.connect(data.scheme, data.host, data.port, data.connect_opts) do
|
||||
{:ok, conn} ->
|
||||
Telemetry.stop(:connect, start, metadata)
|
||||
SSL.maybe_log_secrets(data.scheme, data.connect_opts, conn)
|
||||
data = %{data | conn: conn}
|
||||
{:next_state, :connected, data}
|
||||
|
||||
{:error, error} ->
|
||||
metadata = Map.put(metadata, :error, error)
|
||||
Telemetry.stop(:connect, start, metadata)
|
||||
|
||||
Logger.warning([
|
||||
"Failed to connect to #{data.scheme}://#{data.host}:#{data.port}: ",
|
||||
Exception.message(error)
|
||||
])
|
||||
|
||||
delay = backoff(data.backoff_base, data.backoff_max, failure_count)
|
||||
{:keep_state_and_data, {{:timeout, :reconnect}, delay, failure_count + 1}}
|
||||
end
|
||||
end
|
||||
|
||||
# Capture timeout after trying to reconnect. Immediately attempt to reconnect
|
||||
# to the upstream server
|
||||
def disconnected({:timeout, :reconnect}, failure_count, _data) do
|
||||
{:keep_state_and_data, {:next_event, :internal, {:connect, failure_count}}}
|
||||
end
|
||||
|
||||
# Immediately fail a request if we're disconnected
|
||||
def disconnected({:call, from}, {:request, _, _, _}, _data) do
|
||||
{:keep_state_and_data, {:reply, from, {:error, Error.exception(:disconnected)}}}
|
||||
end
|
||||
|
||||
# Ignore cancel requests if we are disconnected
|
||||
def disconnected({:call, from}, {:cancel, _request_ref}, _data) do
|
||||
{:keep_state_and_data, {:reply, from, {:error, Error.exception(:disconnected)}}}
|
||||
end
|
||||
|
||||
# Immediately fail a request if we're disconnected
|
||||
def disconnected(:cast, {:async_request, pid, request_ref, _, _}, _data) do
|
||||
send(pid, {request_ref, {:error, Error.exception(:disconnected)}})
|
||||
:keep_state_and_data
|
||||
end
|
||||
|
||||
# We cancel all request timeouts as soon as we enter the :disconnected state, but
|
||||
# some timeouts might fire while changing states, so we need to handle them here.
|
||||
# Since we replied to all pending requests when entering the :disconnected state,
|
||||
# we can just do nothing here.
|
||||
def disconnected({:timeout, {:request_timeout, _ref}}, _content, _data) do
|
||||
:keep_state_and_data
|
||||
end
|
||||
|
||||
# Its possible that we can receive an info message telling us that a socket
|
||||
# has been closed. This happens after we enter a disconnected state from a
|
||||
# read_only state but we don't have any requests that are open. We've already
|
||||
# closed the connection and thrown it away at this point so we can just retain
|
||||
# our current state.
|
||||
def disconnected(:info, _message, _data) do
|
||||
:keep_state_and_data
|
||||
end
|
||||
|
||||
@doc false
|
||||
def connected(event, content, data)
|
||||
|
||||
def connected(:enter, _old_state, _data) do
|
||||
:keep_state_and_data
|
||||
end
|
||||
|
||||
# Issue request to the upstream server. We store a ref to the request so we
|
||||
# know who to respond to when we've completed everything
|
||||
def connected({:call, {from_pid, _from_ref} = from}, {:request, request_ref, req, opts}, data) do
|
||||
send_request(from, from_pid, request_ref, req, opts, data)
|
||||
end
|
||||
|
||||
def connected({:call, from}, {:cancel, request_ref}, data) do
|
||||
data = cancel_request(data, request_ref)
|
||||
{:keep_state, data, {:reply, from, :ok}}
|
||||
end
|
||||
|
||||
def connected(:cast, {:async_request, pid, request_ref, req, opts}, data) do
|
||||
if is_nil(data.requests_by_pid[pid]) do
|
||||
Process.monitor(pid)
|
||||
end
|
||||
|
||||
send_request(nil, pid, request_ref, req, opts, data)
|
||||
end
|
||||
|
||||
def connected(:info, {:DOWN, _, :process, pid, _}, data) do
|
||||
{:keep_state, cancel_requests(data, pid)}
|
||||
end
|
||||
|
||||
def connected(:info, message, data) do
|
||||
case HTTP2.stream(data.conn, message) do
|
||||
{:ok, conn, responses} ->
|
||||
data = put_in(data.conn, conn)
|
||||
{data, response_actions} = handle_responses(data, responses)
|
||||
|
||||
cond do
|
||||
HTTP2.open?(data.conn, :write) ->
|
||||
data = continue_requests(data)
|
||||
{:keep_state, data, response_actions}
|
||||
|
||||
HTTP2.open?(data.conn, :read) && Enum.any?(data.requests) ->
|
||||
{:next_state, :connected_read_only, data, response_actions}
|
||||
|
||||
true ->
|
||||
{:next_state, :disconnected, data, response_actions}
|
||||
end
|
||||
|
||||
{:error, conn, error, responses} ->
|
||||
Logger.error([
|
||||
"Received error from server #{data.scheme}:#{data.host}:#{data.port}: ",
|
||||
Exception.message(error)
|
||||
])
|
||||
|
||||
data = put_in(data.conn, conn)
|
||||
{data, actions} = handle_responses(data, responses)
|
||||
|
||||
if HTTP2.open?(conn, :read) && Enum.any?(data.requests) do
|
||||
{:next_state, :connected_read_only, data, actions}
|
||||
else
|
||||
{:next_state, :disconnected, data, actions}
|
||||
end
|
||||
|
||||
:unknown ->
|
||||
Logger.warning(["Received unknown message: ", inspect(message)])
|
||||
:keep_state_and_data
|
||||
end
|
||||
end
|
||||
|
||||
def connected({:timeout, {:request_timeout, ref}}, _content, data) do
|
||||
with {:pop, {request, data}} when not is_nil(request) <- {:pop, pop_request(data, ref)},
|
||||
{:ok, conn} <- HTTP2.cancel_request(data.conn, ref) do
|
||||
data = put_in(data.conn, conn)
|
||||
send(request.from_pid, {request.request_ref, {:error, Error.exception(:request_timeout)}})
|
||||
{:keep_state, data}
|
||||
else
|
||||
{:error, conn, _error} ->
|
||||
data = put_in(data.conn, conn)
|
||||
|
||||
cond do
|
||||
HTTP2.open?(conn, :write) ->
|
||||
{:keep_state, data}
|
||||
|
||||
# Don't bother entering read only mode if we don't have any pending requests.
|
||||
HTTP2.open?(conn, :read) && Enum.any?(data.requests) ->
|
||||
{:next_state, :connected_read_only, data}
|
||||
|
||||
true ->
|
||||
{:next_state, :disconnected, data}
|
||||
end
|
||||
|
||||
# The timer might have fired while we were receiving :done/:error for this
|
||||
# request, so we don't have the request stored anymore but we still get the
|
||||
# timer event. In those cases, we do nothing.
|
||||
{:pop, {nil, _data}} ->
|
||||
:keep_state_and_data
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def connected_read_only(event, content, data)
|
||||
|
||||
def connected_read_only(:enter, _old_state, data) do
|
||||
data =
|
||||
Enum.reduce(data.requests, data, fn
|
||||
# request is awaiting a response and should stay in state
|
||||
{_ref, %{stream: %{status: :done}}}, data ->
|
||||
data
|
||||
|
||||
# request is still sending data and should be discarded
|
||||
{ref, %{stream: %{status: :streaming}} = request}, data ->
|
||||
{^request, data} = pop_request(data, ref)
|
||||
reply(request, {:error, Error.exception(:read_only)})
|
||||
data
|
||||
end)
|
||||
|
||||
{:keep_state, data}
|
||||
end
|
||||
|
||||
# If we're in a read only state then respond with an error immediately
|
||||
def connected_read_only({:call, from}, {:request, _, _, _}, _) do
|
||||
{:keep_state_and_data, {:reply, from, {:error, Error.exception(:read_only)}}}
|
||||
end
|
||||
|
||||
def connected_read_only({:call, from}, {:cancel, request_ref}, data) do
|
||||
data = cancel_request(data, request_ref)
|
||||
{:keep_state, data, {:reply, from, :ok}}
|
||||
end
|
||||
|
||||
def connected_read_only(:cast, {:async_request, pid, request_ref, _, _}, _) do
|
||||
send(pid, {request_ref, {:error, Error.exception(:read_only)}})
|
||||
:keep_state_and_data
|
||||
end
|
||||
|
||||
def connected_read_only(:info, {:DOWN, _, :process, pid, _}, data) do
|
||||
{:keep_state, cancel_requests(data, pid)}
|
||||
end
|
||||
|
||||
def connected_read_only(:info, message, data) do
|
||||
case HTTP2.stream(data.conn, message) do
|
||||
{:ok, conn, responses} ->
|
||||
data = put_in(data.conn, conn)
|
||||
{data, actions} = handle_responses(data, responses)
|
||||
|
||||
# If the connection is still open for reading and we have pending requests
|
||||
# to receive, we should try to wait for the responses. Otherwise enter
|
||||
# the disconnected state so we can try to re-establish a connection.
|
||||
if HTTP2.open?(conn, :read) && Enum.any?(data.requests) do
|
||||
{:keep_state, data, actions}
|
||||
else
|
||||
{:next_state, :disconnected, data, actions}
|
||||
end
|
||||
|
||||
{:error, conn, error, responses} ->
|
||||
Logger.error([
|
||||
"Received error from server #{data.scheme}://#{data.host}:#{data.port}: ",
|
||||
Exception.message(error)
|
||||
])
|
||||
|
||||
data = put_in(data.conn, conn)
|
||||
{data, actions} = handle_responses(data, responses)
|
||||
|
||||
# Same as above, if we're still waiting on responses, we should stay in
|
||||
# this state. Otherwise, we should enter the disconnected state and try
|
||||
# to re-establish a connection.
|
||||
if HTTP2.open?(conn, :read) && Enum.any?(data.requests) do
|
||||
{:keep_state, data, actions}
|
||||
else
|
||||
{:next_state, :disconnected, data, actions}
|
||||
end
|
||||
|
||||
:unknown ->
|
||||
Logger.warning(["Received unknown message: ", inspect(message)])
|
||||
:keep_state_and_data
|
||||
end
|
||||
end
|
||||
|
||||
# In this state, we don't need to call HTTP2.cancel_request/2 since the connection
|
||||
# is closed for writing, so we can't tell the server to cancel the request anymore.
|
||||
def connected_read_only({:timeout, {:request_timeout, ref}}, _content, data) do
|
||||
# We might get a request timeout that fired in the moment when we received the
|
||||
# whole request, so we don't have the request in the state but we get the
|
||||
# timer event anyways. In those cases, we don't do anything.
|
||||
{request, data} = pop_request(data, ref)
|
||||
|
||||
# Its possible that the request doesn't exist so we guard against that here.
|
||||
if request != nil do
|
||||
send(request.from_pid, {request.request_ref, {:error, Error.exception(:request_timeout)}})
|
||||
end
|
||||
|
||||
# If we're out of requests then we should enter the disconnected state.
|
||||
# Otherwise wait for the remaining responses.
|
||||
if Enum.empty?(data.requests) do
|
||||
{:next_state, :disconnected, data}
|
||||
else
|
||||
{:keep_state, data}
|
||||
end
|
||||
end
|
||||
|
||||
defp send_request(from, from_pid, request_ref, req, opts, data) do
|
||||
telemetry_metadata = %{request: req, name: data.finch_name}
|
||||
|
||||
request = %{
|
||||
stream: RequestStream.new(req.body),
|
||||
from: from,
|
||||
from_pid: from_pid,
|
||||
request_ref: request_ref,
|
||||
telemetry: %{
|
||||
metadata: telemetry_metadata,
|
||||
send: Telemetry.start(:send, telemetry_metadata)
|
||||
}
|
||||
}
|
||||
|
||||
body = if req.body == nil, do: nil, else: :stream
|
||||
|
||||
data
|
||||
|> start_request(req.method, Finch.Request.request_path(req), req.headers, body)
|
||||
|> stream_request(request, opts)
|
||||
end
|
||||
|
||||
defp start_request(data, method, path, headers, body) do
|
||||
case HTTP2.request(data.conn, method, path, headers, body) do
|
||||
{:ok, conn, ref} ->
|
||||
{:ok, put_in(data.conn, conn), ref}
|
||||
|
||||
{:error, conn, reason} ->
|
||||
{:error, put_in(data.conn, conn), reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp stream_request({:ok, data, ref}, request, opts) do
|
||||
data = put_request(data, ref, request)
|
||||
|
||||
case continue_request(data, ref, request) do
|
||||
{:ok, data} ->
|
||||
# Set a timeout to close the request after a given timeout
|
||||
request_timeout = {{:timeout, {:request_timeout, ref}}, opts[:receive_timeout], nil}
|
||||
|
||||
{:keep_state, data, [request_timeout]}
|
||||
|
||||
error ->
|
||||
stream_request(error, request, opts)
|
||||
end
|
||||
end
|
||||
|
||||
defp stream_request({:error, data, %HTTPError{reason: :closed_for_writing}}, request, _opts) do
|
||||
reply(request, {:error, Error.exception(:read_only)})
|
||||
|
||||
if HTTP2.open?(data.conn, :read) && Enum.any?(data.requests) do
|
||||
{:next_state, :connected_read_only, data}
|
||||
else
|
||||
{:next_state, :disconnected, data}
|
||||
end
|
||||
end
|
||||
|
||||
defp stream_request({:error, data, error}, request, _opts) do
|
||||
reply(request, {:error, error})
|
||||
|
||||
if HTTP2.open?(data.conn) do
|
||||
{:keep_state, data}
|
||||
else
|
||||
{:next_state, :disconnected, data}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_responses(data, responses) do
|
||||
Enum.reduce(responses, {data, _actions = []}, fn response, {data, actions} ->
|
||||
handle_response(data, response, actions)
|
||||
end)
|
||||
end
|
||||
|
||||
defp handle_response(data, {kind, ref, value}, actions)
|
||||
when kind in [:status, :headers] do
|
||||
data =
|
||||
if request = data.requests[ref] do
|
||||
send(request.from_pid, {request.request_ref, {kind, value}})
|
||||
request = put_in(request.telemetry.metadata[kind], value)
|
||||
put_in(data.requests[ref], request)
|
||||
else
|
||||
data
|
||||
end
|
||||
|
||||
{data, actions}
|
||||
end
|
||||
|
||||
defp handle_response(data, {:data, ref, value}, actions) do
|
||||
if request = data.requests[ref] do
|
||||
send(request.from_pid, {request.request_ref, {:data, value}})
|
||||
end
|
||||
|
||||
{data, actions}
|
||||
end
|
||||
|
||||
defp handle_response(data, {:done, ref}, actions) do
|
||||
{request, data} = pop_request(data, ref)
|
||||
|
||||
if request do
|
||||
send(request.from_pid, {request.request_ref, :done})
|
||||
Telemetry.stop(:recv, request.telemetry.recv, request.telemetry.metadata)
|
||||
end
|
||||
|
||||
{data, [cancel_request_timeout_action(ref) | actions]}
|
||||
end
|
||||
|
||||
defp handle_response(data, {:error, ref, error}, actions) do
|
||||
{request, data} = pop_request(data, ref)
|
||||
|
||||
if request do
|
||||
send(request.from_pid, {request.request_ref, {:error, error}})
|
||||
|
||||
Telemetry.stop(
|
||||
:recv,
|
||||
request.telemetry.recv,
|
||||
Map.put(request.telemetry.metadata, :error, error)
|
||||
)
|
||||
end
|
||||
|
||||
{data, [cancel_request_timeout_action(ref) | actions]}
|
||||
end
|
||||
|
||||
defp cancel_request_timeout_action(request_ref) do
|
||||
# By setting the timeout to :infinity, we cancel this timeout as per
|
||||
# gen_statem documentation.
|
||||
{{:timeout, {:request_timeout, request_ref}}, :infinity, nil}
|
||||
end
|
||||
|
||||
# Exponential backoff with jitter
|
||||
# The backoff algorithm optimizes for tight bounds on completing a request successfully.
|
||||
# It does this by first calculating an exponential backoff factor based on the
|
||||
# number of retries that have been performed. It then multiplies this factor against the
|
||||
# base delay. The total maximum delay is found by taking the minimum of either the calculated delay
|
||||
# or the maximum delay specified. This creates an upper bound on the maximum delay
|
||||
# we can see.
|
||||
#
|
||||
# In order to find the actual delay value we take a random number between 0 and
|
||||
# the maximum delay based on a uniform distribution. This randomness ensures that
|
||||
# our retried requests don't "harmonize" making it harder for the downstream
|
||||
# service to heal.
|
||||
defp backoff(base_backoff, max_backoff, failure_count) do
|
||||
factor = :math.pow(2, failure_count)
|
||||
max_sleep = trunc(min(max_backoff, base_backoff * factor))
|
||||
:rand.uniform(max_sleep)
|
||||
end
|
||||
|
||||
# this is also a wrapper (Mint.HTTP2.stream_request_body/3)
|
||||
defp stream_request_body(data, ref, body) do
|
||||
case HTTP2.stream_request_body(data.conn, ref, body) do
|
||||
{:ok, conn} -> {:ok, put_in(data.conn, conn)}
|
||||
{:error, conn, reason} -> {:error, put_in(data.conn, conn), reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp stream_chunks(data, ref, body, %{stream: %{status: :done}}) do
|
||||
with {:ok, data} <- stream_request_body(data, ref, body) do
|
||||
stream_request_body(data, ref, :eof)
|
||||
end
|
||||
end
|
||||
|
||||
defp stream_chunks(data, ref, body, _), do: stream_request_body(data, ref, body)
|
||||
|
||||
defp continue_requests(data) do
|
||||
Enum.reduce(data.requests, data, fn {ref, request}, data ->
|
||||
with true <- request.stream.status == :streaming,
|
||||
true <- HTTP2.open?(data.conn, :write),
|
||||
{:ok, data} <- continue_request(data, ref, request) do
|
||||
data
|
||||
else
|
||||
false ->
|
||||
data
|
||||
|
||||
{:error, data, %HTTPError{reason: :closed_for_writing}} ->
|
||||
reply(request, {:error, Error.exception(:read_only)})
|
||||
data
|
||||
|
||||
{:error, data, reason} ->
|
||||
reply(request, {:error, reason})
|
||||
data
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp continue_request(data, ref, request) do
|
||||
with :streaming <- request.stream.status,
|
||||
window = smallest_window(data.conn, ref),
|
||||
{stream, chunks} = RequestStream.next_chunk(request.stream, window),
|
||||
request = %{request | stream: stream},
|
||||
{:ok, data} <- stream_chunks(data, ref, chunks, request) do
|
||||
{:ok, complete_request_if_done(data, ref, request)}
|
||||
else
|
||||
:done ->
|
||||
{:ok, complete_request_if_done(data, ref, request)}
|
||||
|
||||
{:error, data, reason} ->
|
||||
{_from, data} = pop_request(data, ref)
|
||||
|
||||
{:error, data, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp complete_request_if_done(data, ref, %{stream: %{status: :done}} = request) do
|
||||
%{from: from, telemetry: telemetry} = request
|
||||
Telemetry.stop(:send, telemetry.send, telemetry.metadata)
|
||||
recv_start = Telemetry.start(:recv, telemetry.metadata)
|
||||
request = put_in(request.telemetry[:recv], recv_start)
|
||||
|
||||
if from do
|
||||
reply(request, {:ok, recv_start})
|
||||
end
|
||||
|
||||
put_in(data.requests[ref], request)
|
||||
end
|
||||
|
||||
defp complete_request_if_done(data, ref, request) do
|
||||
put_in(data.requests[ref], request)
|
||||
end
|
||||
|
||||
defp smallest_window(conn, ref) do
|
||||
min(
|
||||
HTTP2.get_window_size(conn, :connection),
|
||||
HTTP2.get_window_size(conn, {:request, ref})
|
||||
)
|
||||
end
|
||||
|
||||
defp cancel_requests(data, pid) do
|
||||
if request_refs = data.requests_by_pid[pid] do
|
||||
Enum.reduce(request_refs, data, fn request_ref, data ->
|
||||
cancel_request(data, request_ref)
|
||||
end)
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
defp cancel_request(data, request_ref) do
|
||||
# If the Mint ref isn't present, it was removed because the request
|
||||
# already completed and there's nothing to cancel.
|
||||
if ref = data.refs[request_ref] do
|
||||
conn =
|
||||
case HTTP2.cancel_request(data.conn, ref) do
|
||||
{:ok, conn} -> conn
|
||||
{:error, conn, _error} -> conn
|
||||
end
|
||||
|
||||
data = put_in(data.conn, conn)
|
||||
{_from, data} = pop_request(data, ref)
|
||||
data
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
defp put_request(data, ref, request) do
|
||||
PoolMetrics.maybe_add(data.metrics_ref, in_flight_requests: 1)
|
||||
|
||||
data
|
||||
|> put_in([:requests, ref], request)
|
||||
|> put_in([:refs, request.request_ref], ref)
|
||||
|> put_pid(request.from_pid, request.request_ref)
|
||||
end
|
||||
|
||||
defp pop_request(data, ref) do
|
||||
PoolMetrics.maybe_add(data.metrics_ref, in_flight_requests: -1)
|
||||
|
||||
case pop_in(data.requests[ref]) do
|
||||
{nil, data} ->
|
||||
{nil, data}
|
||||
|
||||
{request, data} ->
|
||||
{_ref, data} =
|
||||
data
|
||||
|> pop_pid(request.from_pid, request.request_ref)
|
||||
|> pop_in([:refs, request.request_ref])
|
||||
|
||||
{request, data}
|
||||
end
|
||||
end
|
||||
|
||||
defp put_pid(data, pid, request_ref) do
|
||||
update_in(data.requests_by_pid, fn requests_by_pid ->
|
||||
Map.update(requests_by_pid, pid, MapSet.new([request_ref]), &MapSet.put(&1, request_ref))
|
||||
end)
|
||||
end
|
||||
|
||||
defp pop_pid(data, pid, request_ref) do
|
||||
update_in(data.requests_by_pid, fn requests_by_pid ->
|
||||
requests =
|
||||
requests_by_pid
|
||||
|> Map.get(pid, MapSet.new())
|
||||
|> MapSet.delete(request_ref)
|
||||
|
||||
if Enum.empty?(requests) do
|
||||
Map.delete(requests_by_pid, pid)
|
||||
else
|
||||
Map.put(requests_by_pid, pid, requests)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp reply(%{from: nil, from_pid: pid, request_ref: request_ref}, reply) do
|
||||
send(pid, {request_ref, reply})
|
||||
:ok
|
||||
end
|
||||
|
||||
defp reply(%{from: from}, reply) do
|
||||
:gen_statem.reply(from, reply)
|
||||
end
|
||||
end
|
||||
68
phoenix/deps/finch/lib/finch/http2/pool_metrics.ex
Normal file
68
phoenix/deps/finch/lib/finch/http2/pool_metrics.ex
Normal file
@@ -0,0 +1,68 @@
|
||||
defmodule Finch.HTTP2.PoolMetrics do
|
||||
@moduledoc """
|
||||
HTTP2 Pool metrics.
|
||||
|
||||
Available metrics:
|
||||
|
||||
* `:pool_index` - Index of the pool
|
||||
* `:in_flight_requests` - Number of requests currently on the connection
|
||||
|
||||
Caveats:
|
||||
|
||||
* HTTP2 pools have only one connection and leverage the multiplex nature
|
||||
of the protocol. That's why we only keep the in flight requests, representing
|
||||
the number of streams currently running on the connection.
|
||||
"""
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
defstruct [
|
||||
:pool_index,
|
||||
:in_flight_requests
|
||||
]
|
||||
|
||||
@atomic_idx [
|
||||
pool_idx: 1,
|
||||
in_flight_requests: 2
|
||||
]
|
||||
|
||||
def init(finch_name, shp, pool_idx) do
|
||||
ref = :atomics.new(length(@atomic_idx), [])
|
||||
:atomics.put(ref, @atomic_idx[:pool_idx], pool_idx)
|
||||
|
||||
:persistent_term.put({__MODULE__, finch_name, shp, pool_idx}, ref)
|
||||
{:ok, ref}
|
||||
end
|
||||
|
||||
def maybe_add(nil, _metrics_list), do: :ok
|
||||
|
||||
def maybe_add(ref, metrics_list) do
|
||||
Enum.each(metrics_list, fn {metric_name, val} ->
|
||||
:atomics.add(ref, @atomic_idx[metric_name], val)
|
||||
end)
|
||||
end
|
||||
|
||||
def get_pool_status(name, shp, pool_idx) do
|
||||
{__MODULE__, name, shp, pool_idx}
|
||||
|> :persistent_term.get(nil)
|
||||
|> get_pool_status()
|
||||
end
|
||||
|
||||
def get_pool_status(nil), do: {:error, :not_found}
|
||||
|
||||
def get_pool_status(ref) do
|
||||
%{
|
||||
pool_idx: pool_idx,
|
||||
in_flight_requests: in_flight_requests
|
||||
} =
|
||||
@atomic_idx
|
||||
|> Enum.map(fn {k, idx} -> {k, :atomics.get(ref, idx)} end)
|
||||
|> Map.new()
|
||||
|
||||
result = %__MODULE__{
|
||||
pool_index: pool_idx,
|
||||
in_flight_requests: in_flight_requests
|
||||
}
|
||||
|
||||
{:ok, result}
|
||||
end
|
||||
end
|
||||
83
phoenix/deps/finch/lib/finch/http2/request_stream.ex
Normal file
83
phoenix/deps/finch/lib/finch/http2/request_stream.ex
Normal file
@@ -0,0 +1,83 @@
|
||||
defmodule Finch.HTTP2.RequestStream do
|
||||
@moduledoc false
|
||||
|
||||
defstruct [:body, :status, :buffer, :continuation]
|
||||
|
||||
def new(body) do
|
||||
enumerable =
|
||||
case body do
|
||||
{:stream, stream} -> Stream.map(stream, &with_byte_size/1)
|
||||
nil -> [with_byte_size("")]
|
||||
io_data -> [with_byte_size(io_data)]
|
||||
end
|
||||
|
||||
reducer = &reduce_with_suspend/2
|
||||
|
||||
%__MODULE__{
|
||||
body: body,
|
||||
status: if(body == nil, do: :done, else: :streaming),
|
||||
buffer: <<>>,
|
||||
continuation: &Enumerable.reduce(enumerable, &1, reducer)
|
||||
}
|
||||
end
|
||||
|
||||
defp with_byte_size(binary) when is_binary(binary), do: {binary, byte_size(binary)}
|
||||
defp with_byte_size(io_data), do: io_data |> IO.iodata_to_binary() |> with_byte_size()
|
||||
|
||||
defp reduce_with_suspend(
|
||||
{message, message_size},
|
||||
{message_buffer, message_buffer_size, window}
|
||||
)
|
||||
when message_size + message_buffer_size > window do
|
||||
{:suspend,
|
||||
{[{message, message_size} | message_buffer], message_size + message_buffer_size, window}}
|
||||
end
|
||||
|
||||
defp reduce_with_suspend(
|
||||
{message, message_size},
|
||||
{message_buffer, message_buffer_size, window}
|
||||
) do
|
||||
{:cont, {[message | message_buffer], message_size + message_buffer_size, window}}
|
||||
end
|
||||
|
||||
# gets the next chunk of data that will fit into the given window size
|
||||
def next_chunk(request, window)
|
||||
|
||||
# when the buffer is empty, continue reducing the stream
|
||||
def next_chunk(%__MODULE__{buffer: <<>>} = request, window) do
|
||||
continue_reduce(request, {[], 0, window})
|
||||
end
|
||||
|
||||
def next_chunk(%__MODULE__{buffer: buffer} = request, window) do
|
||||
case buffer do
|
||||
<<bytes_to_send::binary-size(window), rest::binary>> ->
|
||||
# when the buffer contains more bytes than a window, send as much of the
|
||||
# buffer as we can
|
||||
{put_in(request.buffer, rest), bytes_to_send}
|
||||
|
||||
_ ->
|
||||
# when the buffer can fit in the windows, continue reducing using the buffer
|
||||
# as the accumulator
|
||||
continue_reduce(request, {[buffer], byte_size(buffer), window})
|
||||
end
|
||||
end
|
||||
|
||||
defp continue_reduce(request, acc) do
|
||||
case request.continuation.({:cont, acc}) do
|
||||
{finished, {messages, _size, _window}} when finished in [:done, :halted] ->
|
||||
{put_in(request.status, :done), Enum.reverse(messages)}
|
||||
|
||||
{:suspended,
|
||||
{[{overload_message, overload_message_size} | messages_that_fit], total_size, window_size},
|
||||
next_continuation} ->
|
||||
fittable_size = window_size - (total_size - overload_message_size)
|
||||
|
||||
<<fittable_binary::binary-size(fittable_size), overload_binary::binary>> =
|
||||
overload_message
|
||||
|
||||
request = %{request | continuation: next_continuation, buffer: overload_binary}
|
||||
|
||||
{request, Enum.reverse([fittable_binary | messages_that_fit])}
|
||||
end
|
||||
end
|
||||
end
|
||||
32
phoenix/deps/finch/lib/finch/pool.ex
Normal file
32
phoenix/deps/finch/lib/finch/pool.ex
Normal file
@@ -0,0 +1,32 @@
|
||||
defmodule Finch.Pool do
|
||||
@moduledoc false
|
||||
# Defines a behaviour that both http1 and http2 pools need to implement.
|
||||
|
||||
@type request_ref :: {pool_mod :: module(), cancel_ref :: term()}
|
||||
|
||||
@callback request(
|
||||
pid(),
|
||||
Finch.Request.t(),
|
||||
acc,
|
||||
Finch.stream(acc),
|
||||
Finch.name(),
|
||||
list()
|
||||
) :: {:ok, acc} | {:error, term(), acc}
|
||||
when acc: term()
|
||||
|
||||
@callback async_request(
|
||||
pid(),
|
||||
Finch.Request.t(),
|
||||
Finch.name(),
|
||||
list()
|
||||
) :: request_ref()
|
||||
|
||||
@callback cancel_async_request(request_ref()) :: :ok
|
||||
|
||||
@callback get_pool_status(
|
||||
finch_name :: atom(),
|
||||
{schema :: atom(), host :: String.t(), port :: integer()}
|
||||
) :: {:ok, list(map)} | {:error, :not_found}
|
||||
|
||||
defguard is_request_ref(ref) when tuple_size(ref) == 2 and is_atom(elem(ref, 0))
|
||||
end
|
||||
208
phoenix/deps/finch/lib/finch/pool_manager.ex
Normal file
208
phoenix/deps/finch/lib/finch/pool_manager.ex
Normal file
@@ -0,0 +1,208 @@
|
||||
defmodule Finch.PoolManager do
|
||||
@moduledoc false
|
||||
use GenServer
|
||||
|
||||
@mint_tls_opts [
|
||||
:cacertfile,
|
||||
:cacerts,
|
||||
:ciphers,
|
||||
:depth,
|
||||
:eccs,
|
||||
:hibernate_after,
|
||||
:partial_chain,
|
||||
:reuse_sessions,
|
||||
:secure_renegotiate,
|
||||
:server_name_indication,
|
||||
:signature_algs,
|
||||
:signature_algs_cert,
|
||||
:supported_groups,
|
||||
:verify,
|
||||
:verify_fun,
|
||||
:versions
|
||||
]
|
||||
|
||||
@default_conn_hostname "localhost"
|
||||
|
||||
def start_link(config) do
|
||||
GenServer.start_link(__MODULE__, config, name: config.manager_name)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(config) do
|
||||
if config.default_pool_config.start_pool_metrics? do
|
||||
:ets.new(default_shp_table(config.registry_name), [
|
||||
:set,
|
||||
:public,
|
||||
:named_table
|
||||
])
|
||||
end
|
||||
|
||||
Enum.each(config.pools, fn {shp, _} ->
|
||||
do_start_pools(shp, config)
|
||||
end)
|
||||
|
||||
{:ok, config}
|
||||
end
|
||||
|
||||
def get_pool(registry_name, {_scheme, _host, _port} = key, opts \\ []) do
|
||||
case lookup_pool(registry_name, key) do
|
||||
{pid, _} = pool when is_pid(pid) ->
|
||||
pool
|
||||
|
||||
:none ->
|
||||
if Keyword.get(opts, :auto_start?, true),
|
||||
do: start_pools(registry_name, key),
|
||||
else: :not_found
|
||||
end
|
||||
end
|
||||
|
||||
def lookup_pool(registry, key) do
|
||||
case all_pool_instances(registry, key) do
|
||||
[] ->
|
||||
:none
|
||||
|
||||
[pool] ->
|
||||
pool
|
||||
|
||||
pools ->
|
||||
# TODO implement alternative strategies
|
||||
Enum.random(pools)
|
||||
end
|
||||
end
|
||||
|
||||
def all_pool_instances(registry, key), do: Registry.lookup(registry, key)
|
||||
|
||||
def start_pools(registry_name, shp) do
|
||||
{:ok, config} = Registry.meta(registry_name, :config)
|
||||
GenServer.call(config.manager_name, {:start_pools, shp})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:start_pools, shp}, _from, state) do
|
||||
reply =
|
||||
case lookup_pool(state.registry_name, shp) do
|
||||
:none -> do_start_pools(shp, state)
|
||||
pool -> pool
|
||||
end
|
||||
|
||||
{:reply, reply, state}
|
||||
end
|
||||
|
||||
defp do_start_pools(shp, config) do
|
||||
pool_config = pool_config(config, shp)
|
||||
|
||||
if pool_config.start_pool_metrics? do
|
||||
maybe_track_default_shp(config, shp)
|
||||
put_pool_count(config, shp, pool_config.count)
|
||||
end
|
||||
|
||||
Enum.map(1..pool_config.count, fn pool_idx ->
|
||||
pool_args = pool_args(shp, config, pool_config, pool_idx)
|
||||
# Choose pool type here...
|
||||
{:ok, pid} =
|
||||
DynamicSupervisor.start_child(config.supervisor_name, {pool_config.mod, pool_args})
|
||||
|
||||
{pid, pool_config.mod}
|
||||
end)
|
||||
|> hd()
|
||||
end
|
||||
|
||||
defp put_pool_count(%{registry_name: name}, shp, val),
|
||||
do: :persistent_term.put({__MODULE__, :pool_count, name, shp}, val)
|
||||
|
||||
def get_pool_count(finch_name, shp),
|
||||
do: :persistent_term.get({__MODULE__, :pool_count, finch_name, shp}, nil)
|
||||
|
||||
defp maybe_track_default_shp(%{pools: pools, registry_name: name}, shp) do
|
||||
if Map.has_key?(pools, shp),
|
||||
do: :ok,
|
||||
else: add_default_shp(name, shp)
|
||||
end
|
||||
|
||||
defp default_shp_table(name), do: :"#{name}.default_shp_table"
|
||||
|
||||
defp add_default_shp(name, shp) do
|
||||
true =
|
||||
name
|
||||
|> default_shp_table()
|
||||
|> :ets.insert({shp})
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def get_default_shps(name) do
|
||||
tname = default_shp_table(name)
|
||||
|
||||
if :ets.whereis(tname) == :undefined do
|
||||
[]
|
||||
else
|
||||
tname
|
||||
|> :ets.tab2list()
|
||||
|> Enum.map(fn {shp} -> shp end)
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_remove_default_shp(name, shp) do
|
||||
tname = default_shp_table(name)
|
||||
|
||||
if :ets.whereis(tname) == :undefined do
|
||||
:ok
|
||||
else
|
||||
true = :ets.delete(tname, shp)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp pool_config(%{pools: config, default_pool_config: default}, shp) do
|
||||
config
|
||||
|> Map.get(shp, default)
|
||||
|> maybe_drop_tls_options(shp)
|
||||
|> maybe_add_hostname(shp)
|
||||
end
|
||||
|
||||
# Drop TLS options from :conn_opts for default pools with :http scheme,
|
||||
# otherwise you will get :badarg error from :gen_tcp
|
||||
defp maybe_drop_tls_options(config, {:http, _, _} = _shp) when is_map(config) do
|
||||
with conn_opts when is_list(conn_opts) <- config[:conn_opts],
|
||||
trns_opts when is_list(trns_opts) <- conn_opts[:transport_opts] do
|
||||
trns_opts = Keyword.drop(trns_opts, @mint_tls_opts)
|
||||
conn_opts = Keyword.put(conn_opts, :transport_opts, trns_opts)
|
||||
Map.put(config, :conn_opts, conn_opts)
|
||||
else
|
||||
_ -> config
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_drop_tls_options(config, _), do: config
|
||||
|
||||
# Hostname is required when the address is not a URL (binary) so we need to specify
|
||||
# a default value in case the configuration does not specify one.
|
||||
defp maybe_add_hostname(config, {_scheme, {:local, _path}, _port} = _shp) when is_map(config) do
|
||||
conn_opts =
|
||||
config |> Map.get(:conn_opts, []) |> Keyword.put_new(:hostname, @default_conn_hostname)
|
||||
|
||||
Map.put(config, :conn_opts, conn_opts)
|
||||
end
|
||||
|
||||
defp maybe_add_hostname(config, _), do: config
|
||||
|
||||
defp pool_args(shp, config, %{mod: Finch.HTTP1.Pool} = pool_config, pool_idx),
|
||||
do: {
|
||||
shp,
|
||||
config.registry_name,
|
||||
pool_config.size,
|
||||
pool_config,
|
||||
pool_config.pool_max_idle_time,
|
||||
pool_config.start_pool_metrics?,
|
||||
pool_idx
|
||||
}
|
||||
|
||||
defp pool_args(shp, config, %{mod: Finch.HTTP2.Pool} = pool_config, pool_idx),
|
||||
do: {
|
||||
shp,
|
||||
config.registry_name,
|
||||
pool_config,
|
||||
pool_config.start_pool_metrics?,
|
||||
pool_idx
|
||||
}
|
||||
end
|
||||
157
phoenix/deps/finch/lib/finch/request.ex
Normal file
157
phoenix/deps/finch/lib/finch/request.ex
Normal file
@@ -0,0 +1,157 @@
|
||||
defmodule Finch.Request do
|
||||
@moduledoc """
|
||||
A request struct.
|
||||
"""
|
||||
|
||||
@enforce_keys [:scheme, :host, :port, :method, :path, :headers, :body, :query]
|
||||
defstruct [
|
||||
:scheme,
|
||||
:host,
|
||||
:port,
|
||||
:method,
|
||||
:path,
|
||||
:headers,
|
||||
:body,
|
||||
:query,
|
||||
:unix_socket,
|
||||
private: %{}
|
||||
]
|
||||
|
||||
@atom_methods [
|
||||
:get,
|
||||
:post,
|
||||
:put,
|
||||
:patch,
|
||||
:delete,
|
||||
:head,
|
||||
:options
|
||||
]
|
||||
@methods [
|
||||
"GET",
|
||||
"POST",
|
||||
"PUT",
|
||||
"PATCH",
|
||||
"DELETE",
|
||||
"HEAD",
|
||||
"OPTIONS"
|
||||
]
|
||||
@atom_to_method Enum.zip(@atom_methods, @methods) |> Enum.into(%{})
|
||||
|
||||
@typedoc """
|
||||
An HTTP request method represented as an `atom()` or a `String.t()`.
|
||||
|
||||
The following atom methods are supported: `#{Enum.map_join(@atom_methods, "`, `", &inspect/1)}`.
|
||||
You can use any arbitrary method by providing it as a `String.t()`.
|
||||
"""
|
||||
@type method() :: :get | :post | :head | :patch | :delete | :options | :put | String.t()
|
||||
|
||||
@typedoc """
|
||||
A Uniform Resource Locator, the address of a resource on the Web.
|
||||
"""
|
||||
@type url() :: String.t() | URI.t()
|
||||
|
||||
@typedoc """
|
||||
Request headers.
|
||||
"""
|
||||
@type headers() :: Mint.Types.headers()
|
||||
|
||||
@typedoc """
|
||||
Optional request body.
|
||||
"""
|
||||
@type body() :: iodata() | {:stream, Enumerable.t()} | nil
|
||||
|
||||
@type private_metadata() :: %{optional(atom()) => term()}
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
scheme: Mint.Types.scheme(),
|
||||
host: String.t() | nil,
|
||||
port: :inet.port_number(),
|
||||
method: String.t(),
|
||||
path: String.t(),
|
||||
headers: headers(),
|
||||
body: body(),
|
||||
query: String.t() | nil,
|
||||
unix_socket: String.t() | nil,
|
||||
private: private_metadata()
|
||||
}
|
||||
|
||||
@doc """
|
||||
Sets a new **private** key and value in the request metadata. This storage is meant to be used by libraries
|
||||
and frameworks to inject information about the request that needs to be retrieved later on, for example,
|
||||
from handlers that consume `Finch.Telemetry` events.
|
||||
"""
|
||||
@spec put_private(t(), key :: atom(), value :: term()) :: t()
|
||||
def put_private(%__MODULE__{private: private} = request, key, value) when is_atom(key) do
|
||||
%{request | private: Map.put(private, key, value)}
|
||||
end
|
||||
|
||||
def put_private(%__MODULE__{}, key, _) do
|
||||
raise ArgumentError, """
|
||||
got unsupported private metadata key #{inspect(key)}
|
||||
only atoms are allowed as keys of the `:private` field.
|
||||
"""
|
||||
end
|
||||
|
||||
@doc false
|
||||
def request_path(%{path: path, query: nil}), do: path
|
||||
def request_path(%{path: path, query: ""}), do: path
|
||||
def request_path(%{path: path, query: query}), do: "#{path}?#{query}"
|
||||
|
||||
@doc false
|
||||
def build(method, url, headers, body, opts) do
|
||||
unix_socket = Keyword.get(opts, :unix_socket)
|
||||
{scheme, host, port, path, query} = parse_url(url)
|
||||
|
||||
%Finch.Request{
|
||||
scheme: scheme,
|
||||
host: host,
|
||||
port: port,
|
||||
method: build_method(method),
|
||||
path: path,
|
||||
headers: headers,
|
||||
body: body,
|
||||
query: query,
|
||||
unix_socket: unix_socket
|
||||
}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def parse_url(url) when is_binary(url) do
|
||||
url |> URI.parse() |> parse_url()
|
||||
end
|
||||
|
||||
def parse_url(%URI{} = parsed_uri) do
|
||||
normalized_path = parsed_uri.path || "/"
|
||||
|
||||
scheme =
|
||||
case parsed_uri.scheme do
|
||||
"https" ->
|
||||
:https
|
||||
|
||||
"http" ->
|
||||
:http
|
||||
|
||||
nil ->
|
||||
raise ArgumentError, "scheme is required for url: #{URI.to_string(parsed_uri)}"
|
||||
|
||||
scheme ->
|
||||
raise ArgumentError,
|
||||
"invalid scheme \"#{scheme}\" for url: #{URI.to_string(parsed_uri)}"
|
||||
end
|
||||
|
||||
{scheme, parsed_uri.host, parsed_uri.port, normalized_path, parsed_uri.query}
|
||||
end
|
||||
|
||||
defp build_method(method) when is_binary(method), do: method
|
||||
defp build_method(method) when method in @atom_methods, do: @atom_to_method[method]
|
||||
|
||||
defp build_method(method) do
|
||||
supported = Enum.map_join(@atom_methods, ", ", &inspect/1)
|
||||
|
||||
raise ArgumentError, """
|
||||
got unsupported atom method #{inspect(method)}.
|
||||
Only the following methods can be provided as atoms: #{supported}.
|
||||
Otherwise you must pass a binary.
|
||||
"""
|
||||
end
|
||||
end
|
||||
21
phoenix/deps/finch/lib/finch/response.ex
Normal file
21
phoenix/deps/finch/lib/finch/response.ex
Normal file
@@ -0,0 +1,21 @@
|
||||
defmodule Finch.Response do
|
||||
@moduledoc """
|
||||
A response to a request.
|
||||
"""
|
||||
|
||||
alias __MODULE__
|
||||
|
||||
defstruct [
|
||||
:status,
|
||||
body: "",
|
||||
headers: [],
|
||||
trailers: []
|
||||
]
|
||||
|
||||
@type t :: %Response{
|
||||
status: Mint.Types.status(),
|
||||
body: binary(),
|
||||
headers: Mint.Types.headers(),
|
||||
trailers: Mint.Types.headers()
|
||||
}
|
||||
end
|
||||
26
phoenix/deps/finch/lib/finch/ssl.ex
Normal file
26
phoenix/deps/finch/lib/finch/ssl.ex
Normal file
@@ -0,0 +1,26 @@
|
||||
defmodule Finch.SSL do
|
||||
@moduledoc false
|
||||
|
||||
alias Mint.HTTP
|
||||
|
||||
def maybe_log_secrets(:https, conn_opts, mint) do
|
||||
ssl_key_log_file_device = Keyword.get(conn_opts, :ssl_key_log_file_device)
|
||||
|
||||
if ssl_key_log_file_device != nil do
|
||||
socket = HTTP.get_socket(mint)
|
||||
# Note: not every ssl library version returns information for :keylog. By using `with` here,
|
||||
# anything other than the expected return value is silently ignored.
|
||||
with {:ok, [{:keylog, keylog_items}]} <- :ssl.connection_information(socket, [:keylog]) do
|
||||
for keylog_item <- keylog_items do
|
||||
:ok = IO.puts(ssl_key_log_file_device, keylog_item)
|
||||
end
|
||||
end
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_log_secrets(_scheme, _conn_opts, _mint) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
318
phoenix/deps/finch/lib/finch/telemetry.ex
Normal file
318
phoenix/deps/finch/lib/finch/telemetry.ex
Normal file
@@ -0,0 +1,318 @@
|
||||
defmodule Finch.Telemetry do
|
||||
@moduledoc """
|
||||
Telemetry integration.
|
||||
|
||||
Unless specified, all times are in `:native` units.
|
||||
|
||||
Finch executes the following events:
|
||||
|
||||
### Request Start
|
||||
|
||||
`[:finch, :request, :start]` - Executed when `Finch.request/3` or `Finch.stream/5` is called.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:system_time` - The system time.
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:name` - The name of the Finch instance.
|
||||
* `:request` - The request (`Finch.Request`).
|
||||
|
||||
### Request Stop
|
||||
|
||||
`[:finch, :request, :stop]` - Executed after `Finch.request/3` or `Finch.stream/5` ended.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:duration` - Time taken from the request start event.
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:name` - The name of the Finch instance.
|
||||
* `:request` - The request (`Finch.Request`).
|
||||
* `:result` - The result of the operation. In case of `Finch.stream/5` this is
|
||||
`{:ok, acc} | {:error, Exception.t()}`, where `acc` is the accumulator result of the
|
||||
reducer passed in `Finch.stream/5`. In case of `Finch.request/3` this is
|
||||
`{:ok, Finch.Response.t()} | {:error, Exception.t()}`.
|
||||
|
||||
### Request Exception
|
||||
|
||||
`[:finch, :request, :exception]` - Executed when an exception occurs while executing
|
||||
`Finch.request/3` or `Finch.stream/5`.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:duration` - The time it took since the start before raising the exception.
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:name` - The name of the Finch instance.
|
||||
* `:request` - The request (`Finch.Request`).
|
||||
* `:kind` - The type of exception.
|
||||
* `:reason` - Error description or error data.
|
||||
* `:stacktrace` - The stacktrace.
|
||||
|
||||
### Queue Start
|
||||
|
||||
`[:finch, :queue, :start]` - Executed before checking out an HTTP1 connection from the pool.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:system_time` - The system time.
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:name` - The name of the Finch instance.
|
||||
* `:pool` - The pool's PID.
|
||||
* `:request` - The request (`Finch.Request`).
|
||||
|
||||
### Queue Stop
|
||||
|
||||
`[:finch, :queue, :stop]` - Executed after an HTTP1 connection is retrieved from the pool.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:duration` - Time taken to check out a pool connection.
|
||||
* `:idle_time` - Elapsed time since the connection was last checked in or initialized.
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:name` - The name of the Finch instance.
|
||||
* `:pool` - The pool's PID.
|
||||
* `:request` - The request (`Finch.Request`).
|
||||
|
||||
### Queue Exception
|
||||
|
||||
`[:finch, :queue, :exception]` - Executed if checking out an HTTP1 connection throws an exception.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:duration` - The time it took since queue start event before raising an exception.
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:name` - The name of the Finch instance.
|
||||
* `:request` - The request (`Finch.Request`).
|
||||
* `:kind` - The type of exception.
|
||||
* `:reason` - Error description or error data.
|
||||
* `:stacktrace` - The stacktrace.
|
||||
|
||||
### Connect Start
|
||||
|
||||
`[:finch, :connect, :start]` - Executed before opening a new connection.
|
||||
If a connection is being re-used this event will *not* be executed.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:system_time` - The system time.
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:name` - The name of the Finch instance.
|
||||
* `:scheme` - The scheme used in the connection. either `http` or `https`.
|
||||
* `:host` - The host address.
|
||||
* `:port` - The port to connect on.
|
||||
|
||||
### Connect Stop
|
||||
|
||||
`[:finch, :connect, :stop]` - Executed after a connection is opened.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:duration` - Time taken to connect to the host.
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:name` - The name of the Finch instance.
|
||||
* `:scheme` - The scheme used in the connection. either `http` or `https`.
|
||||
* `:host` - The host address.
|
||||
* `:port` - The port to connect on.
|
||||
* `:error` - This value is optional. It includes any errors that occurred while opening the connection.
|
||||
|
||||
### Send Start
|
||||
|
||||
`[:finch, :send, :start]` - Executed before sending a request.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:name` - The name of the Finch instance.
|
||||
* `:system_time` - The system time.
|
||||
* `:idle_time` - Elapsed time since the connection was last checked in or initialized.
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:request` - The request (`Finch.Request`).
|
||||
|
||||
### Send Stop
|
||||
|
||||
`[:finch, :send, :stop]` - Executed after a request is finished.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:name` - The name of the Finch instance.
|
||||
* `:duration` - Time taken to make the request.
|
||||
* `:idle_time` - Elapsed time since the connection was last checked in or initialized.
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:request` - The request (`Finch.Request`).
|
||||
* `:error` - This value is optional. It includes any errors that occurred while making the request.
|
||||
|
||||
### Receive Start
|
||||
|
||||
`[:finch, :recv, :start]` - Executed before receiving the response.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:system_time` - The system time.
|
||||
* `:idle_time` - Elapsed time since the connection was last checked in or initialized.
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:name` - The name of the Finch instance.
|
||||
* `:request` - The request (`Finch.Request`).
|
||||
|
||||
### Receive Stop
|
||||
|
||||
`[:finch, :recv, :stop]` - Executed after a response has been fully received.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:duration` - Duration to receive the response.
|
||||
* `:idle_time` - Elapsed time since the connection was last checked in or initialized.
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:name` - The name of the Finch instance.
|
||||
* `:request` - The request (`Finch.Request`).
|
||||
* `:status` - The response status (`Mint.Types.status()`).
|
||||
* `:headers` - The response headers (`Mint.Types.headers()`).
|
||||
* `:error` - This value is optional. It includes any errors that occurred while receiving the response.
|
||||
|
||||
### Receive Exception
|
||||
|
||||
`[:finch, :recv, :exception]` - Executed if an exception is thrown before the response has
|
||||
been fully received.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:duration` - The time it took before raising an exception
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:name` - The name of the Finch instance.
|
||||
* `:request` - The request (`Finch.Request`).
|
||||
* `:kind` - The type of exception.
|
||||
* `:reason` - Error description or error data.
|
||||
* `:stacktrace` - The stacktrace.
|
||||
|
||||
### Reused Connection
|
||||
|
||||
`[:finch, :reused_connection]` - Executed if an existing HTTP1 connection is reused. There are no measurements provided with this event.
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:name` - The name of the Finch instance.
|
||||
* `:scheme` - The scheme used in the connection. either `http` or `https`.
|
||||
* `:host` - The host address.
|
||||
* `:port` - The port to connect on.
|
||||
|
||||
### Conn Max Idle Time Exceeded
|
||||
|
||||
`[:finch, :conn_max_idle_time_exceeded]` - Executed if an HTTP1 connection was discarded because the `conn_max_idle_time` had been reached.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:idle_time` - Elapsed time since the connection was last checked in or initialized.
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:scheme` - The scheme used in the connection. either `http` or `https`.
|
||||
* `:host` - The host address.
|
||||
* `:port` - The port to connect on.
|
||||
|
||||
### Pool Max Idle Time Exceeded
|
||||
|
||||
`[:finch, :pool_max_idle_time_exceeded]` - Executed if an HTTP1 pool was terminated because the `pool_max_idle_time` has been reached. There are no measurements provided with this event.
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:scheme` - The scheme used in the connection. either `http` or `https`.
|
||||
* `:host` - The host address.
|
||||
* `:port` - The port to connect on.
|
||||
|
||||
### Max Idle Time Exceeded (Deprecated)
|
||||
|
||||
`[:finch, :max_idle_time_exceeded]` - Executed if an HTTP1 connection was discarded because the `max_idle_time` had been reached.
|
||||
|
||||
*Deprecated:* use `:conn_max_idle_time_exceeded` event instead.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:idle_time` - Elapsed time since the connection was last checked in or initialized.
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:scheme` - The scheme used in the connection. either `http` or `https`.
|
||||
* `:host` - The host address.
|
||||
* `:port` - The port to connect on.
|
||||
"""
|
||||
|
||||
@doc false
|
||||
# emits a `start` telemetry event and returns the the start time
|
||||
def start(event, meta \\ %{}, extra_measurements \\ %{}) do
|
||||
start_time = System.monotonic_time()
|
||||
|
||||
:telemetry.execute(
|
||||
[:finch, event, :start],
|
||||
Map.merge(extra_measurements, %{system_time: System.system_time()}),
|
||||
meta
|
||||
)
|
||||
|
||||
start_time
|
||||
end
|
||||
|
||||
@doc false
|
||||
# Emits a stop event.
|
||||
def stop(event, start_time, meta \\ %{}, extra_measurements \\ %{}) do
|
||||
end_time = System.monotonic_time()
|
||||
measurements = Map.merge(extra_measurements, %{duration: end_time - start_time})
|
||||
|
||||
:telemetry.execute(
|
||||
[:finch, event, :stop],
|
||||
measurements,
|
||||
meta
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def exception(event, start_time, kind, reason, stack, meta \\ %{}, extra_measurements \\ %{}) do
|
||||
end_time = System.monotonic_time()
|
||||
measurements = Map.merge(extra_measurements, %{duration: end_time - start_time})
|
||||
|
||||
meta =
|
||||
meta
|
||||
|> Map.put(:kind, kind)
|
||||
|> Map.put(:reason, reason)
|
||||
|> Map.put(:stacktrace, stack)
|
||||
|
||||
:telemetry.execute([:finch, event, :exception], measurements, meta)
|
||||
end
|
||||
|
||||
@doc false
|
||||
# Used for reporting generic events
|
||||
def event(event, measurements, meta) do
|
||||
:telemetry.execute([:finch, event], measurements, meta)
|
||||
end
|
||||
|
||||
@doc false
|
||||
# Used to easily create :start, :stop, :exception events.
|
||||
def span(event, start_metadata, fun) do
|
||||
:telemetry.span(
|
||||
[:finch, event],
|
||||
start_metadata,
|
||||
fun
|
||||
)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user