).
with_connection_loop(Context, Socket, Kont) :-
- catch(
- setup_call_cleanup(
- (
- log_msg("tcp", "Accepting connections...~n", []),
- socket_server_accept(Socket, Client, S0, []),
- log_msg("tcp", "Connected client ~q~n", [Client])
+ % Failure-driven loop: Scryer has no heap GC and only reclaims the global
+ % stack on backtracking. Handling each connection inside `repeat`/`fail`
+ % means we backtrack to `repeat` after every request, which truncates the
+ % heap (including the whole file slurped by phrase_from_file) back to a
+ % fixed point. A plain recursive loop would never reclaim it and RSS would
+ % grow without bound. The if-then-else commits the connection handler's
+ % choicepoints whether it succeeds or fails, then `fail` triggers the
+ % heap-reclaiming backtrack to `repeat`.
+ %
+ % NOTE: a small residual growth (~4-8 KB/request) remains and is expected.
+ % Each connection allocates TCP + TLS stream objects in Scryer's arena,
+ % which is append-only and only freed at process exit (no incremental
+ % sweep, and backtracking does not reclaim it). This is a Scryer
+ % limitation, not a bug here; mitigate operationally (periodic restart or
+ % a systemd MemoryMax= with Restart=always).
+ repeat,
+ ( catch(
+ setup_call_cleanup(
+ (
+ log_msg("tcp", "Accepting connections...~n", []),
+ socket_server_accept(Socket, Client, S0, []),
+ log_msg("tcp", "Connected client ~q~n", [Client])
+ ),
+ with_tls_connection(S0, Context, Kont),
+ ( close(S0),
+ log_msg("tcp", "Closed connection for client ~q~n", [Client])
+ )
),
- with_tls_connection(S0, Context, Kont),
- ( close(S0),
- log_msg("tcp", "Closed connection for client ~q~n", [Client])
- )
- ),
- Error,
- handle_conn_error(Error)
+ Error,
+ handle_conn_error(Error)
+ )
+ -> true
+ ; true
),
- with_connection_loop(Context, Socket, Kont).
+ fail.
handle_conn_error(error(permission_error(open, source_sink, _), tls_server_negotiate/3)) :- !,
log_msg("error", "TLS handshake failed~n", []).