From 0f257d06b6a4b1170621af66e9b54addf4d8e954 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= <essen@ninenines.eu>
Date: Fri, 7 Feb 2025 16:57:58 +0100
Subject: Add hibernate option to cowboy_http and cowboy_http2

When enabled the connection process will automatically hibernate.
Because hibernation triggers GC, this can be used as a way to
keep memory usage lower, at the cost of performance.
---
 src/cowboy_http.erl  | 39 ++++++++++++++++++++++++---------------
 src/cowboy_http2.erl | 41 +++++++++++++++++++++++++----------------
 2 files changed, 49 insertions(+), 31 deletions(-)

(limited to 'src')

diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl
index 1914454..f940e47 100644
--- a/src/cowboy_http.erl
+++ b/src/cowboy_http.erl
@@ -17,6 +17,7 @@
 -module(cowboy_http).
 
 -export([init/6]).
+-export([loop/1]).
 
 -export([system_continue/3]).
 -export([system_terminate/4]).
@@ -32,6 +33,7 @@
 	dynamic_buffer_initial_average => non_neg_integer(),
 	dynamic_buffer_initial_size => pos_integer(),
 	env => cowboy_middleware:env(),
+	hibernate => boolean(),
 	http10_keepalive => boolean(),
 	idle_timeout => timeout(),
 	inactivity_timeout => timeout(),
@@ -192,7 +194,7 @@ init(Parent, Ref, Socket, Transport, ProxyHeader, Opts) ->
 		dynamic_buffer_moving_average=maps:get(dynamic_buffer_initial_average, Opts, 0),
 		last_streamid=maps:get(max_keepalive, Opts, 1000)},
 	safe_setopts_active(State),
-	loop(set_timeout(State, request_timeout)).
+	before_loop(set_timeout(State, request_timeout)).
 
 -include("cowboy_dynamic_buffer.hrl").
 
@@ -223,6 +225,13 @@ flush_passive(Socket, Messages) ->
 		ok
 	end.
 
+before_loop(State=#state{opts=#{hibernate := true}}) ->
+	proc_lib:hibernate(?MODULE, loop, [State]);
+before_loop(State) ->
+	loop(State).
+
+-spec loop(#state{}) -> ok.
+
 loop(State=#state{parent=Parent, socket=Socket, transport=Transport, opts=Opts,
 		buffer=Buffer, timer=TimerRef, children=Children, in_streamid=InStreamID,
 		last_streamid=LastStreamID}) ->
@@ -233,7 +242,7 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport, opts=Opts,
 		%% we want to process was received fully.
 		{OK, Socket, Data} when OK =:= element(1, Messages), InStreamID > LastStreamID ->
 			State1 = maybe_resize_buffer(State, Data),
-			loop(State1);
+			before_loop(State1);
 		%% Socket messages.
 		{OK, Socket, Data} when OK =:= element(1, Messages) ->
 			State1 = maybe_resize_buffer(State, Data),
@@ -246,37 +255,37 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport, opts=Opts,
 				%% Hardcoded for compatibility with Ranch 1.x.
 				Passive =:= tcp_passive; Passive =:= ssl_passive ->
 			safe_setopts_active(State),
-			loop(State);
+			before_loop(State);
 		%% Timeouts.
 		{timeout, Ref, {shutdown, Pid}} ->
 			cowboy_children:shutdown_timeout(Children, Ref, Pid),
-			loop(State);
+			before_loop(State);
 		{timeout, TimerRef, Reason} ->
 			timeout(State, Reason);
 		{timeout, _, _} ->
-			loop(State);
+			before_loop(State);
 		%% System messages.
 		{'EXIT', Parent, shutdown} ->
 			Reason = {stop, {exit, shutdown}, 'Parent process requested shutdown.'},
-			loop(initiate_closing(State, Reason));
+			before_loop(initiate_closing(State, Reason));
 		{'EXIT', Parent, Reason} ->
 			terminate(State, {stop, {exit, Reason}, 'Parent process terminated.'});
 		{system, From, Request} ->
 			sys:handle_system_msg(Request, From, Parent, ?MODULE, [], State);
 		%% Messages pertaining to a stream.
 		{{Pid, StreamID}, Msg} when Pid =:= self() ->
-			loop(info(State, StreamID, Msg));
+			before_loop(info(State, StreamID, Msg));
 		%% Exit signal from children.
 		Msg = {'EXIT', Pid, _} ->
-			loop(down(State, Pid, Msg));
+			before_loop(down(State, Pid, Msg));
 		%% Calls from supervisor module.
 		{'$gen_call', From, Call} ->
 			cowboy_children:handle_supervisor_call(Call, From, Children, ?MODULE),
-			loop(State);
+			before_loop(State);
 		%% Unknown messages.
 		Msg ->
 			cowboy:log(warning, "Received stray message ~p.~n", [Msg], Opts),
-			loop(State)
+			before_loop(State)
 	after InactivityTimeout ->
 		terminate(State, {internal_error, timeout, 'No message or data received before timeout.'})
 	end.
@@ -362,12 +371,12 @@ timeout(State, idle_timeout) ->
 		'Connection idle longer than configuration allows.'}).
 
 parse(<<>>, State) ->
