Skip to content

Commit f0108a3

Browse files
committed
feat(CLI): Add option learn-ocaml build serve --serve-during-build
Motivation: - Makes it possible to provide a feature similar to the `--replace` option (namely, `learn-ocaml build serve --replace`) within a Docker context. As `--replace` needed to successively start 2 learn-ocaml processes listening to the same port, but if we spin two different containers, ./www is not shared between containers, nor the local TCP interface. - If tweaking the default entrypoint could be an alternative solution, the shell script would be involved to cope with the need to handle signals properly. - The solution implemented in this patch is simpler and can be enabled in a docker-compose context by passing: ``` environment: LEARNOCAML_SERVE_DURING_BUILD: 'true' ``` then run a command such as `docker restart learn-ocaml`. Remarks: - Using docker-compose, to restart a server and benefit from this feature, use ( docker compose stop ; docker compose restart ) rather than ( docker compose down ; docker compose up -d ) - This commit has been double-checked with both native server ( make ; make opaminstall ; learn-ocaml build serve --serve-during-build --repo=$REPO ) and bytecode server ( make ; make opaminstall ; mv $(which learn-ocaml-server){,~} ; learn-ocaml build serve --serve-during-build --repo=$REPO ) Close #594
1 parent baf55bd commit f0108a3

File tree

4 files changed

+178
-49
lines changed

4 files changed

+178
-49
lines changed

src/main/learnocaml_main.ml

Lines changed: 103 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,22 @@ module Args = struct
7676
Mandatory for '$(b,learn-ocaml build)' if the site is not hosted in path '/', \
7777
which typically occurs for static deployment."
7878

79+
let serve_during_build =
80+
value & flag &
81+
info ["serve-during-build"] ~docs ~env:(Cmd.Env.info "LEARNOCAML_SERVE_DURING_BUILD") ~doc:
82+
"If the $(i,--app-dir) directory already exists from a previous build, \
83+
create a child process thanks to $(i,Unix.fork) to serve this version \
84+
during the build, reducing server's downtime as much as possible. \
85+
This flag requires to run both commands '$(b,learn-ocaml build serve)'. \
86+
After the build, the child process stops and a new server starts. \
87+
This flag is useful in a docker-compose context, and can be enabled \
88+
by adding to the environment: '$(i,LEARNOCAML_SERVE_DURING_BUILD=true)'."
89+
90+
let child_pid =
91+
(* Note: option `--child-pid` is specific to the native learn-ocaml-server,
92+
hence this dummy value here, to avoid copying it in "SERVER OPTIONS". *)
93+
Term.const (-2)
94+
7995
module Grader = struct
8096
let info = info ~docs:"GRADER OPTIONS"
8197

@@ -263,17 +279,18 @@ module Args = struct
263279
app_dir: string;
264280
repo_dir: string;
265281
build_dir: string;
282+
serve_during_build: bool;
266283
grader: Grader.t;
267284
builder: Builder.t;
268285
server: Server.t;
269286
}
270287

271-
let term =
272-
let apply commands app_dir repo_dir build_dir grader builder server =
273-
{ commands; app_dir; repo_dir; build_dir; grader; builder; server }
288+
let term child_pid =
289+
let apply commands app_dir repo_dir build_dir grader builder server serve_during_build =
290+
{ commands; app_dir; repo_dir; build_dir; grader; builder; server; serve_during_build }
274291
in
275292
Term.(const apply $commands $app_dir $repo_dir $build_dir
276-
$Grader.term $Builder.term $Server.term app_dir base_url)
293+
$Grader.term $Builder.term $Server.term app_dir base_url child_pid $serve_during_build)
277294
end
278295

