Skip to content

Commit f470149

Browse files
committed
Re-vendor Mint with larger default HTTP/2 receive windows
Absorbs two new upstream Mint commits on top of the existing vendored stack: * `Raise default HTTP/2 receive windows` — bumps Mint's defaults from the spec-mandated 64 KB to 4 MB per stream and 16 MB per connection. At typical RTTs (10-150 ms) 64 KB caps throughput far below link speed; 4/16 MB unlocks ~40 MB/s transcontinental and stream < conn lets ~4 parallel streams run at full rate before the conn pool binds. * `Batch HTTP/2 receive-window refills` — gates `WINDOW_UPDATE` on a configurable threshold (`:receive_window_update_threshold`, default 160_000, ~10× the default max frame size) instead of refilling on every DATA frame. Mitigates the amplification-DoS shape where a malicious server sends many tiny DATA frames to force many WINDOW_UPDATE responses. On the hex side this means the explicit window tuning that the previous re-vendor added to `Conn.do_connect` (setting `client_settings: [initial_window_size: 8_000_000]` and calling `set_window_size(conn, :connection, 8_000_000)`) is now redundant — Mint's 4/16 MB defaults already cover hex's bulk-tarball workload. Drop the tuning block and the `maybe_bump_connection_window/1` helper; `do_connect` now just sets `protocols: [:http1, :http2]` and lets Mint's defaults handle the rest. Also: * Exclude the vendored `lib/hex/mint/**` tree from `.formatter.exs` so `mix format --check-formatted` doesn't fight upstream formatting every re-vendor. * Format drift in `lib/hex/http.ex` and `lib/hex/http/pool.ex` that the vendored formatter exclusion surfaced. * Update `scripts/vendor_mint.sh` comment block to list the new larger-default-windows branch alongside the other pending upstream items.
1 parent 7fcea16 commit f470149

24 files changed

Lines changed: 280 additions & 178 deletions

.formatter.exs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
[
2-
inputs: [
3-
"*.exs",
4-
"config/*.exs",
5-
"lib/**/*.ex",
6-
"test/**/*.{ex,exs}"
7-
]
2+
inputs:
3+
["*.exs", "config/*.exs", "test/**/*.{ex,exs}"] ++
4+
(Path.wildcard("lib/**/*.ex") -- Path.wildcard("lib/hex/mint/**/*.ex"))
85
]

lib/hex/http/pool.ex

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ defmodule Hex.HTTP.Pool do
6262
defp inet_variant(connect_opts) do
6363
transport_opts = Keyword.get(connect_opts, :transport_opts, [])
6464

65-
case {Keyword.get(transport_opts, :inet4, true),
66-
Keyword.get(transport_opts, :inet6, false)} do
65+
case {Keyword.get(transport_opts, :inet4, true), Keyword.get(transport_opts, :inet6, false)} do
6766
{true, false} -> :inet
6867
{false, true} -> :inet6
6968
_ -> :default

lib/hex/http/pool/conn.ex

Lines changed: 23 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ defmodule Hex.HTTP.Pool.Conn do
4545
def handle_continue(:connect, state), do: do_connect(state)
4646

4747
@impl true
48-
def handle_cast({:request, from, method, path, headers, {:stream, fun, offset}}, %{ready: true} = state) do
48+
def handle_cast(
49+
{:request, from, method, path, headers, {:stream, fun, offset}},
50+
%{ready: true} = state
51+
) do
4952
case MintHTTP.request(state.conn, method, path, headers, :stream) do
5053
{:ok, conn, ref} ->
5154
case stream_body(conn, ref, fun, offset) do
@@ -140,65 +143,35 @@ defmodule Hex.HTTP.Pool.Conn do
140143

141144
## Connect / reconnect
142145

