update
This commit is contained in:
4
phoenix/deps/finch/.formatter.exs
Normal file
4
phoenix/deps/finch/.formatter.exs
Normal file
@@ -0,0 +1,4 @@
|
||||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
||||
BIN
phoenix/deps/finch/.hex
Normal file
BIN
phoenix/deps/finch/.hex
Normal file
Binary file not shown.
264
phoenix/deps/finch/CHANGELOG.md
Normal file
264
phoenix/deps/finch/CHANGELOG.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# Changelog
|
||||
|
||||
## v0.21.0 (2026-01-22)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Add support for querying default pool metrics via `Finch.get_pool_status/2` #329
|
||||
- Add more details on `Finch.request/3` docs #327
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Prevent idle HTTP/1 pools from being terminated while connections are in use #292
|
||||
- Disable unsupported HTTP/2 server push responses to avoid crashes #333
|
||||
- Drop `:cacerts` from defaults for HTTP connections to avoid breaking plain HTTP pools #333
|
||||
- Only track default pool metrics when `start_pool_metrics?` is enabled #329
|
||||
|
||||
### Other
|
||||
|
||||
- Elixir 1.19 compatibility updates for HTTP/1 pool state handling #331
|
||||
- CI: update test matrix and x509/deps for Elixir 1.19 #330
|
||||
- Add ALPN large-body regression test coverage (issue #265) #332
|
||||
|
||||
## v0.20.0 (2025-07-04)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Support manual pool termination #299
|
||||
- Refactor HTTP1 pool state for better maintainability #308
|
||||
- Add `:supported_groups` to list of TLS options #307
|
||||
- Be more explicit about the `:default` pool in documentation #314
|
||||
- Upgrade `nimble_options` to document deprecations #315
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix Finch.stream_while/5 on halt for both HTTP/1 and HTTP/2 #320
|
||||
- Return accumulator when Finch.stream/5 and Finch.stream_while/5 fail #295
|
||||
- Fix documentation reference for get_pool_status/2 #301
|
||||
|
||||
### Other
|
||||
|
||||
- Upgrade CI VM to Ubuntu 24 #321
|
||||
- CI housekeeping: support Elixir 1.17/Erlang OTP 27, bump Credo and deps #303
|
||||
- Update GitHub CI badge URL #304
|
||||
|
||||
## v0.19.0 (2024-09-04)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Update @mint_tls_opts in pool_manager.ex #266
|
||||
- Document there is no backpressure on HTTP2 #283
|
||||
- Fix test: compare file size instead of map #284
|
||||
- Finch.request/3: Use improper list and avoid Enum.reverse #286
|
||||
- Require Mint 1.6 #287
|
||||
- Remove castore dependency #274
|
||||
- Fix typos and improve language in docs and comments #285
|
||||
- fix logo size in README #275
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Tweak Finch supervisor children startup order #289, fixes #277
|
||||
- implement handle_cancelled/2 pool callback #268, fixes #257
|
||||
- type Finch.request_opt() was missing the :request_timeout option #278
|
||||
|
||||
## v0.18.0 (2024-02-09)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Add Finch name to telemetry events #252
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix several minor dialyzer errors and run dialyzer in CI #259, #261
|
||||
|
||||
## v0.17.0 (2024-01-07)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Add support for async requests #228, #231
|
||||
- Add stream example to docs #230
|
||||
- Fix calls to deprecated Logger.warn/2 #232
|
||||
- Fix typos #233
|
||||
- Docs: do not use streams with async_request #238
|
||||
- Add Finch.stream_while/5 #239
|
||||
- Set MIX_ENV=test on CI #241
|
||||
- Update HTTP/2 pool log level to warning for retried action #240
|
||||
- Split trailers from headers #242
|
||||
- Introduce :request_timeout option #244
|
||||
- Support ALPN over HTTP1 pools #250
|
||||
- Deprecate :protocol in favour of :protocols #251
|
||||
- Implement pool telemetry #248
|
||||
|
||||
## v0.16.0 (2023-04-13)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- add `Finch.request!/3` #219
|
||||
- allow usage with nimble_pool 1.0 #220
|
||||
|
||||
## v0.15.0 (2023-03-16)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- allow usage with nimble_options 1.0 #218
|
||||
- allow usage with castore 1.0 #210
|
||||
|
||||
## v0.14.0 (2022-11-30)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Improve error message for pool timeouts #126
|
||||
- Relax nimble_options version to allow usage with 0.5.0 #204
|
||||
|
||||
## v0.13.0 (2022-07-26)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Define `Finch.child_spec/1` which will automatically use the `Finch` `:name` as the `:id`, allowing users to start multiple instances under the same Supervisor without any additional configuration #202
|
||||
- Include the changelog in the generated HexDocs #201
|
||||
- Fix typo in `Finch.Telemetry` docs #198
|
||||
|
||||
## v0.12.0 (2022-05-03)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Add support for private request metadata #180
|
||||
- Hide docs for deprecated `Finch.request/6` #195
|
||||
- Add support for Mint.UnsafeProxy connections #184
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- In v0.11.0 headers and status codes were added to Telemetry events in a way that made invalid assumptions
|
||||
regarding the shape of the response accumulator, this has been resolved in #196
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Telemetry updates #176
|
||||
- Rename the telemetry event `:request` to `:send` and `:response` to `:recv`.
|
||||
- Introduce a new `:request` field which contains the full `Finch.Request.t()` in place of the `:scheme`, `:host`, `:port`, `:path`, `:method` fields wherever possible. The new `:request` field can be found on the `:request`, `:queue`, `:send`, and `:recv` events.
|
||||
- Rename the meta data field `:error` to `:reason` for all `:exception` events to follow the standard introduced in [telemetry](https://github.com/beam-telemetry/telemetry/blob/3f069cfd2193396bee221d0709287c1bdaa4fabf/src/telemetry.erl#L335)
|
||||
- Introduce a new `[:finch, :request, :start | :stop | :exception]` telemetry event that emits
|
||||
whenever `Finch.request/3` or `Finch.stream/5` are called.
|
||||
|
||||
## v0.11.0 (2022-03-28)
|
||||
|
||||
- Add `:pool_max_idle_time` option to enable termination of idle HTTP/1 pools.
|
||||
- Add `:conn_max_idle_time` and deprecate `:max_idle_time` to make the distinction from
|
||||
`:pool_max_idle_time` more obvious.
|
||||
- Add headers and status code to Telemetry events.
|
||||
|
||||
## v0.10.2 (2022-01-12)
|
||||
|
||||
- Complete the typespec for Finch.Request.t()
|
||||
- Fix the typespec for Finch.build/5
|
||||
- Update deps
|
||||
|
||||
## v0.10.1 (2021-12-27)
|
||||
|
||||
- Fix handling of iodata in HTTP/2 request streams.
|
||||
|
||||
## v0.10.0 (2021-12-12)
|
||||
|
||||
- Add ability to stream the request body for HTTP/2 requests.
|
||||
- Check and respect window sizes during HTTP/2 requests.
|
||||
|
||||
## v0.9.1 (2021-10-17)
|
||||
|
||||
- Upgrade NimbleOptions dep to 0.4.0.
|
||||
|
||||
## v0.9.0 (2021-10-17)
|
||||
|
||||
- Add support for unix sockets.
|
||||
|
||||
## v0.8.3 (2021-10-15)
|
||||
|
||||
- Return Error struct when HTTP2 connection is closed and a timeout occurs.
|
||||
- Do not leak messages/connections when cancelling streaming requests.
|
||||
|
||||
## v0.8.2 (2021-09-09)
|
||||
|
||||
- Demonitor http/2 connections when the request is done.
|
||||
|
||||
## v0.8.1 (2021-07-27)
|
||||
|
||||
- Update mix.exs to allow compatibility with Telemetry v1.0
|
||||
- Avoid appending "?" to request_path when query string is an empty string
|
||||
|
||||
## v0.8.0 (2021-06-23)
|
||||
|
||||
- HTTP2 connections will now always return Exceptions.
|
||||
|
||||
## v0.7.0 (2021-05-10)
|
||||
|
||||
- Add support for SSLKEYLOGFILE.
|
||||
- Drop HTTPS options for default HTTP pools to avoid `:badarg` errors.
|
||||
|
||||
## v0.6.3 (2021-02-22)
|
||||
|
||||
- Return more verbose errors when finch is configured with bad URLs.
|
||||
|
||||
## v0.6.2 (2021-02-19)
|
||||
|
||||
- Fix incorrect type spec for stream/5
|
||||
- Add default transport options for keepalive, timeouts, and nodelay.
|
||||
|
||||
## v0.6.1 (2021-02-17)
|
||||
|
||||
- Update Mint to 1.2.1, which properly handles HTTP/1.0 style responses that close
|
||||
the connection at the same time as sending the response.
|
||||
- Update NimblePool to 0.2.4 which includes a bugfix that prevents extra connections
|
||||
being opened.
|
||||
- Fix the typespec for Finch.stream/5.
|
||||
- Fix assertion that was not actually being called in a test case.
|
||||
|
||||
## v0.6.0 (2020-12-15)
|
||||
|
||||
- Add ability to stream the request body for HTTP/1.x requests.
|
||||
|
||||
## v0.5.2 (2020-11-10)
|
||||
|
||||
- Fix deprecation in nimble_options.
|
||||
|
||||
## v0.5.1 (2020-10-27)
|
||||
|
||||
- Fix crash in http2 pools when a message is received in disconnected state.
|
||||
|
||||
## v0.5.0 (2020-10-26)
|
||||
|
||||
- Add `:max_idle_time` option for http1 pools
|
||||
- Optimize http2 connection closing.
|
||||
- Use new lazy pools in NimblePool
|
||||
- Additional `idle_time` measurements for all http1 connection telemetry
|
||||
|
||||
## v0.4.0 (2020-10-2)
|
||||
|
||||
- Update all dependencies. This includes bug fixes for Mint.
|
||||
|
||||
## v0.3.2 (2020-09-18)
|
||||
|
||||
- Add metadata to connection start telemetry in http/2 pools
|
||||
|
||||
## v0.3.1 (2020-08-29)
|
||||
|
||||
- Add HTTP method to telemetry events
|
||||
- BUGFIX - Include query parameters in HTTP/2 requests
|
||||
|
||||
## v0.3.0 (2020-06-24)
|
||||
|
||||
- HTTP/2 support
|
||||
- Streaming support for both http/1.1 and http/2 pools
|
||||
- New api for building and making requests
|
||||
- typespec fixes
|
||||
|
||||
## v0.2.0 (2020-05-06)
|
||||
|
||||
- Response body now defaults to an empty string instead of nil
|
||||
|
||||
## v0.1.1 (2020-05-04)
|
||||
|
||||
- Accepts a URI struct in request/3/4/5/6, Todd Resudek
|
||||
- Fix `http_method()` typespec, Ryan Johnson
|
||||
|
||||
## v0.1.0 (2020-04-25)
|
||||
|
||||
- Initial Release
|
||||
9
phoenix/deps/finch/LICENSE.md
Normal file
9
phoenix/deps/finch/LICENSE.md
Normal file
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Christopher Jon Keathley & Nico Daniel Piderman
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
121
phoenix/deps/finch/README.md
Normal file
121
phoenix/deps/finch/README.md
Normal file
@@ -0,0 +1,121 @@
|
||||
<img alt="Finch" height="350px" src="assets/Finch_logo_onWhite.png#gh-light-mode-only">
|
||||
<img alt="Finch" height="350px" src="assets/Finch_logo_all-White.png#gh-dark-mode-only">
|
||||
|
||||
[](https://github.com/sneako/finch/actions/workflows/elixir.yml)
|
||||
[](https://hex.pm/packages/finch)
|
||||
[](https://hexdocs.pm/finch/)
|
||||
|
||||
<!-- MDOC !-->
|
||||
|
||||
An HTTP client with a focus on performance, built on top of
|
||||
[Mint](https://github.com/elixir-mint/mint) and [NimblePool](https://github.com/dashbitco/nimble_pool).
|
||||
|
||||
We attempt to achieve this goal by providing efficient connection pooling strategies and avoiding copying of memory wherever possible.
|
||||
|
||||
Most developers will most likely prefer to use the fabulous HTTP client [Req](https://github.com/wojtekmach/req) which takes advantage of Finch's pooling and provides an extremely friendly and pleasant to use API.
|
||||
|
||||
## Usage
|
||||
|
||||
In order to use Finch, you must start it and provide a `:name`. Often in your
|
||||
supervision tree:
|
||||
|
||||
```elixir
|
||||
children = [
|
||||
{Finch, name: MyFinch}
|
||||
]
|
||||
```
|
||||
|
||||
Or, in rare cases, dynamically:
|
||||
|
||||
```elixir
|
||||
Finch.start_link(name: MyFinch)
|
||||
```
|
||||
|
||||
Once you have started your instance of Finch, you are ready to start making requests:
|
||||
|
||||
```elixir
|
||||
Finch.build(:get, "https://hex.pm") |> Finch.request(MyFinch)
|
||||
```
|
||||
|
||||
When using HTTP/1, Finch will parse the passed in URL into a `{scheme, host, port}`
|
||||
tuple, and maintain one or more connection pools for each `{scheme, host, port}` you
|
||||
interact with.
|
||||
|
||||
You can also configure a pool size and count to be used for specific URLs that are
|
||||
known before starting Finch. The passed URLs will be parsed into `{scheme, host, port}`,
|
||||
and the corresponding pools will be started. See `Finch.start_link/1` for configuration
|
||||
options.
|
||||
|
||||
```elixir
|
||||
children = [
|
||||
{Finch,
|
||||
name: MyConfiguredFinch,
|
||||
pools: %{
|
||||
:default => [size: 10, count: 2],
|
||||
"https://hex.pm" => [size: 32, count: 8]
|
||||
}}
|
||||
]
|
||||
```
|
||||
|
||||
Pools will be started for each configured `{scheme, host, port}` when Finch is started.
|
||||
For any unconfigured `{scheme, host, port}`, the pool will be started the first time
|
||||
it is requested using the `:default` configuration. This means given the pool
|
||||
configuration above each origin/`{scheme, host, port}` will launch 2 (`:count`) new pool
|
||||
processes. So, if you encountered 10 separate combinations, that'd be 20 pool processes.
|
||||
|
||||
Note pools are not automatically terminated by default, if you need to
|
||||
terminate them after some idle time, use the `pool_max_idle_time` option (available only for HTTP1 pools).
|
||||
|
||||
## Telemetry
|
||||
|
||||
Finch uses Telemetry to provide instrumentation. See the `Finch.Telemetry`
|
||||
module for details on specific events.
|
||||
|
||||
## Logging TLS Secrets
|
||||
|
||||
Finch supports logging TLS secrets to a file. These can be later used in a tool such as
|
||||
Wireshark to decrypt HTTPS sessions. To use this feature you must specify the file to
|
||||
which the secrets should be written. If you are using TLSv1.3 you must also add
|
||||
`keep_secrets: true` to your pool `:transport_opts`. For example:
|
||||
|
||||
```elixir
|
||||
{Finch,
|
||||
name: MyFinch,
|
||||
pools: %{
|
||||
default: [conn_opts: [transport_opts: [keep_secrets: true]]]
|
||||
}}
|
||||
```
|
||||
|
||||
There are two different ways to specify this file:
|
||||
|
||||
1. The `:ssl_key_log_file` connection option in your pool configuration. For example:
|
||||
|
||||
```elixir
|
||||
{Finch,
|
||||
name: MyFinch,
|
||||
pools: %{
|
||||
default: [
|
||||
conn_opts: [
|
||||
ssl_key_log_file: "/writable/path/to/the/sslkey.log"
|
||||
]
|
||||
]
|
||||
}}
|
||||
```
|
||||
|
||||
2. Alternatively, you could also set the `SSLKEYLOGFILE` environment variable.
|
||||
|
||||
<!-- MDOC !-->
|
||||
|
||||
## Installation
|
||||
|
||||
The package can be installed by adding `finch` to your list of dependencies in `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[
|
||||
{:finch, "~> 0.20"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
The docs can be found at [https://hexdocs.pm/finch](https://hexdocs.pm/finch).
|
||||
46
phoenix/deps/finch/hex_metadata.config
Normal file
46
phoenix/deps/finch/hex_metadata.config
Normal file
@@ -0,0 +1,46 @@
|
||||
{<<"links">>,
|
||||
[{<<"Changelog">>,<<"https://hexdocs.pm/finch/changelog.html">>},
|
||||
{<<"GitHub">>,<<"https://github.com/sneako/finch">>}]}.
|
||||
{<<"name">>,<<"finch">>}.
|
||||
{<<"version">>,<<"0.21.0">>}.
|
||||
{<<"description">>,<<"An HTTP client focused on performance.">>}.
|
||||
{<<"elixir">>,<<"~> 1.13">>}.
|
||||
{<<"app">>,<<"finch">>}.
|
||||
{<<"licenses">>,[<<"MIT">>]}.
|
||||
{<<"requirements">>,
|
||||
[[{<<"name">>,<<"mint">>},
|
||||
{<<"app">>,<<"mint">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 1.6.2 or ~> 1.7">>},
|
||||
{<<"repository">>,<<"hexpm">>}],
|
||||
[{<<"name">>,<<"nimble_pool">>},
|
||||
{<<"app">>,<<"nimble_pool">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 1.1">>},
|
||||
{<<"repository">>,<<"hexpm">>}],
|
||||
[{<<"name">>,<<"nimble_options">>},
|
||||
{<<"app">>,<<"nimble_options">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 0.4 or ~> 1.0">>},
|
||||
{<<"repository">>,<<"hexpm">>}],
|
||||
[{<<"name">>,<<"telemetry">>},
|
||||
{<<"app">>,<<"telemetry">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 0.4 or ~> 1.0">>},
|
||||
{<<"repository">>,<<"hexpm">>}],
|
||||
[{<<"name">>,<<"mime">>},
|
||||
{<<"app">>,<<"mime">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 1.0 or ~> 2.0">>},
|
||||
{<<"repository">>,<<"hexpm">>}]]}.
|
||||
{<<"files">>,
|
||||
[<<"lib">>,<<"lib/finch.ex">>,<<"lib/finch">>,<<"lib/finch/http1">>,
|
||||
<<"lib/finch/http1/conn.ex">>,<<"lib/finch/http1/pool.ex">>,
|
||||
<<"lib/finch/http1/pool_metrics.ex">>,<<"lib/finch/telemetry.ex">>,
|
||||
<<"lib/finch/response.ex">>,<<"lib/finch/request.ex">>,
|
||||
<<"lib/finch/pool_manager.ex">>,<<"lib/finch/error.ex">>,
|
||||
<<"lib/finch/http2">>,<<"lib/finch/http2/request_stream.ex">>,
|
||||
<<"lib/finch/http2/pool.ex">>,<<"lib/finch/http2/pool_metrics.ex">>,
|
||||
<<"lib/finch/ssl.ex">>,<<"lib/finch/pool.ex">>,<<".formatter.exs">>,
|
||||
<<"mix.exs">>,<<"README.md">>,<<"LICENSE.md">>,<<"CHANGELOG.md">>]}.
|
||||
{<<"build_tools">>,[<<"mix">>]}.
|
||||
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
|
||||
73
phoenix/deps/finch/mix.exs
Normal file
73
phoenix/deps/finch/mix.exs
Normal file
@@ -0,0 +1,73 @@
|
||||
defmodule Finch.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
@name "Finch"
|
||||
@version "0.21.0"
|
||||
@repo_url "https://github.com/sneako/finch"
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :finch,
|
||||
version: @version,
|
||||
elixir: "~> 1.13",
|
||||
description: "An HTTP client focused on performance.",
|
||||
package: package(),
|
||||
docs: docs(),
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
start_permanent: Mix.env() == :prod,
|
||||
name: @name,
|
||||
source_url: @repo_url,
|
||||
deps: deps()
|
||||
]
|
||||
end
|
||||
|
||||
defp elixirc_paths(:test), do: ["lib", "test/support"]
|
||||
defp elixirc_paths(:dev), do: ["lib", "test/support/test_usage.ex"]
|
||||
defp elixirc_paths(_), do: ["lib"]
|
||||
|
||||
def application do
|
||||
[
|
||||
extra_applications: [:logger]
|
||||
]
|
||||
end
|
||||
|
||||
defp deps do
|
||||
[
|
||||
{:mint, "~> 1.6.2 or ~> 1.7"},
|
||||
{:nimble_pool, "~> 1.1"},
|
||||
{:nimble_options, "~> 0.4 or ~> 1.0"},
|
||||
{:telemetry, "~> 0.4 or ~> 1.0"},
|
||||
{:mime, "~> 1.0 or ~> 2.0"},
|
||||
{:ex_doc, "~> 0.28", only: :dev, runtime: false},
|
||||
{:credo, "~> 1.3", only: [:dev, :test]},
|
||||
{:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false},
|
||||
{:bypass, "~> 2.0", only: :test},
|
||||
{:cowboy, "~> 2.7", only: [:dev, :test]},
|
||||
{:plug_cowboy, "~> 2.0", only: [:dev, :test]},
|
||||
{:x509, "~> 0.8", only: [:dev, :test]},
|
||||
{:mimic, "~> 1.7", only: :test}
|
||||
]
|
||||
end
|
||||
|
||||
defp package do
|
||||
[
|
||||
licenses: ["MIT"],
|
||||
links: %{
|
||||
"GitHub" => @repo_url,
|
||||
"Changelog" => "https://hexdocs.pm/finch/changelog.html"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
defp docs do
|
||||
[
|
||||
logo: "assets/Finch_logo_all-White.png",
|
||||
source_ref: "v#{@version}",
|
||||
source_url: @repo_url,
|
||||
main: @name,
|
||||
extras: [
|
||||
"CHANGELOG.md"
|
||||
]
|
||||
]
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user