-	loop(State#state{buffer= <<>>});
+	before_loop(State#state{buffer= <<>>});
 %% Do not process requests that come in after the last request
 %% and discard the buffer if any to save memory.
 parse(_, State=#state{in_streamid=InStreamID, in_state=#ps_request_line{},
 		last_streamid=LastStreamID}) when InStreamID > LastStreamID ->
-	loop(State#state{buffer= <<>>});
+	before_loop(State#state{buffer= <<>>});
 parse(Buffer, State=#state{in_state=#ps_request_line{empty_lines=EmptyLines}}) ->
 	after_parse(parse_request(Buffer, State, EmptyLines));
 parse(Buffer, State=#state{in_state=PS=#ps_header{headers=Headers, name=undefined}}) ->
@@ -442,7 +451,7 @@ after_parse({data, _, IsFin, _, State=#state{buffer=Buffer}}) ->
 		nofin -> idle_timeout
 	end));
 after_parse({more, State}) ->
-	loop(set_timeout(State, idle_timeout)).
+	before_loop(set_timeout(State, idle_timeout)).
 
 update_flow(fin, _, State) ->
 	%% This function is only called after parsing, therefore we
@@ -1622,12 +1631,12 @@ terminate_linger_loop(State=#state{socket=Socket}, TimerRef, Messages) ->
 
 -spec system_continue(_, _, #state{}) -> ok.
 system_continue(_, _, State) ->
-	loop(State).
+	before_loop(State).
 
 -spec system_terminate(any(), _, _, #state{}) -> no_return().
 system_terminate(Reason0, _, _, State) ->
 	Reason = {stop, {exit, Reason0}, 'sys:terminate/2,3 was called.'},
-	loop(initiate_closing(State, Reason)).
+	before_loop(initiate_closing(State, Reason)).
 
 -spec system_code_change(Misc, _, _, _) -> {ok, Misc} when Misc::{#state{}, binary()}.
 system_code_change(Misc, _, _, _) ->
diff --git a/src/cowboy_http2.erl b/src/cowboy_http2.erl
index a6ffca7..207a967 100644
--- a/src/cowboy_http2.erl
+++ b/src/cowboy_http2.erl
@@ -17,6 +17,7 @@
 -export([init/6]).
 -export([init/10]).
 -export([init/12]).
+-export([loop/2]).
 
 -export([system_continue/3]).
 -export([system_terminate/4]).
@@ -36,6 +37,7 @@
 	env => cowboy_middleware:env(),
 	goaway_initial_timeout => timeout(),
 	goaway_complete_timeout => timeout(),
+	hibernate => boolean(),
 	idle_timeout => timeout(),
 	inactivity_timeout => timeout(),
 	initial_connection_window_size => 65535..16#7fffffff,
@@ -188,7 +190,7 @@ init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, Buffer
 		http2_status=sequence, http2_machine=HTTP2Machine}), 0),
 	safe_setopts_active(State),
 	case Buffer of
-		<<>> -> loop(State, Buffer);
+		<<>> -> before_loop(State, Buffer);
 		_ -> parse(State, Buffer)
 	end.
 
@@ -250,7 +252,7 @@ init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, Buffer
 	ok = maybe_socket_error(State, Transport:send(Socket, Preface)),
 	safe_setopts_active(State),
 	case Buffer of
-		<<>> -> loop(State, Buffer);
+		<<>> -> before_loop(State, Buffer);
 		_ -> parse(State, Buffer)
 	end.
 
@@ -267,6 +269,13 @@ setopts_active(#state{socket=Socket, transport=Transport, opts=Opts}) ->
 safe_setopts_active(State) ->
 	ok = maybe_socket_error(State, setopts_active(State)).
 
+before_loop(State=#state{opts=#{hibernate := true}}, Buffer) ->
+	proc_lib:hibernate(?MODULE, loop, [State, Buffer]);
+before_loop(State, Buffer) ->
+	loop(State, Buffer).
+
+-spec loop(#state{}, binary()) -> ok.
+
 loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
 		opts=Opts, timer=TimerRef, children=Children}, Buffer) ->
 	Messages = Transport:messages(),
@@ -288,11 +297,11 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
 				%% Hardcoded for compatibility with Ranch 1.x.
 				Passive =:= tcp_passive; Passive =:= ssl_passive ->
 			safe_setopts_active(State),
-			loop(State, Buffer);
+			before_loop(State, Buffer);
 		%% System messages.
 		{'EXIT', Parent, shutdown} ->
 			Reason = {stop, {exit, shutdown}, 'Parent process requested shutdown.'},
-			loop(initiate_closing(State, Reason), Buffer);
+			before_loop(initiate_closing(State, Reason), Buffer);
 		{'EXIT', Parent, Reason} ->
 			terminate(State, {stop, {exit, Reason}, 'Parent process terminated.'});
 		{system, From, Request} ->
@@ -302,27 +311,27 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
 			tick_idle_timeout(State, Buffer);
 		{timeout, Ref, {shutdown, Pid}} ->
 			cowboy_children:shutdown_timeout(Children, Ref, Pid),
-			loop(State, Buffer);
+			before_loop(State, Buffer);
 		{timeout, TRef, {cow_http2_machine, Name}} ->
-			loop(timeout(State, Name, TRef), Buffer);
+			before_loop(timeout(State, Name, TRef), Buffer);
 		{timeout, TimerRef, {goaway_initial_timeout, Reason}} ->
-			loop(closing(State, Reason), Buffer);
+			before_loop(closing(State, Reason), Buffer);
 		{timeout, TimerRef, {goaway_complete_timeout, Reason}} ->
 			terminate(State, {stop, stop_reason(Reason),
 				'Graceful shutdown timed out.'});
 		%% Messages pertaining to a stream.
 		{{Pid, StreamID}, Msg} when Pid =:= self() ->
-			loop(info(State, StreamID, Msg), Buffer);
+			before_loop(info(State, StreamID, Msg), Buffer);
 		%% Exit signal from children.
 		Msg = {'EXIT', Pid, _} ->
-			loop(down(State, Pid, Msg), Buffer);
+			before_loop(down(State, Pid, Msg), Buffer);
 		%% Calls from supervisor module.
 		{'$gen_call', From, Call} ->
 			cowboy_children:handle_supervisor_call(Call, From, Children, ?MODULE),
-			loop(State, Buffer);
+			before_loop(State, Buffer);
 		Msg ->
 			cowboy:log(warning, "Received stray message ~p.", [Msg], Opts),
-			loop(State, Buffer)
+			before_loop(State, Buffer)
 	after InactivityTimeout ->
 		terminate(State, {internal_error, timeout, 'No message or data received before timeout.'})
 	end.
@@ -331,7 +340,7 @@ tick_idle_timeout(State=#state{idle_timeout_num=?IDLE_TIMEOUT_TICKS}, _) ->
 	terminate(State, {stop, timeout,
 		'Connection idle longer than configuration allows.'});
 tick_idle_timeout(State=#state{idle_timeout_num=TimeoutNum}, Buffer) ->
-	loop(set_idle_timeout(State, TimeoutNum + 1), Buffer).
+	before_loop(set_idle_timeout(State, TimeoutNum + 1), Buffer).
 
 set_idle_timeout(State=#state{http2_status=Status, timer=TimerRef}, _)
 		when Status =:= closing_initiated orelse Status =:= closing,
@@ -372,7 +381,7 @@ parse(State=#state{http2_status=sequence}, Data) ->
 		{ok, Rest} ->
 			parse(State#state{http2_status=settings}, Rest);
 		more ->
-			loop(State, Data);
+			before_loop(State, Data);
 		Error = {connection_error, _, _} ->
 			terminate(State, Error)
 	end;
@@ -391,7 +400,7 @@ parse(State=#state{http2_status=Status, http2_machine=HTTP2Machine, streams=Stre
 		more when Status =:= closing, Streams =:= #{} ->
 			terminate(State, {stop, normal, 'The connection is going away.'});
 		more ->
-			loop(State, Data)
+			before_loop(State, Data)
 	end.
 
 %% Frame rate flood protection.
@@ -1379,12 +1388,12 @@ terminate_stream_handler(#state{opts=Opts}, StreamID, Reason, StreamState) ->
 
 -spec system_continue(_, _, {#state{}, binary()}) -> ok.
 system_continue(_, _, {State, Buffer}) ->
-	loop(State, Buffer).
+	before_loop(State, Buffer).
 
 -spec system_terminate(any(), _, _, {#state{}, binary()}) -> no_return().
 system_terminate(Reason0, _, _, {State, Buffer}) ->
 	Reason = {stop, {exit, Reason0}, 'sys:terminate/2,3 was called.'},
-	loop(initiate_closing(State, Reason), Buffer).
+	before_loop(initiate_closing(State, Reason), Buffer).
 
 -spec system_code_change(Misc, _, _, _) -> {ok, Misc} when Misc::{#state{}, binary()}.
 system_code_change(Misc, _, _, _) ->
-- 
cgit v1.2.3