diff --git a/src/hackney_h3.erl b/src/hackney_h3.erl index acb1a9e3..90233af1 100644 --- a/src/hackney_h3.erl +++ b/src/hackney_h3.erl @@ -30,6 +30,8 @@ -include("hackney_lib.hrl"). -export([ + %% Connection table, created by hackney_sup + init_table/0, %% High-level API is_available/0, request/2, request/3, request/4, request/5, @@ -788,7 +790,7 @@ init({Host, Port, Opts, Owner}) -> case quic_h3:connect(Host, Port, H3Opts) of {ok, H3Conn} -> ConnRef = make_ref(), - _ = ensure_table(), + ok = init_table(), ets:insert(?CONN_TABLE, {ConnRef, self()}), {ok, #state{h3_conn = H3Conn, conn_ref = ConnRef, @@ -1005,12 +1007,19 @@ cacertfile_ders(File) -> {error, _} -> [] end. -ensure_table() -> +%% @doc Create the table mapping connection refs to their processes. hackney_sup +%% calls this at start so the table outlives every connection: owned by the +%% first connection, it vanished when that one closed and took every other +%% live connection's entry with it. The call in init/1 only covers hackney_h3 +%% used without the hackney application. +-spec init_table() -> ok. +init_table() -> case ets:whereis(?CONN_TABLE) of undefined -> try - ets:new(?CONN_TABLE, - [named_table, public, set, {read_concurrency, true}]) + _ = ets:new(?CONN_TABLE, + [named_table, public, set, {read_concurrency, true}]), + ok catch error:badarg -> ok end; diff --git a/src/hackney_sup.erl b/src/hackney_sup.erl index 7cd7acdb..6d111c20 100644 --- a/src/hackney_sup.erl +++ b/src/hackney_sup.erl @@ -45,6 +45,9 @@ init([]) -> %% initialize the TLS options key memo table ok = hackney_ssl:init_key_cache(), + %% initialize the HTTP/3 connection table + ok = hackney_h3:init_table(), + Specs = [ %% manager ?CHILD(hackney_manager, worker), diff --git a/test/hackney_h3_low_level_tests.erl b/test/hackney_h3_low_level_tests.erl index 876b18e8..6d5c23fa 100644 --- a/test/hackney_h3_low_level_tests.erl +++ b/test/hackney_h3_low_level_tests.erl @@ -111,9 +111,27 @@ quic_connection_test_() -> {"QUIC connection tests", with_server([ {"Connect to a local server", fun test_local_connect/1}, - {"Test stream opening", fun test_open_stream/1} + {"Test stream opening", fun test_open_stream/1}, + {"Closing a connection leaves the others usable", + fun test_close_keeps_other_conns/1} ])}. +%% The connection table belongs to hackney_sup. Owned by the first connection, +%% it went away when that one closed, and every other live connection could no +%% longer be reached. +test_close_keeps_other_conns(Server) -> + ?assertEqual(whereis(hackney_sup), ets:info(hackney_h3_conns, owner)), + {ok, RefA} = connect(Server), + {ok, _} = wait_connected(RefA), + {ok, RefB} = connect(Server), + {ok, _} = wait_connected(RefB), + [{RefA, PidA}] = ets:lookup(hackney_h3_conns, RefA), + Mon = erlang:monitor(process, PidA), + hackney_h3:close(RefA, normal), + receive {'DOWN', Mon, process, PidA, _} -> ok after 5000 -> error(not_closed) end, + ?assertMatch({ok, _}, hackney_h3:peername(RefB)), + hackney_h3:close(RefB, normal). + test_local_connect(Server) -> {ok, ConnRef} = connect(Server), ConnResult = wait_connected(ConnRef),