Packages

HTTP/1.1, HTTP/2, and HTTP/3 server for Erlang/OTP 27+

Current section

Files

Jump to
nhttp src nhttp_stream_worker.erl
Raw

src/nhttp_stream_worker.erl

-module(nhttp_stream_worker).
-moduledoc false.
%%%-----------------------------------------------------------------------------
%% API
%%%-----------------------------------------------------------------------------
-export([
reap/1,
start/3,
start_request/6,
stop/1
]).
%%%-----------------------------------------------------------------------------
%% PROC_LIB ENTRY
%%%-----------------------------------------------------------------------------
-export([
reaper/1,
run/3,
run_request/6
]).
%%%-----------------------------------------------------------------------------
%% LOCAL MACROS
%%%-----------------------------------------------------------------------------
-define(TERMINAL_KEY, '$nhttp_stream_terminal').
-define(REAP_GRACE, 5000).
%%%-----------------------------------------------------------------------------
%% API
%%%-----------------------------------------------------------------------------
-doc """
Reap outstanding workers on connection teardown without blocking the
caller.
Spawns a detached reaper that monitors each worker and waits up to the
grace window. A worker that exits on its own (a graceful producer
reacting to the conn's `DOWN`, or one already finished) is dropped; any
survivor past the grace is `kill`ed. This preserves a producer's chance
to clean up on disconnect while guaranteeing a stuck worker cannot
orphan.
""".
-spec reap([pid()]) -> ok.
reap([]) ->
ok;
reap(Pids) ->
_Reaper = spawn(?MODULE, reaper, [Pids]),
ok.
-doc """
Spawn a worker to run `Producer(SendChunk)`.
The worker process owns the producer's execution. The connection process
(`ConnPid`) owns the socket and applies wire-level flow control. Messages
between worker and conn are tagged with `Ref`.
The worker is spawned with `proc_lib:spawn/3` (no link): a producer crash
does not propagate to the connection. The connection should monitor the
returned pid to learn when the worker exits.
Monitoring alone only reaps a worker that is parked at a `receive` (it
sees the conn's `DOWN` and exits). A producer stuck in handler code (a
blocked external call, an infinite loop) never reaches a `receive`, so
it would outlive conn death, drain, and application shutdown. The
connection therefore `reap/1`s every outstanding worker on termination:
graceful workers exit on the conn's `DOWN` within the grace window and
are dropped, while stuck survivors are `kill`ed. See
`nhttp_conn:terminate/2`, `nhttp_conn_h3:terminate/2`, and the h1 push
teardown.
""".
-spec start(pid(), reference(), nhttp_stream:producer()) -> pid().
start(ConnPid, Ref, Producer) ->
proc_lib:spawn(?MODULE, run, [ConnPid, Ref, Producer]).
-doc """
Spawn a worker to run `Handler:handle_request(Request, HState)` and post
the result to the conn as `{request_result, StreamId, Ref, Result}`.
For `{reply, _, _}`, `{abort, _, _}`, and exception results the worker
posts the result and exits. For `{stream, Spec, _}` streaming results
the worker posts the result then enters the iterator/producer loop,
sending chunks via `{send_chunk, ...}` messages and blocking on
`chunk_ack` for backpressure. For `{accept_body, _, _}` results the
worker posts the result and waits for `{body_chunk, Ref, Event}`
messages from the conn, dispatching each into
`Handler:handle_request_body/3`.
Exceptions raised by streaming callbacks are surfaced via the worker's
exit reason. The conn monitors the pid and emits the appropriate
response.
""".
-spec start_request(
pid(), nhttp_lib:stream_id(), reference(), module(), nhttp_lib:request(), term()
) ->
pid().
start_request(ConnPid, StreamId, Ref, Handler, Request, HState) ->
proc_lib:spawn(?MODULE, run_request, [ConnPid, StreamId, Ref, Handler, Request, HState]).
-doc """
Unconditionally `kill` a single worker.
Used where the connection has already given the worker its chance to
finish (the h1 push pipeline hand-off) and only needs to reap a stuck
survivor. For teardown of a worker map use `reap/1`, which preserves
graceful cleanup.
""".
-spec stop(pid()) -> ok.
stop(WorkerPid) ->
true = exit(WorkerPid, kill),
ok.
%%%-----------------------------------------------------------------------------
%% PROC_LIB ENTRY
%%%-----------------------------------------------------------------------------
-spec reaper([pid()]) -> ok.
reaper(Pids) ->
MRefs = maps:from_list([{erlang:monitor(process, P), P} || P <- Pids]),
reaper_wait(MRefs).
-spec run(pid(), reference(), nhttp_stream:producer()) -> ok.
run(ConnPid, Ref, Producer) ->
MRef = erlang:monitor(process, ConnPid),
SendChunk = fun(Data) -> send_chunk(ConnPid, Ref, MRef, Data) end,
finalize_producer(ConnPid, Ref, MRef, Producer(SendChunk)).
-spec run_request(
pid(), nhttp_lib:stream_id(), reference(), module(), nhttp_lib:request(), term()
) -> ok.
run_request(ConnPid, StreamId, Ref, Handler, Request, HState) ->
MRef = erlang:monitor(process, ConnPid),
Result = nhttp_handler:safe_handle_request(Handler, Request, HState),
dispatch_result(ConnPid, StreamId, Ref, MRef, Handler, Result, HState).
%%%-----------------------------------------------------------------------------
%% INTERNAL FUNCTIONS
%%%-----------------------------------------------------------------------------
-spec ack_body_chunk(pid(), reference()) -> ok.
ack_body_chunk(ConnPid, Ref) ->
ConnPid ! {body_chunk_ack, self(), Ref},
ok.
-spec dispatch_result(
pid(),
nhttp_lib:stream_id(),
reference(),
reference(),
module(),
term(),
term()
) -> ok.
dispatch_result(ConnPid, StreamId, Ref, _MRef, _Handler, {reply, _, _} = Result, _HState) ->
post_result(ConnPid, StreamId, Ref, Result),
ok;
dispatch_result(
ConnPid, StreamId, Ref, MRef, _Handler, {stream, Spec, _NewHState} = Result, _HState
) ->
post_result(ConnPid, StreamId, Ref, Result),
run_stream_spec(ConnPid, Ref, MRef, Spec);
dispatch_result(
ConnPid, StreamId, Ref, MRef, Handler, {accept_body, _, _} = Result, _HState
) ->
post_result(ConnPid, StreamId, Ref, Result),
run_body(ConnPid, StreamId, Ref, MRef, Handler, Result);
dispatch_result(ConnPid, StreamId, Ref, _MRef, _Handler, {upgrade, websocket, _} = Result, _HState) ->
post_result(ConnPid, StreamId, Ref, Result),
ok;
dispatch_result(
ConnPid, StreamId, Ref, _MRef, _Handler, {upgrade, websocket, _, _} = Result, _HState
) ->
post_result(ConnPid, StreamId, Ref, Result),
ok;
dispatch_result(ConnPid, StreamId, Ref, _MRef, _Handler, {abort, _, _} = Result, _HState) ->
post_result(ConnPid, StreamId, Ref, Result),
ok;
dispatch_result(
ConnPid, StreamId, Ref, _MRef, _Handler, {nhttp_handler_exception, _, _} = Exc, _HState
) ->
post_result(ConnPid, StreamId, Ref, Exc),
ok;
dispatch_result(ConnPid, StreamId, Ref, _MRef, _Handler, Other, HState) ->
post_result(ConnPid, StreamId, Ref, {abort, {bad_handler_return, Other}, HState}),
ok.
-spec finalize_producer(
pid(),
reference(),
reference(),
ok | {trailers, nhttp_lib:headers()} | {error, term()} | term()
) -> ok.
finalize_producer(ConnPid, Ref, MRef, {trailers, Trailers}) when is_list(Trailers) ->
send_trailers(ConnPid, Ref, MRef, Trailers);
finalize_producer(ConnPid, Ref, MRef, _Other) ->
finish(ConnPid, Ref, MRef).
-spec finish(pid(), reference(), reference()) -> ok.
finish(ConnPid, Ref, MRef) ->
case get(?TERMINAL_KEY) of
closed ->
ok;
_ ->
ConnPid ! {stream_done, self(), Ref},
receive
{chunk_ack, Ref, _Result} ->
ok;
{'DOWN', MRef, process, ConnPid, _Reason} ->
ok
end
end.
-spec post_result(pid(), nhttp_lib:stream_id(), reference(), term()) -> ok.
post_result(ConnPid, StreamId, Ref, Result) ->
ConnPid ! {request_result, StreamId, Ref, Result},
ok.
-spec reaper_wait(#{reference() => pid()}) -> ok.
reaper_wait(MRefs) when map_size(MRefs) =:= 0 ->
ok;
reaper_wait(MRefs) ->
receive
{'DOWN', MRef, process, _Pid, _Reason} ->
reaper_wait(maps:remove(MRef, MRefs))
after ?REAP_GRACE ->
ok = lists:foreach(fun(Pid) -> exit(Pid, kill) end, maps:values(MRefs))
end.
-spec run_body(
pid(),
nhttp_lib:stream_id(),
reference(),
reference(),
module(),
{accept_body, term(), term()}
) -> ok.
run_body(ConnPid, StreamId, Ref, MRef, Handler, {accept_body, BodyState, HState}) ->
receive
{body_chunk, Ref, Event} ->
ack_body_chunk(ConnPid, Ref),
Result = nhttp_handler:safe_handle_request_body(
Handler, Event, BodyState, HState
),
case Result of
{accept_body, _NewBodyState, _NewHState} = Next ->
case Event of
{fin, _} ->
post_result(
ConnPid, StreamId, Ref, {abort, body_unconsumed, element(3, Next)}
),
ok;
{abort, _} ->
post_result(
ConnPid, StreamId, Ref, {abort, peer_aborted, element(3, Next)}
),
ok;
_ ->
run_body(ConnPid, StreamId, Ref, MRef, Handler, Next)
end;
{nhttp_handler_exception, _, _} = Exc ->
post_result(ConnPid, StreamId, Ref, Exc),
ok;
Final ->
dispatch_result(ConnPid, StreamId, Ref, MRef, Handler, Final, HState)
end;
{'DOWN', MRef, process, ConnPid, _Reason} ->
ok
end.
-spec run_stream_spec(
pid(), reference(), reference(), nhttp_stream:spec()
) -> ok.
run_stream_spec(ConnPid, Ref, MRef, {producer, _Status, _Headers, Producer}) ->
SendChunk = fun(Data) -> send_chunk(ConnPid, Ref, MRef, Data) end,
finalize_producer(ConnPid, Ref, MRef, Producer(SendChunk)).
-spec send_chunk(pid(), reference(), reference(), iodata()) ->
ok | {error, closed | timeout}.
send_chunk(ConnPid, Ref, MRef, Data) ->
case get(?TERMINAL_KEY) of
closed ->
{error, closed};
_ ->
ConnPid ! {send_chunk, self(), Ref, Data},
receive
{chunk_ack, Ref, ok} ->
ok;
{chunk_ack, Ref, {error, _Reason} = Err} ->
put(?TERMINAL_KEY, closed),
Err;
{'DOWN', MRef, process, ConnPid, _Reason} ->
put(?TERMINAL_KEY, closed),
{error, closed}
end
end.
-spec send_trailers(pid(), reference(), reference(), nhttp_lib:headers()) -> ok.
send_trailers(ConnPid, Ref, MRef, TrailerHeaders) ->
case get(?TERMINAL_KEY) of
closed ->
ok;
_ ->
ConnPid ! {send_trailers, self(), Ref, TrailerHeaders},
receive
{chunk_ack, Ref, _Result} ->
ok;
{'DOWN', MRef, process, ConnPid, _Reason} ->
ok
end
end.