Skip to content

Commit 4336ca6

Browse files
committed
Unify HTTP/1 and HTTP/2 pool, fix 1xx handling
The previous pool design split HTTP/1 and HTTP/2 into separate modules and probed ALPN in the caller process. That caller opened the socket, then tried to hand it off to a worker via `controlling_process/2` — which fails with `:not_owner` when called from anywhere except the current socket owner. The "fix" in HTTP1.Worker.init/1 (calling `controlling_process(conn, self())` from the worker) could never have worked at runtime; the HTTP/2 path had the same latent bug. Replaced the HTTP1/HTTP1.Worker/HTTP2 modules with a unified design: * Hex.HTTP.Pool.Host (one GenServer per {scheme, host, port}) owns a pool of Conn processes. On start it spawns two probe Conns; when the first reports its protocol it scales up to 8 for :http1 or stays at 2 for :http2. Dispatches by least in-flight to the Conn with free capacity; queues waiters otherwise. * Hex.HTTP.Pool.Conn (one GenServer per Mint connection) connects inside its own process, so the socket is owned from birth — no controlling_process handoff ever needed. Reports negotiated protocol and per-conn capacity (1 for HTTP/1, server's max_concurrent_streams for HTTP/2) back to its host. Requests arrive as casts carrying the caller's `from`; Conn replies directly to the caller and casts back :req_done so the host decrements in-flight. Handles GOAWAY by draining in-flight then reconnecting with exponential backoff. * Hex.HTTP.Pool now just wires up the Registry + DynamicSupervisor and routes requests to the Host for a given {scheme, host, port}. No more probe phase, no ETS cache, no initial_conn plumbing. Also fixes 1xx informational response handling in the vendored Mint HTTP/1 module, which previously emitted `:done` and popped the request after a 100 Continue / 103 Early Hints etc., causing the real final response to arrive with no active request and the connection to close. Upstream fix submitted as elixir-mint/mint#479; the vendored copy marks the change with `# HEX PATCH` comments for easy re-application after re-vendoring. Conn.process_response resets accumulated headers/data on each new `:status` so informational headers (e.g. 103 Early Hints `link:`) don't bleed into the final response. Added bypass-backed regression tests for 100 Continue round-trip, 100 Continue with early error response, and 103 Early Hints with headers. test/mix/tasks/hex.registry_test.exs now uses `Hex.HTTP.config/0` instead of the raw httpc-defaulting config.
1 parent 54c57b3 commit 4336ca6

9 files changed

Lines changed: 508 additions & 828 deletions

File tree

lib/hex/http/pool.ex

Lines changed: 23 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,24 @@ defmodule Hex.HTTP.Pool do
33

44
# Top-level Mint-based HTTP connection pool.
55
#
6-
# Responsibilities:
7-
# * Supervises a Registry and a DynamicSupervisor for per-host pools
8-
# * Owns an ETS table caching the protocol (:http1 | :http2) discovered
9-
# via ALPN for each {scheme, host, port} tuple
10-
# * Routes `request/5` to the appropriate HTTP/1 or HTTP/2 pool,
11-
# probing the server the first time we see a host
6+
# Owns a Registry and a DynamicSupervisor. Each {scheme, host, port} maps to
7+
# one `Hex.HTTP.Pool.Host` child which owns its own pool of
8+
# `Hex.HTTP.Pool.Conn` processes. Protocol (HTTP/1 vs HTTP/2) is discovered
9+
# inside the Conn on its first connect, so this layer is protocol-agnostic.
1210

1311
use Supervisor
1412

15-
alias Hex.HTTP.Pool.{HTTP1, HTTP2}
16-
alias Hex.Mint.HTTP, as: MintHTTP
13+
alias Hex.HTTP.Pool.Host
1714

1815
@registry Hex.HTTP.Pool.Registry
1916
@dyn_sup Hex.HTTP.Pool.DynamicSupervisor
20-
@ets :hex_http_pool_protocol
2117

2218
def start_link(_opts \\ []) do
2319
Supervisor.start_link(__MODULE__, [], name: __MODULE__)
2420
end
2521

2622
@impl true
2723
def init(_) do
28-
_ =
29-
try do
30-
:ets.new(@ets, [:named_table, :public, :set, read_concurrency: true])
31-
rescue
32-
ArgumentError -> :ok
33-
end
34-
3524
children = [
3625
{Registry, keys: :unique, name: @registry},
3726
{DynamicSupervisor, name: @dyn_sup, strategy: :one_for_one}
@@ -54,124 +43,29 @@ defmodule Hex.HTTP.Pool do
5443
"""
5544
def request(url, method, headers, body, opts) do
5645
{scheme, host, port, path} = parse_url(url)
46+
key = {scheme, host, port}
5747

58-
do_request({scheme, host, port}, path, method, headers, body, opts)
59-
end
60-
61-
defp do_request(key, path, method, headers, body, opts) do
62-
case :ets.lookup(@ets, key) do
63-
[{^key, :http1}] ->
64-
request_http1(key, path, method, headers, body, opts, nil)
65-
66-
[{^key, :http2}] ->
67-
case request_http2(key, path, method, headers, body, opts, nil) do
68-
{:error, :read_only} ->
69-
_ = stop_pool(key)
70-
do_request(key, path, method, headers, body, opts)
71-
72-
other ->
73-
other
74-
end
75-
76-
[] ->
77-
probe_and_dispatch(key, path, method, headers, body, opts)
78-
end
79-
end
80-
81-
defp probe_and_dispatch(key, path, method, headers, body, opts) do
82-
connect_opts = Keyword.get(opts, :connect_opts, [])
83-
84-
case probe_connect(key, connect_opts) do
85-
{:ok, conn, :http1} ->
86-
:ets.insert(@ets, {key, :http1})
87-
request_http1(key, path, method, headers, body, opts, conn)
88-
89-
{:ok, conn, :http2} ->
90-
:ets.insert(@ets, {key, :http2})
91-
request_http2(key, path, method, headers, body, opts, conn)
92-
93-
{:error, reason} ->
94-
{:error, reason}
95-
end
96-
end
97-
98-
defp probe_connect({scheme, host, port}, connect_opts) do
99-
opts = Keyword.merge([protocols: [:http1, :http2]], connect_opts)
100-
101-
case MintHTTP.connect(scheme, host, port, opts) do
102-
{:ok, conn} ->
103-
{:ok, conn, MintHTTP.protocol(conn)}
104-
105-
{:error, reason} ->
106-
{:error, reason}
107-
end
108-
end
109-
110-
defp request_http1({scheme, host, port} = key, path, method, headers, body, opts, initial_conn) do
111-
connect_opts = Keyword.get(opts, :connect_opts, [])
11248
timeout = Keyword.get(opts, :timeout, 15_000)
113-
114-
connect_fun = fn ->
115-
MintHTTP.connect(
116-
scheme,
117-
host,
118-
port,
119-
Keyword.merge([protocols: [:http1]], connect_opts)
120-
)
121-
end
122-
123-
pid = get_or_start_pool(key, :http1, connect_fun, [])
124-
125-
HTTP1.request(pid, method, path, headers, body,
126-
timeout: timeout,
127-
initial_conn: initial_conn
128-
)
129-
end
130-
131-
defp request_http2({scheme, host, port} = key, path, method, headers, body, opts, initial_conn) do
13249
connect_opts = Keyword.get(opts, :connect_opts, [])
133-
timeout = Keyword.get(opts, :timeout, 15_000)
134-
135-
connect_fun = fn ->
136-
MintHTTP.connect(
137-
scheme,
138-
host,
139-
port,
140-
Keyword.merge([protocols: [:http2]], connect_opts)
141-
)
142-
end
14350

144-
pid =
145-
get_or_start_pool(key, :http2, connect_fun, initial_conn: initial_conn)
146-
147-
HTTP2.request(pid, method, path, headers, body, timeout)
51+
pid = get_or_start_host(key, connect_opts)
52+
Host.request(pid, method, path, headers, body, timeout)
14853
end
14954

150-
defp get_or_start_pool(key, protocol, connect_fun, extra_opts) do
151-
reg_key = {protocol, key}
152-
153-
case Registry.lookup(@registry, reg_key) do
154-
[{pid, _}] ->
155-
pid
156-
157-
[] ->
158-
start_pool(reg_key, protocol, key, connect_fun, extra_opts)
55+
defp get_or_start_host(key, connect_opts) do
56+
case Registry.lookup(@registry, key) do
57+
[{pid, _}] -> pid
58+
[] -> start_host(key, connect_opts)
15959
end
16060
end
16161

162-
defp start_pool(reg_key, protocol, key, connect_fun, extra_opts) do
163-
module =
164-
case protocol do
165-
:http1 -> HTTP1
166-
:http2 -> HTTP2
167-
end
168-
169-
via = {:via, Registry, {@registry, reg_key}}
170-
arg = {key, connect_fun, Keyword.put(extra_opts, :name, via)}
62+
defp start_host(key, connect_opts) do
63+
via = {:via, Registry, {@registry, key}}
64+
arg = {key, connect_opts, [name: via]}
17165

17266
spec = %{
173-
id: {module, reg_key},
174-
start: {module, :start_link, [arg]},
67+
id: {Host, key},
68+
start: {Host, :start_link, [arg]},
17569
restart: :temporary
17670
}
17771

@@ -183,22 +77,14 @@ defmodule Hex.HTTP.Pool do
18377
pid
18478

18579
{:error, reason} ->
186-
case Registry.lookup(@registry, reg_key) do
187-
[{pid, _}] -> pid
188-
[] -> raise "failed to start HTTP pool for #{inspect(reg_key)}: #{inspect(reason)}"
189-
end
190-
end
191-
end
80+
case Registry.lookup(@registry, key) do
81+
[{pid, _}] ->
82+
pid
19283

193-
defp stop_pool({_, _, _} = key) do
194-
for protocol <- [:http1, :http2] do
195-
case Registry.lookup(@registry, {protocol, key}) do
196-
[{pid, _}] -> DynamicSupervisor.terminate_child(@dyn_sup, pid)
197-
[] -> :ok
198-
end
84+
[] ->
85+
raise "failed to start Hex HTTP host pool for #{inspect(key)}: #{inspect(reason)}"
86+
end
19987
end
200-
201-
:ets.delete(@ets, key)
20288
end
20389

20490
## URL parsing
@@ -225,6 +111,4 @@ defmodule Hex.HTTP.Pool do
225111
# For tests / debugging
226112
@doc false
227113
def __registry__, do: @registry
228-
@doc false
229-
def __ets__, do: @ets
230114
end

0 commit comments

Comments
 (0)