Skip to content

Commit dc6f569

Browse files
authored
Merge pull request #597 from pfitaxel/feat/serve-during-rebuild
Add option `learn-ocaml build serve --serve-during-build` and fix related minor issues
2 parents b7ed29c + cc6caa3 commit dc6f569

File tree

9 files changed

+203
-59
lines changed

9 files changed

+203
-59
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ ENTRYPOINT ["dumb-init","/usr/bin/learn-ocaml-client"]
4848
FROM alpine:3.13 as program
4949

5050
RUN apk update \
51-
&& apk add ncurses-libs libev dumb-init git openssl \
51+
&& apk add ncurses-libs libev dumb-init git openssl lsof \
5252
&& addgroup learn-ocaml \
5353
&& adduser learn-ocaml -DG learn-ocaml
5454

Dockerfile.test-server

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ LABEL org.label-schema.build-date="${BUILD_DATE}" \
5454
org.label-schema.schema-version="1.0"
5555

5656
RUN apk update \
57-
&& apk add ncurses-libs libev dumb-init git openssl \
57+
&& apk add ncurses-libs libev dumb-init git openssl lsof \
5858
&& addgroup learn-ocaml \
5959
&& adduser learn-ocaml -DG learn-ocaml
6060

learn-ocaml.opam

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ license: "MIT"
1717
homepage: "https://github.com/ocaml-sf/learn-ocaml"
1818
bug-reports: "https://github.com/ocaml-sf/learn-ocaml/issues"
1919
dev-repo: "git+https://github.com/ocaml-sf/learn-ocaml"
20+
depexts: [
21+
["lsof"] {os-distribution = "alpine"}
22+
]
2023
depends: [
2124
"asak" { >= "0.4"}
2225
"base64"

src/app/learnocaml_index_main.ml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -824,8 +824,8 @@ let () =
824824
Lwt.async @@ fun () ->
825825
set_string_translations ();
826826
Dom_html.document##.title :=
827-
Js.string ("Learn OCaml" ^ " v."^Learnocaml_api.version);
828-
Manip.setInnerText El.version ("v."^Learnocaml_api.version);
827+
Js.string ("Learn OCaml" ^ " v"^Learnocaml_api.version);
828+
Manip.setInnerText El.version ("v"^Learnocaml_api.version);
829829
Learnocaml_local_storage.init () ;
830830
let sync_button_group = button_group () in
831831
disable_button_group sync_button_group;

src/main/learnocaml_main.ml

Lines changed: 113 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,23 @@ 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:"SERVER OPTIONS"
82+
~env:(Cmd.Env.info "LEARNOCAML_SERVE_DURING_BUILD") ~doc:
83+
"If the directory specified by $(b,--app-dir) already exists from a \
84+
previous build, create a temporary child process to serve it \
85+
while the build completes, in order to reduce server downtime. \
86+
This flag requires to run both commands '$(b,learn-ocaml build serve)'. \
87+
After the build, the child process stops and a new server starts. \
88+
This flag is useful in a docker-compose context, and can be enabled \
89+
by adding to the environment: '$(env)=true'."
90+
91+
let child_pid =
92+
(* Note: option `--child-pid` is specific to the native learn-ocaml-server,
93+
hence this dummy value here, to avoid copying it in "SERVER OPTIONS". *)
94+
Term.const (None: int option)
95+
7996
module Grader = struct
8097
let info = info ~docs:"GRADER OPTIONS"
8198

@@ -263,17 +280,18 @@ module Args = struct
263280
app_dir: string;
264281
repo_dir: string;
265282
build_dir: string;
283+
serve_during_build: bool;
266284
grader: Grader.t;
267285
builder: Builder.t;
268286
server: Server.t;
269287
}
270288

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 }
289+
let term child_pid =
290+
let apply commands app_dir repo_dir build_dir grader builder server serve_during_build =
291+
{ commands; app_dir; repo_dir; build_dir; grader; builder; server; serve_during_build }
274292
in
275293
Term.(const apply $commands $app_dir $repo_dir $build_dir
276-
$Grader.term $Builder.term $Server.term app_dir base_url)
294+
$Grader.term $Builder.term $Server.term app_dir base_url child_pid $serve_during_build)
277295
end
278296