143-
@receive_window_size 8_000_000
144-
145146
defp do_connect(%{key: {scheme, host, port, _inet}, connect_opts: opts} = state) do
146-
# Negotiate HTTP/2 via ALPN when the server supports it; fall back to
147-
# HTTP/1. Benchmarked against real hex.pm + repo.hex.pm — with the HTTP/2
148-
# window bumps applied below, both protocols are equivalent on
149-
# `mix deps.get` wall time, and HTTP/2 uses slightly less CPU (fewer
150-
# TLS handshakes).
151-
opts =
152-
Keyword.merge(
153-
[
154-
protocols: [:http1, :http2],
155-
# Per-stream initial window (SETTINGS). The connection-level window
156-
# is bumped via set_window_size/3 immediately below.
157-
client_settings: [initial_window_size: @receive_window_size]
158-
],
159-
opts
160-
)
147+
# Negotiate HTTP/2 via ALPN when the server supports it; fall back to HTTP/1.
148+
# Both protocols are equivalent on `mix deps.get` wall time and HTTP/2 uses
149+
# slightly less CPU (fewer TLS handshakes). Mint's default HTTP/2 receive
150+
# windows (4 MB per stream, 16 MB per connection) are already tuned for bulk
151+
# downloads, so no extra tuning is needed here.
152+
opts = Keyword.merge([protocols: [:http1, :http2]], opts)
161153

162154
case MintHTTP.connect(scheme, host, port, opts) do
163155
{:ok, conn} ->
164-
case maybe_bump_connection_window(conn) do
165-
{:ok, conn} ->
166-
protocol = MintHTTP.protocol(conn)
167-
capacity = compute_capacity(conn, protocol)
168-
GenServer.cast(state.host_pid, {:conn_ready, self(), protocol, capacity})
169-
170-
{:noreply,
171-
%{
172-
state
173-
| conn: conn,
174-
protocol: protocol,
175-
capacity: capacity,
176-
ready: true,
177-
backoff_ms: 0
178-
}}
179-
180-
{:error, _conn, reason} ->
181-
schedule_reconnect(reason, %{state | conn: nil, ready: false})
182-
end
156+
protocol = MintHTTP.protocol(conn)
157+
capacity = compute_capacity(conn, protocol)
158+
GenServer.cast(state.host_pid, {:conn_ready, self(), protocol, capacity})
159+
160+
{:noreply,
161+
%{
162+
state
163+
| conn: conn,
164+
protocol: protocol,
165+
capacity: capacity,
166+
ready: true,
167+
backoff_ms: 0
168+
}}
183169

184170
{:error, reason} ->
185171
schedule_reconnect(reason, %{state | conn: nil, ready: false})
186172
end
187173
end
188174

189-
# HTTP/2 default per-connection receive window is 64 KB (RFC 7540 §5.2.2),
190-
# not tunable via SETTINGS. Hex tarballs are routinely multi-MB and we run
191-
# them in parallel sharing one HTTP/2 connection, so without bumping this
192-
# the server pauses every ~64 KB waiting for a `WINDOW_UPDATE` round-trip.
193-
# HTTP/1 has no concept of a receive window so this is a no-op there —
194-
# `set_window_size/3` is only on `Hex.Mint.HTTP2`.
195-
defp maybe_bump_connection_window(conn) do
196-
case MintHTTP.protocol(conn) do
197-
:http2 -> Hex.Mint.HTTP2.set_window_size(conn, :connection, @receive_window_size)
198-
:http1 -> {:ok, conn}
199-
end
200-
end
201-
202175
defp close_and_reconnect(state) do
203176
if state.conn, do: safe_close(state.conn)
204177
schedule_reconnect(:closed, %{state | conn: nil, ready: false})

lib/hex/mint/core/conn.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Vendored from mint v1.7.1 (74471c9), do not edit manually
1+
# Vendored from mint v1.7.1 (d30d2cf), do not edit manually
22

33
defmodule Hex.Mint.Core.Conn do
44
@moduledoc false