279296
open Args
@@ -391,12 +408,12 @@ let main o =
391408
end
392409
else Lwt.return_unit
393410
in
394-
let generate o =
411+
let generate ?(check_port = true) o =
395412
if List.mem Build o.commands then
396413
(let get_app_dir o =
397414
if not (List.mem Serve o.commands) then
398415
Lwt.return o.app_dir
399-
else if o.server.Server.replace then
416+
else if o.server.Server.replace || o.serve_during_build then
400417
let temp_dir = temp_app_dir o in
401418
(if Sys.file_exists temp_dir then
402419
(Printf.eprintf "Warning: temporary directory %s already exists\n%!"
@@ -407,7 +424,8 @@ let main o =
407424
else
408425
Lwt.return_unit)
409426
>>= fun () -> Lwt.return temp_dir
410-
else if Learnocaml_server.check_running () <> None then
427+
else if check_port && Learnocaml_server.check_running () <> None then
428+
(* This server-specific check is here to fail earlier if need be *)
411429
(Printf.eprintf
412430
"Error: another server is already running on port %d \
413431
(consider using option `--replace`)\n%!"
@@ -500,12 +518,27 @@ let main o =
500518
else
501519
Lwt.return true
502520
in
503-
let run_server o =
521+
let kill_once pid =
522+
let already = ref false in
523+
fun () ->
524+
if !already then () else
525+
(already := true;
526+
Learnocaml_server.kill_running pid;
527+
ignore (Unix.waitpid [Unix.WNOHANG] pid))
528+
in
529+
(* child_pid = None => no --serve-during-build
530+
child_pid = Some 0 => --serve-during-build, child process
531+
child_pid = Some n, n>0 => --serve-during-build, main process *)
532+
let run_server ~child_pid o =
504533
if List.mem Serve o.commands then
534+
let int_child_pid = Option.value child_pid ~default:(-1) in
505535
let () =
506-
if o.server.Server.replace then
507-
let running = Learnocaml_server.check_running () in
508-
Option.iter Learnocaml_server.kill_running running;
536+
if o.server.Server.replace || (o.serve_during_build && int_child_pid > 0) then
537+
let () =
538+
(if int_child_pid > 0 then kill_once int_child_pid ()
539+
else let running = Learnocaml_server.check_running () in
540+
Option.iter Learnocaml_server.kill_running running)
541+
in
509542
let temp = temp_app_dir o in
510543
let app_dir = absolute_filename o.app_dir in
511544
let bak =
@@ -542,6 +575,7 @@ let main o =
542575
("--sync-dir="^o.server.sync_dir) ::
543576
("--base-url="^o.builder.Builder.base_url) ::
544577
("--port="^string_of_int o.server.port) ::
578+
("--child-pid="^string_of_int int_child_pid) ::
545579
(match o.server.cert with None -> [] | Some c -> ["--cert="^c])
546580
in
547581
Lwt.return
@@ -550,8 +584,13 @@ let main o =
550584
Unix.execv native_server
551585
(Array.of_list (native_server::server_args))))
552586
else begin
553-
Printf.printf "Starting server on port %d\n%!"
554-
!Learnocaml_server.port;
587+
let comment = match int_child_pid with
588+
| -1 -> ""
589+
| 0 -> "(child)"
590+
| _pid -> "(main)"
591+
in
592+
Printf.printf "Starting server%s on port %d\n%!"
593+
comment !Learnocaml_server.port;
555594
if o.builder.Builder.base_url <> "" then
556595
Printf.printf "Base URL: %s\n%!" o.builder.Builder.base_url;
557596
Learnocaml_server.launch () >>= fun ret ->
@@ -560,19 +599,57 @@ let main o =
560599
else
561600
Lwt.return (`Success true)
562601
in
602+
let lwt_run_server ~child_pid build_ok =
603+
if build_ok then
604+
run_server ~child_pid o >>= function
605+
| `Success true -> Lwt.return (`Code 0)
606+
| `Success false -> Lwt.return (`Code 10)
607+
| `Continuation f -> Lwt.return (`Continuation f)
608+
else
609+
Lwt.return (`Code 1)
610+
in
611+
(* NOTE: the code below handles "learn-ocaml build serve --serve-during-build"
612+
by relying on Lwt.fork; and to stay on the safe side, we make sure
613+
that this fork is triggered before the first Lwt_main.run command. *)
563614
let ret =
564-
Lwt_main.run
565-
(grade o >>= function
566-
| Some i -> Lwt.return (`Code i)
567-
| None ->
568-
generate o >>= fun success ->
569-
if success then
570-
run_server o >>= function
571-
| `Success true -> Lwt.return (`Code 0)
572-
| `Success false -> Lwt.return (`Code 10)
573-
| `Continuation f -> Lwt.return (`Continuation f)
574-
else
575-
Lwt.return (`Code 1))
615+
if o.serve_during_build then begin
616+
if not (List.mem Build o.commands && List.mem Serve o.commands) then
617+
(Printf.eprintf
618+
"Error: option `--serve-during-build` requires both commands `build serve`.\n%!";
619+
exit 1)
620+
else if o.server.Server.replace then
621+
(Printf.eprintf
622+
"Error: option `--replace` is incompatible with option `--serve-during-build`.\n%!";
623+
exit 10)
624+
else if Learnocaml_server.check_running () <> None then
625+
(Printf.eprintf
626+
"Error: another server is already running on port %d \
627+
(consider using option `--replace`)\n%!"
628+
!Learnocaml_server.port;
629+
exit 10);
630+
match Lwt_unix.fork () with
631+
| 0 ->
632+
if Sys.file_exists o.app_dir then
633+
Lwt_main.run (lwt_run_server ~child_pid:(Some 0) true)
634+
else
635+
(Printf.eprintf
636+
"Info: no existing app-dir in '%s', \
637+
will be available at next run (skipping child server start).\n%!" o.app_dir;
638+
`Code 0)
639+
| child_pid ->
640+
at_exit (kill_once child_pid);
641+
Lwt_main.run
642+
(grade o >>= function
643+
| Some i -> Lwt.return (`Code i)
644+
| None ->
645+
generate ~check_port:false o >>= lwt_run_server ~child_pid:(Some child_pid))
646+
end
647+
else
648+
Lwt_main.run
649+
(grade o >>= function
650+
| Some i -> Lwt.return (`Code i)
651+
| None ->
652+
generate o >>= lwt_run_server ~child_pid:None)
576653
in
577654
match ret with
578655
| `Code n -> exit n
@@ -627,7 +704,7 @@ let main_info =
627704
~version:Learnocaml_api.version
628705
"learn-ocaml"
629706

630-
let main_term = Term.(const main $ Args.term)
707+
let main_term = Term.(const main $ Args.term child_pid)
631708

632709
let () =
633710
match

src/main/learnocaml_server_args.ml

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ module type S = sig
2020
port: int;
2121
cert: string option;
2222
replace: bool;
23+
child_pid: int;
2324
}
2425

25-
val term: string Cmdliner.Term.t -> string Cmdliner.Term.t -> t Cmdliner.Term.t
26+
val term: string Cmdliner.Term.t -> string Cmdliner.Term.t -> int Cmdliner.Term.t -> t Cmdliner.Term.t
2627
end
2728

2829
module Args (SN : Section_name) = struct
@@ -54,19 +55,24 @@ module Args (SN : Section_name) = struct
5455

5556
let replace =
5657
value & flag &
57-
info ["replace"] ~doc:
58-
"Replace a previously running instance of the server on the same port."
58+
info ["replace"] ~env:(Cmd.Env.info "LEARNOCAML_REPLACE") ~doc:
59+
"Replace a previously running instance of the server on the same port. \
60+
This flag is incompatible with flag $(i,--serve-during-build), \
61+
albeit both flags aim to reduce server downtime. In practice, \
62+
flag $(i,--replace) is useful in a non-Docker context, while \
63+
flag $(i,--serve-during-build) is useful in a Docker context."
5964

6065
type t = {
6166
sync_dir: string;
6267
base_url: string;
6368
port: int;
6469
cert: string option;
6570
replace: bool;
71+
child_pid: int;
6672
}
6773

68-
let term app_dir base_url =
69-
let apply app_dir sync_dir base_url port cert replace =
74+
let term app_dir base_url child_pid =
75+
let apply app_dir sync_dir base_url port cert replace child_pid =
7076
Learnocaml_store.static_dir := app_dir;
7177
Learnocaml_store.sync_dir := sync_dir;
7278
let port = match port, cert with
@@ -80,10 +86,10 @@ module Args (SN : Section_name) = struct
8086
| None -> None);
8187
Learnocaml_server.port := port;
8288
Learnocaml_server.base_url := base_url;
83-
{ sync_dir; base_url; port; cert; replace }
89+
{ sync_dir; base_url; port; cert; replace; child_pid }
8490
in
8591
(* warning: if you add any options here, remember to pass them through when
8692
calling the native server from learn-ocaml main *)
87-
Term.(const apply $ app_dir $ sync_dir $ base_url $ port $ cert $ replace)
93+
Term.(const apply $ app_dir $ sync_dir $ base_url $ port $ cert $ replace $ child_pid)
8894

8995
end

src/main/learnocaml_server_args.mli

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ module type S = sig
1717
port: int;
1818
cert: string option;
1919
replace: bool;
20+
child_pid: int;
2021
}
2122

22-
val term: string Cmdliner.Term.t -> string Cmdliner.Term.t -> t Cmdliner.Term.t
23+
val term: string Cmdliner.Term.t -> string Cmdliner.Term.t -> int Cmdliner.Term.t -> t Cmdliner.Term.t
2324
end
2425

2526
module Args : functor (_ : Section_name) -> S

src/main/learnocaml_server_main.ml

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,31 +25,66 @@ let signal_waiter =
2525
let _ = Lwt_unix.on_signal Sys.sigterm handler in
2626
waiter
2727

28+
let kill_once pid =
29+
let already = ref false in
30+
fun () ->
31+
if !already then () else
32+
(already := true;
33+
Learnocaml_server.kill_running pid;
34+
ignore (Unix.waitpid [Unix.WNOHANG] pid))
35+
2836
let main o =
2937
let open Server_args in
30-
Printf.printf "Learnocaml server v.%s starting on port %d\n%!"
31-
Learnocaml_api.version o.port;
38+
if o.child_pid < -1 then
39+
(Printf.eprintf "Error: incorrect value `--child-pid=%d`\n%!" o.child_pid;
40+
exit 10);
41+
if o.child_pid >= 0 && o.replace then
42+
(Printf.eprintf "Error: option `--replace` is incompatible with option `--child-pid`\n%!";
43+
exit 10);
44+
(* Note: "if o.child_pid>0 then at_exit (kill_once o.child_pid);" is unneeded
45+
as "learn-ocaml serve" already made sure that the child pid terminated. *)
46+
let comment = match o.child_pid with
47+
| -1 -> ""
48+
| 0 -> "(child)"
49+
| _pid -> "(main)"
50+
in
51+
Printf.printf "Learnocaml server%s v.%s starting on port %d\n%!"
52+
comment Learnocaml_api.version o.port;
3253
if o.base_url <> "" then
3354
Printf.printf "Base URL: %s\n%!" o.base_url;
3455
let () =
35-
match Learnocaml_server.check_running (), o.replace with
36-
| None, _ -> ()
37-
| Some _, false ->
38-
Printf.eprintf "Error: another server is already running on port %d \
39-
(consider using option `--replace`)\n%!"
40-
!Learnocaml_server.port;
41-
exit 10
42-
| Some pid, true ->
43-
Learnocaml_server.kill_running pid
56+
match Learnocaml_server.check_running (), o.replace, o.child_pid with
57+
| None, _, _ -> ()
58+
| Some _, false, -1 ->
59+
Printf.eprintf "Error: another server is already running on port %d \
60+
(consider using option `--replace`)\n%!"
61+
!Learnocaml_server.port;
62+
exit 10
63+
| Some _, false, 0 ->
64+
Printf.eprintf "Warning(child): another server is running on port %d \
65+
(skipping child server start)\n%!"
66+
!Learnocaml_server.port;
67+
exit 0
68+
| Some pid, false, pid' ->
69+
if pid = pid' then
70+
kill_once pid' ()
71+
else
72+
(Printf.eprintf "Error: another server (pid %d) is already running on port %d \
73+
(while expecting `--child-pid=%d`)\n%!"
74+
pid !Learnocaml_server.port pid';
75+
Learnocaml_server.kill_running pid';
76+
exit 10)
77+
| Some pid, true, _ ->
78+
Learnocaml_server.kill_running pid
4479
in
4580
let rec run () =
4681
let minimum_duration = 15. in
4782
let t0 = Unix.time () in
4883
try
4984
Lwt_main.run @@ Lwt.pick [
50-
(Learnocaml_server.launch () >|= function true -> 0 | false -> 10);
51-
signal_waiter
52-
]
85+
(Learnocaml_server.launch () >|= function true -> 0 | false -> 10);
86+
signal_waiter
87+
]
5388
with Unix.Unix_error (err, fn, arg) ->
5489
Format.eprintf "SERVER CRASH in %s(%s):@ @[<hv 2>%s@]@."
5590
fn arg (Unix.error_message err);
@@ -98,6 +133,16 @@ let base_url =
98133
Mandatory for '$(b,learn-ocaml build)' if the site is not hosted in path '/', \
99134
which typically occurs for static deployment."
100135

136+
let child_pid =
137+
let open Arg in
138+
value & opt int (-1) &
139+
info ["child-pid"] ~docv:"CHILD_PID" ~doc:
140+
"Set the pid of the child process created by $(i,Unix.fork) when the CLI \
141+
option $(i,--serve-during-build) is used. If $(docv) has the value 0, \
142+
it means the current instance is the temporary server (child process). \
143+
If $(docv) is ommitted or has the value -1, it means no forking occured \
144+
and the server should check no concurrent server is running on this port."
145+
101146
let exits =
102147
let open Cmd.Exit in
103148
[ info ~doc:"Default exit." ok
@@ -116,7 +161,7 @@ let main_info =
116161
"learn-ocaml-server"
117162

118163

119-
let main_term = Term.(const main $ Server_args.term app_dir base_url)
164+
let main_term = Term.(const main $ Server_args.term app_dir base_url child_pid)
120165

121166
let () =
122167
match

0 commit comments

Comments
 (0)