From 8aa1b907d1a902c5defdfd78f8ae556c12e79f70 Mon Sep 17 00:00:00 2001 From: Yawar Amin Date: Tue, 9 Sep 2025 11:51:25 -0400 Subject: [PATCH 1/5] Stop server on SIGTERM by default Fix #174 --- example/z-playground/server/playground.ml | 8 +------- src/dream.mli | 2 +- src/http/http.ml | 9 ++++++--- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/example/z-playground/server/playground.ml b/example/z-playground/server/playground.ml index 191c5d04..b45b9561 100644 --- a/example/z-playground/server/playground.ml +++ b/example/z-playground/server/playground.ml @@ -479,12 +479,6 @@ let rec gc ?(initial = true) () = let () = Dream.log "Starting playground"; - (* Stop when systemd sends SIGTERM. *) - let stop, signal_stop = Lwt.wait () in - Lwt_unix.on_signal Sys.sigterm (fun _signal -> - Lwt.wakeup_later signal_stop ()) - |> ignore; - (* Build the base image. *) Lwt_main.run begin Lwt_io.(with_file ~mode:Output "Dockerfile" (fun channel -> @@ -520,7 +514,7 @@ let () = Dream.html (Client.html example) in - Dream.run ~interface:"0.0.0.0" ~port:80 ~stop ~adjust_terminal:false + Dream.run ~interface:"0.0.0.0" ~port:80 ~adjust_terminal:false @@ Dream.logger @@ Dream.router [ diff --git a/src/dream.mli b/src/dream.mli index 9a1bd5dd..d803e4bb 100644 --- a/src/dream.mli +++ b/src/dream.mli @@ -2130,7 +2130,7 @@ val run : - [~stop] is a promise that causes the server to stop accepting new requests, and {!Dream.run} to return. Requests that have already entered the Web application continue to be processed. The default value is a - promise that never resolves. + promise that resolves when the [TERM] signal is received. - [~error_handler] handles all errors, both from the application, and low-level errors. See {!section-errors} and example {{:https://github.com/camlworks/dream/tree/master/example/9-error#folders-and-files} diff --git a/src/http/http.ml b/src/http/http.ml index c1054ea2..3d7359db 100644 --- a/src/http/http.ml +++ b/src/http/http.ml @@ -673,7 +673,10 @@ let serve_with_maybe_https let default_interface = "localhost" let default_port = 8080 -let never = fst (Lwt.wait ()) +let on_sigterm = + let promise, resolve = Lwt.wait () in + ignore (Lwt_unix.on_signal Sys.sigterm (fun _ -> Lwt.wakeup_later signal_stop ())); + promise let network ~port ~socket_path = match socket_path with @@ -684,7 +687,7 @@ let serve ?(interface = default_interface) ?(port = default_port) ?socket_path - ?(stop = never) + ?(stop = on_sigterm) ?(error_handler = Error_handler.default) ?(tls = false) ?certificate_file @@ -712,7 +715,7 @@ let run ?(interface = default_interface) ?(port = default_port) ?socket_path - ?(stop = never) + ?(stop = on_sigterm) ?(error_handler = Error_handler.default) ?(tls = false) ?certificate_file From e2d19b1b5ee308f6ed8a08a8761f25f9083a06c6 Mon Sep 17 00:00:00 2001 From: Yawar Amin Date: Tue, 9 Sep 2025 13:09:09 -0400 Subject: [PATCH 2/5] Fix variable name --- src/http/http.ml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/http/http.ml b/src/http/http.ml index 3d7359db..f9bbaea8 100644 --- a/src/http/http.ml +++ b/src/http/http.ml @@ -675,7 +675,7 @@ let default_interface = "localhost" let default_port = 8080 let on_sigterm = let promise, resolve = Lwt.wait () in - ignore (Lwt_unix.on_signal Sys.sigterm (fun _ -> Lwt.wakeup_later signal_stop ())); + ignore (Lwt_unix.on_signal Sys.sigterm (fun _ -> Lwt.wakeup_later resolve ())); promise let network ~port ~socket_path = From 2e4ed9881e8c1e3d273e19ca17bac4206cf48315 Mon Sep 17 00:00:00 2001 From: Yawar Amin Date: Fri, 3 Oct 2025 13:01:59 -0400 Subject: [PATCH 3/5] Handle SIGTERM only inside Dream server Don't install the signal handler at the module scope, to avoid polluting the user's application with potentially unused handlers. --- src/http/http.ml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/http/http.ml b/src/http/http.ml index f9bbaea8..b446e4e2 100644 --- a/src/http/http.ml +++ b/src/http/http.ml @@ -673,7 +673,14 @@ let serve_with_maybe_https let default_interface = "localhost" let default_port = 8080 -let on_sigterm = + +(* Lazy option default to avoid side effects. *) +let option_or opt default = match opt with + | Some v -> v + | None -> default () + +(* Lazy SIGTERM handler to avoid side effects. *) +let on_sigterm () = let promise, resolve = Lwt.wait () in ignore (Lwt_unix.on_signal Sys.sigterm (fun _ -> Lwt.wakeup_later resolve ())); promise @@ -687,7 +694,7 @@ let serve ?(interface = default_interface) ?(port = default_port) ?socket_path - ?(stop = on_sigterm) + ?stop ?(error_handler = Error_handler.default) ?(tls = false) ?certificate_file @@ -699,7 +706,7 @@ let serve "serve" ~interface ~network:(network ~port ~socket_path) - ~stop + ~stop:(option_or stop on_sigterm) ~error_handler ~tls:(if tls then `OpenSSL else `No) ?certificate_file @@ -715,7 +722,7 @@ let run ?(interface = default_interface) ?(port = default_port) ?socket_path - ?(stop = on_sigterm) + ?stop ?(error_handler = Error_handler.default) ?(tls = false) ?certificate_file @@ -762,7 +769,7 @@ let run "run" ~interface ~network:(network ~port ~socket_path) - ~stop + ~stop:(option_or stop on_sigterm) ~error_handler ~tls:(if tls then `OpenSSL else `No) ?certificate_file ?key_file From dd6747b00318aff4aeddf3b68c7d403e238e7dc0 Mon Sep 17 00:00:00 2001 From: Sebastian Willenbrink Date: Wed, 25 Mar 2026 18:23:54 +0100 Subject: [PATCH 4/5] Handle SIGTERM and SIGINT --- src/http/http.ml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/http/http.ml b/src/http/http.ml index b446e4e2..6cce597a 100644 --- a/src/http/http.ml +++ b/src/http/http.ml @@ -679,11 +679,13 @@ let option_or opt default = match opt with | Some v -> v | None -> default () -(* Lazy SIGTERM handler to avoid side effects. *) -let on_sigterm () = - let promise, resolve = Lwt.wait () in - ignore (Lwt_unix.on_signal Sys.sigterm (fun _ -> Lwt.wakeup_later resolve ())); - promise +(* Lazy signal handler to avoid side effects. *) +let on_termination_signal () = + let term_promise, term_resolve = Lwt.wait () in + let int_promise, int_resolve = Lwt.wait () in + ignore (Lwt_unix.on_signal Sys.sigterm (fun _ -> Lwt.wakeup_later term_resolve ())); + ignore (Lwt_unix.on_signal Sys.sigint (fun _ -> Lwt.wakeup_later int_resolve ())); + Lwt.pick [term_promise; int_promise] let network ~port ~socket_path = match socket_path with @@ -706,7 +708,7 @@ let serve "serve" ~interface ~network:(network ~port ~socket_path) - ~stop:(option_or stop on_sigterm) + ~stop:(option_or stop on_termination_signal) ~error_handler ~tls:(if tls then `OpenSSL else `No) ?certificate_file @@ -769,7 +771,7 @@ let run "run" ~interface ~network:(network ~port ~socket_path) - ~stop:(option_or stop on_sigterm) + ~stop:(option_or stop on_termination_signal) ~error_handler ~tls:(if tls then `OpenSSL else `No) ?certificate_file ?key_file From 426d83ef0d638cebc5a5547306d4a47e47c93242 Mon Sep 17 00:00:00 2001 From: Sebastian Willenbrink Date: Wed, 25 Mar 2026 18:43:33 +0100 Subject: [PATCH 5/5] Update docs --- src/dream.mli | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dream.mli b/src/dream.mli index d803e4bb..d5b98d97 100644 --- a/src/dream.mli +++ b/src/dream.mli @@ -2130,7 +2130,7 @@ val run : - [~stop] is a promise that causes the server to stop accepting new requests, and {!Dream.run} to return. Requests that have already entered the Web application continue to be processed. The default value is a - promise that resolves when the [TERM] signal is received. + promise that resolves when [SIGTERM] or [SIGINT] is received. - [~error_handler] handles all errors, both from the application, and low-level errors. See {!section-errors} and example {{:https://github.com/camlworks/dream/tree/master/example/9-error#folders-and-files}