lib/hex/mint/core/headers.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Vendored from mint v1.7.1 (74471c9), do not edit manually
1+
# Vendored from mint v1.7.1 (d30d2cf), do not edit manually
22

33
defmodule Hex.Mint.Core.Headers do
44
@moduledoc false

lib/hex/mint/core/transport.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Vendored from mint v1.7.1 (74471c9), do not edit manually
1+
# Vendored from mint v1.7.1 (d30d2cf), do not edit manually
22

33
defmodule Hex.Mint.Core.Transport do
44
@moduledoc false

lib/hex/mint/core/transport/ssl.ex

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Vendored from mint v1.7.1 (74471c9), do not edit manually
1+
# Vendored from mint v1.7.1 (d30d2cf), do not edit manually
22

33
defmodule Hex.Mint.Core.Transport.SSL do
44
@moduledoc false
@@ -603,18 +603,14 @@ defmodule Hex.Mint.Core.Transport.SSL do
603603
end
604604

605605
defp get_cacertfile(path) do
606-
if Application.get_env(:mint, :persistent_term) do
607-
case :persistent_term.get({:hex_mint, {:cacertfile, path}}, :error) do
608-
{:ok, cacerts} ->
609-
cacerts
610-
611-
:error ->
612-
cacerts = decode_cacertfile(path)
613-
:persistent_term.put({:hex_mint, {:cacertfile, path}}, {:ok, cacerts})
614-
cacerts
615-
end
616-
else
617-
decode_cacertfile(path)
606+
case :persistent_term.get({:hex_mint, {:cacertfile, path}}, :error) do
607+
{:ok, cacerts} ->
608+
cacerts
609+
610+
:error ->
611+
cacerts = decode_cacertfile(path)
612+
:persistent_term.put({:hex_mint, {:cacertfile, path}}, {:ok, cacerts})
613+
cacerts
618614
end
619615
end
620616

lib/hex/mint/core/transport/tcp.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Vendored from mint v1.7.1 (74471c9), do not edit manually
1+
# Vendored from mint v1.7.1 (d30d2cf), do not edit manually
22

33
defmodule Hex.Mint.Core.Transport.TCP do
44
@moduledoc false

lib/hex/mint/core/util.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Vendored from mint v1.7.1 (74471c9), do not edit manually
1+
# Vendored from mint v1.7.1 (d30d2cf), do not edit manually
22

33
defmodule Hex.Mint.Core.Util do
44
@moduledoc false

lib/hex/mint/http.ex

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Vendored from mint v1.7.1 (74471c9), do not edit manually
1+
# Vendored from mint v1.7.1 (d30d2cf), do not edit manually
22

33
defmodule Hex.Mint.HTTP do
44
_ = """
@@ -240,6 +240,17 @@ defmodule Hex.Mint.HTTP do
240240
server. See `Hex.Mint.HTTP2.put_settings/2` for more information. This is only used
241241
in HTTP/2 connections.
242242
243+
* `:connection_window_size` - (integer) the initial size of the connection-level
244+
HTTP/2 receive window, in bytes. Sent to the server as a `WINDOW_UPDATE` frame
245+
on stream 0 as part of the connection preface. Defaults to 16 MB. Can be
246+
raised later with `Hex.Mint.HTTP2.set_window_size/3`.
247+
248+
* `:receive_window_update_threshold` - (integer) the minimum number of bytes of receive
249+
window that must remain on a connection or stream before a `WINDOW_UPDATE`
250+
frame is sent to refill it. Lower values send more frequent, smaller updates;
251+
higher values batch updates into fewer, larger ones. Defaults to 160_000
252+
(approximately 10× the default max frame size).
253+
243254
There may be further protocol specific options that only take effect when the corresponding
244255
connection is established. Check `Hex.Mint.HTTP1.connect/4` and `Hex.Mint.HTTP2.connect/4` for
245256
details.

0 commit comments

Comments
 (0)