279297
open Args
@@ -319,7 +337,7 @@ let temp_app_dir o =
319337
((basename o.app_dir) ^ ".temp")
320338

321339
let main o =
322-
Printf.printf "Learnocaml v.%s running.\n%!" Learnocaml_api.version;
340+
Printf.printf "Learnocaml v%s running.\n%!" Learnocaml_api.version;
323341
let grade o =
324342
if List.mem Grade o.commands then
325343
(if List.mem Build o.commands || List.mem Serve o.commands then
@@ -391,23 +409,24 @@ let main o =
391409
end
392410
else Lwt.return_unit
393411
in
394-
let generate o =
412+
let generate ?(check_port = true) o =
395413
if List.mem Build o.commands then
396414
(let get_app_dir o =
397415
if not (List.mem Serve o.commands) then
398416
Lwt.return o.app_dir
399-
else if o.server.Server.replace then
400-
let app_dir = temp_app_dir o in
401-
(if Sys.file_exists app_dir then
417+
else if o.server.Server.replace || o.serve_during_build then
418+
let temp_dir = temp_app_dir o in
419+
(if Sys.file_exists temp_dir then
402420
(Printf.eprintf "Warning: temporary directory %s already exists\n%!"
403-
app_dir;
421+
temp_dir;
404422
Lwt.return_unit)
405423
else if Sys.file_exists o.app_dir then
406-
Lwt_utils.copy_tree o.app_dir app_dir
424+
Lwt_utils.copy_tree o.app_dir temp_dir
407425
else
408426
Lwt.return_unit)
409-
>>= fun () -> Lwt.return app_dir
410-
else if Learnocaml_server.check_running () <> None then
427+
>>= fun () -> Lwt.return temp_dir
428+
else if check_port && Learnocaml_server.check_running () <> None then
429+
(* This server-specific check is here to fail earlier if need be *)
411430
(Printf.eprintf
412431
"Error: another server is already running on port %d \
413432
(consider using option `--replace`)\n%!"
@@ -500,12 +519,29 @@ let main o =
500519
else
501520
Lwt.return true
502521
in
503-
let run_server o =
522+
let kill_once pid =
523+
let already = ref false in
524+
fun () ->
525+
if !already then () else
526+
(already := true;
527+
Unix.kill pid Sys.sigint;
528+
Printf.eprintf "Waiting for child process %d to terminate... %!" pid;
529+
ignore (Unix.waitpid [] pid);
530+
prerr_endline "ok ")
531+
in
532+
(* child_pid = None => no --serve-during-build
533+
child_pid = Some 0 => --serve-during-build, child process
534+
child_pid = Some n, n>0 => --serve-during-build, main process *)
535+
let run_server ~child_pid o =
504536
if List.mem Serve o.commands then
505537
let () =
506-
if o.server.Server.replace then
507-
let running = Learnocaml_server.check_running () in
508-
Option.iter Learnocaml_server.kill_running running;
538+
let int_child_pid = Option.value child_pid ~default:(-1) in
539+
if o.server.Server.replace || (o.serve_during_build && int_child_pid > 0) then
540+
let () =
541+
(if int_child_pid > 0 then kill_once int_child_pid ()
542+
else let running = Learnocaml_server.check_running () in
543+
Option.iter Learnocaml_server.kill_running running)
544+
in
509545
let temp = temp_app_dir o in
510546
let app_dir = absolute_filename o.app_dir in
511547
let bak =
@@ -542,6 +578,8 @@ let main o =
542578
("--sync-dir="^o.server.sync_dir) ::
543579
("--base-url="^o.builder.Builder.base_url) ::
544580
("--port="^string_of_int o.server.port) ::
581+
(match child_pid with None -> [] | Some n -> ["--child-pid="^string_of_int n])
582+
@
545583
(match o.server.cert with None -> [] | Some c -> ["--cert="^c])
546584
in
547585
Lwt.return
@@ -550,8 +588,13 @@ let main o =
550588
Unix.execv native_server
551589
(Array.of_list (native_server::server_args))))
552590
else begin
553-
Printf.printf "Starting server on port %d\n%!"
554-
!Learnocaml_server.port;
591+
let comment = match child_pid with
592+
| None -> ""
593+
| Some 0 -> "(temporary)"
594+
| Some _pid -> "(main)"
595+
in
596+
Printf.printf "Starting server%s on port %d\n%!"
597+
comment !Learnocaml_server.port;
555598
if o.builder.Builder.base_url <> "" then
556599
Printf.printf "Base URL: %s\n%!" o.builder.Builder.base_url;
557600
Learnocaml_server.launch () >>= fun ret ->
@@ -560,19 +603,57 @@ let main o =
560603
else
561604
Lwt.return (`Success true)
562605
in
606+
let lwt_run_server ~child_pid build_ok =
607+
if build_ok then
608+
run_server ~child_pid o >>= function
609+
| `Success true -> Lwt.return (`Code 0)
610+
| `Success false -> Lwt.return (`Code 10)
611+
| `Continuation f -> Lwt.return (`Continuation f)
612+
else
613+
Lwt.return (`Code 1)
614+
in
615+
(* NOTE: the code below handles "learn-ocaml build serve --serve-during-build"
616+
by relying on Lwt_unix.fork; and to stay on the safe side, we make sure
617+
that this fork is triggered before the first Lwt_main.run command. *)
563618
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))
619+
if o.serve_during_build then begin
620+
if not (List.mem Build o.commands && List.mem Serve o.commands) then
621+
(Printf.eprintf
622+
"Error: option `--serve-during-build` requires both commands `build serve`.\n%!";
623+
exit 1)
624+
else if o.server.Server.replace then
625+
(Printf.eprintf
626+
"Error: option `--replace` is incompatible with option `--serve-during-build`.\n%!";
627+
exit 10)
628+
else if Learnocaml_server.check_running () <> None then
629+
(Printf.eprintf
630+
"Error: another server is already running on port %d \
631+
(consider using option `--replace` instead of `--serve-during-build`)\n%!"
632+
!Learnocaml_server.port;
633+
exit 10);
634+
match Lwt_unix.fork () with
635+
| 0 ->
636+
if Sys.file_exists o.app_dir then
637+
Lwt_main.run (lwt_run_server ~child_pid:(Some 0) true)
638+
else
639+
(Printf.eprintf
640+
"Info: no existing app-dir in '%s', \
641+
will be available at next run (skipping temporary server start).\n%!" o.app_dir;
642+
`Code 0)
643+
| child_pid ->
644+
at_exit (kill_once child_pid);
645+
Lwt_main.run
646+
(grade o >>= function
647+
| Some i -> Lwt.return (`Code i)
648+
| None ->
649+
generate ~check_port:false o >>= lwt_run_server ~child_pid:(Some child_pid))
650+
end
651+
else
652+
Lwt_main.run
653+
(grade o >>= function
654+
| Some i -> Lwt.return (`Code i)
655+
| None ->
656+
generate o >>= lwt_run_server ~child_pid:None)
576657
in
577658
match ret with
578659
| `Code n -> exit n
@@ -627,7 +708,7 @@ let main_info =
627708
~version:Learnocaml_api.version
628709
"learn-ocaml"
629710

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

632713
let () =
633714
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 option;
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 option 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+
Use this to reduce server downtime when updating the content \
61+
of an instance: the running server will only be stopped once the \
62+
new one is ready. If running in a Docker context, you may want to \
63+
have a look at the flag $(b,--serve-during-build) instead."
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 option;
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 option;
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 option Cmdliner.Term.t -> t Cmdliner.Term.t
2324
end
2425

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

0 commit comments

Comments
 (0)