Skip to content

Commit a2a6317

Browse files
authored
refactor: Refactor pending target cleanup into Sandbox (#14483)
The build system used to special-case non-sandboxed actions by manually adding and removing their targets from Pending_targets while real sandboxes went through Sandbox.create and Sandbox.destroy. That kept the cleanup lifecycle split between Build_system and Sandbox. Move pending target tracking into Sandbox and make Sandbox.with_ take an optional sandbox mode. A missing mode now creates a fake sandbox context: paths map to themselves, target extraction is a no-op, and destroying the context only removes the action from the pending-target set. Real sandbox modes still create and destroy actual sandbox directories. Keep the fake non-sandbox context outside the live-sandbox throttle. Only real sandboxes consume one of the 100 global sandbox slots. Signed-off-by: Rudi Grinberg <me@rgrinberg.com>
1 parent c1cee52 commit a2a6317

3 files changed

Lines changed: 130 additions & 107 deletions

File tree

src/dune_engine/build_system.ml

Lines changed: 28 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -120,25 +120,6 @@ let rec with_locks ~f = function
120120
~f:(fun () -> with_locks ~f mutexes)
121121
;;
122122

123-
module Pending_targets = struct
124-
(* All file and directory targets of non-sandboxed actions that are currently
125-
being executed. On exit, we need to delete them as they might contain
126-
garbage. *)
127-
128-
let t = ref Targets.empty
129-
let remove targets = t := Targets.diff !t (Targets.Validated.unvalidate targets)
130-
let add targets = t := Targets.combine !t (Targets.Validated.unvalidate targets)
131-
132-
let cleanup () =
133-
let targets = !t in
134-
t := Targets.empty;
135-
Targets.iter
136-
targets
137-
~file:(fun p -> p |> Path.Build.to_string |> Fpath.unlink_no_err)
138-
~dir:(fun p -> Path.rm_rf (Path.build p))
139-
;;
140-
end
141-
142123
type rule_execution_result =
143124
{ facts : Dep.Fact.t Dep.Map.t
144125
; targets : Digest.t Targets.Produced.t
@@ -403,18 +384,9 @@ module Internal = struct
403384
=
404385
action
405386
in
406-
let deps =
407-
Dep.Facts.paths
408-
~expand_aliases:
409-
(Execution_parameters.expand_aliases_in_sandbox execution_parameters)
410-
facts
411-
in
412387
let execute_action sandbox =
413-
let action =
414-
match sandbox with
415-
| None -> action
416-
| Some sandbox -> Action.sandbox action sandbox
417-
in
388+
let is_sandboxed = Sandbox.is_sandboxed sandbox in
389+
let action = if is_sandboxed then Action.sandbox action sandbox else action in
418390
let action =
419391
(* We must add the creation of the stamp file after sandboxing it, as
420392
otherwise the stamp file would end up inside the sandbox. This is
@@ -438,12 +410,7 @@ module Internal = struct
438410
| None -> Path.Build.root
439411
| Some context -> context.build_dir
440412
in
441-
let root =
442-
Path.build
443-
(match sandbox with
444-
| None -> root
445-
| Some sandbox -> Sandbox.map_path sandbox root)
446-
in
413+
let root = Path.build (Sandbox.map_path sandbox root) in
447414
let action_trace = Action_trace.create rule_digest in
448415
with_locks locks ~f:(fun () ->
449416
let* action_exec_result =
@@ -464,17 +431,17 @@ module Internal = struct
464431
let* action_exec_result = Action_exec.Exec_result.ok_exn action_exec_result in
465432
let* () = Action_trace.collect action_trace in
466433
let* () =
467-
match sandbox with
468-
| None -> Fiber.return ()
469-
| Some sandbox ->
434+
if not is_sandboxed
435+
then Fiber.return ()
436+
else (
470437
(* The stamp file for anonymous actions is always created outside
471438
the sandbox, so we can't move it. *)
472439
let should_be_skipped =
473440
match rule_kind with
474441
| Normal_rule -> fun (_ : Path.Build.t) -> false
475442
| Anonymous_action { stamp_file; _ } -> Path.Build.equal stamp_file
476443
in
477-
Sandbox.move_targets_to_build_dir sandbox ~should_be_skipped ~targets
444+
Sandbox.move_targets_to_build_dir sandbox ~should_be_skipped ~targets)
478445
in
479446
let+ produced_targets =
480447
maybe_async_rule_file_op (fun () -> Targets.Produced.of_validated targets)
@@ -483,35 +450,26 @@ module Internal = struct
483450
| Ok produced_targets -> { Exec_result.produced_targets; action_exec_result }
484451
| Error error -> User_error.raise ~loc (Targets.Produced.Error.message error))
485452
in
486-
match sandbox_mode with
487-
| Some mode ->
488-
Sandbox.with_live_sandbox_slot ~f:(fun () ->
489-
let* sandbox =
490-
Sandbox.create
491-
~mode
492-
(Option.value ~default:Ignore corrections)
493-
~dirs:(Dep.Facts.necessary_dirs_for_sandboxing facts)
494-
~deps
495-
~rule_dir:targets.root
496-
~rule_loc:loc
497-
~rule_digest
498-
in
499-
(* CR-someday rgrinberg: Dynamic actions may discover dependencies
500-
while this slot is held. If all sandbox slots are held by such
501-
actions, sandboxed rules for the discovered dependencies cannot
502-
start. *)
503-
Fiber.finalize
504-
~finally:(fun () -> Sandbox.destroy sandbox)
505-
(fun () -> execute_action (Some sandbox)))
506-
| None ->
507-
(* If the action is not sandboxed, we use [pending_file_targets] to
508-
clean up the build directory if the action is interrupted. *)
509-
Pending_targets.add targets;
510-
Fiber.finalize
511-
~finally:(fun () ->
512-
Pending_targets.remove targets;
513-
Fiber.return ())
514-
(fun () -> execute_action None)
453+
let deps, sandbox_dirs =
454+
match sandbox_mode with
455+
| None -> Path.Set.empty, Path.Build.Set.empty
456+
| Some _ ->
457+
( Dep.Facts.paths
458+
~expand_aliases:
459+
(Execution_parameters.expand_aliases_in_sandbox execution_parameters)
460+
facts
461+
, Dep.Facts.necessary_dirs_for_sandboxing facts )
462+
in
463+
Sandbox.with_
464+
~mode:sandbox_mode
465+
(Option.value ~default:Ignore corrections)
466+
~rule_loc:loc
467+
~dirs:sandbox_dirs
468+
~deps
469+
~rule_dir:targets.root
470+
~rule_digest
471+
~targets
472+
~f:execute_action
515473

516474
and promote_targets ~rule_mode ~targets ~promote_source =
517475
match rule_mode, !Clflags.promote with
@@ -1176,7 +1134,7 @@ let run f =
11761134
Memo.run_with_error_handler f ~handle_error_no_raise:report_early_exn)
11771135
in
11781136
Dtemp.clear ();
1179-
Pending_targets.cleanup ();
1137+
Sandbox.cleanup_pending_targets ();
11801138
Target_promotion.save ();
11811139
let* outcome =
11821140
match outcome with

src/dune_engine/sandbox.ml

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,27 @@ let max_live_sandboxes = 250
55
let live_sandbox_throttle = lazy (Fiber.Throttle.create max_live_sandboxes)
66
let with_live_sandbox_slot ~f = Fiber.Throttle.run (Lazy.force live_sandbox_throttle) ~f
77

8+
module Pending_targets = struct
9+
(* All file and directory targets of non-sandboxed actions that are currently
10+
being executed. On exit, we need to delete them as they might contain
11+
garbage. *)
12+
13+
let t = ref Targets.empty
14+
let remove targets = t := Targets.diff !t (Targets.Validated.unvalidate targets)
15+
let add targets = t := Targets.combine !t (Targets.Validated.unvalidate targets)
16+
17+
let cleanup () =
18+
let targets = !t in
19+
t := Targets.empty;
20+
Targets.iter
21+
targets
22+
~file:(fun p -> p |> Path.Build.to_string |> Fpath.unlink_no_err)
23+
~dir:(fun p -> Path.rm_rf (Path.build p))
24+
;;
25+
end
26+
27+
let cleanup_pending_targets = Pending_targets.cleanup
28+
829
let maybe_async f =
930
(* It would be nice to do this check only once and return a function, but the
1031
type of this function would need to be polymorphic which is forbidden by the
@@ -47,16 +68,30 @@ let init =
4768

4869
type snapshot = [ `Dir | `File of Stat.t ] Path.Map.t
4970

50-
type t =
71+
type real =
5172
{ dir : Path.Build.t
5273
; snapshot : snapshot option
5374
; corrections : Corrections.t
5475
; deps : Path.Set.t option
5576
; loc : Loc.t
5677
}
5778

58-
let dir t = t.dir
59-
let map_path t p = Path.Build.append t.dir p
79+
type t =
80+
| Sandboxed of real
81+
| No_sandbox of { targets : Targets.Validated.t }
82+
83+
let is_sandboxed = function
84+
| Sandboxed _ -> true
85+
| No_sandbox _ -> false
86+
;;
87+
88+
let map_real_path t p = Path.Build.append t.dir p
89+
90+
let map_path t p =
91+
match t with
92+
| Sandboxed t -> map_real_path t p
93+
| No_sandbox _ -> p
94+
;;
6095

6196
let copy_recursively =
6297
let chmod_file = Permissions.add Permissions.write in
@@ -105,7 +140,7 @@ let copy_recursively =
105140
()
106141
;;
107142

108-
let create_dir t dir = Path.mkdir_p (Path.build (map_path t dir))
143+
let create_dir t dir = Path.mkdir_p (Path.build (map_real_path t dir))
109144

110145
let create_dirs t ~dirs ~rule_dir =
111146
create_dir t rule_dir;
@@ -150,7 +185,7 @@ let link_deps t ~mode ~deps =
150185
"Action depends on source tree. All actions should depend on the copies in the \
151186
build directory instead."
152187
[ "path", Path.to_dyn path ]
153-
| Some p -> link path (Path.build (map_path t p)))
188+
| Some p -> link path (Path.build (map_real_path t p)))
154189
;;
155190

156191
let snapshot t =
@@ -177,7 +212,7 @@ let snapshot t =
177212
snapshot
178213
;;
179214

180-
let find_corrected_files (t : t) ~deps =
215+
let find_corrected_files (t : real) ~deps =
181216
(* CR-someday rgrinberg: fuse this step with deletion *)
182217
let start = Time.now () in
183218
let corrected =
@@ -244,7 +279,7 @@ let register_corrected_file_promotions t ~deps =
244279
})
245280
;;
246281

247-
let create
282+
let create_real
248283
~mode
249284
(corrections : Corrections.t)
250285
~rule_loc
@@ -311,7 +346,7 @@ let register_snapshot_promotion t (targets : Targets.Validated.t) ~old_snapshot
311346
let add_copy_file p = diffs := p :: !diffs in
312347
let deletes = ref [] in
313348
let add_delete what file = deletes := (what, in_source_tree file) :: !deletes in
314-
let target_root_in_sandbox = map_path t targets.root in
349+
let target_root_in_sandbox = map_real_path t targets.root in
315350
let () =
316351
Path.Map.iter2 old_snapshot new_snapshot ~f:(fun p before after ->
317352
if
@@ -379,7 +414,7 @@ let hint_delete_dir =
379414
]
380415
;;
381416

382-
let move_targets_to_build_dir t ~should_be_skipped ~(targets : Targets.Validated.t)
417+
let move_real_targets_to_build_dir t ~should_be_skipped ~(targets : Targets.Validated.t)
383418
: unit Fiber.t
384419
=
385420
let open Fiber.O in
@@ -401,9 +436,9 @@ let move_targets_to_build_dir t ~should_be_skipped ~(targets : Targets.Validated
401436
targets
402437
~file:(fun target ->
403438
if not (should_be_skipped target)
404-
then rename_optional_file ~src:(map_path t target) ~dst:target)
439+
then rename_optional_file ~src:(map_real_path t target) ~dst:target)
405440
~dir:(fun target ->
406-
let src_dir = map_path t target in
441+
let src_dir = map_real_path t target in
407442
(match Path.Untracked.stat (Path.build target) with
408443
| Error (Unix.ENOENT, _, _) -> ()
409444
| Error e ->
@@ -432,24 +467,54 @@ let move_targets_to_build_dir t ~should_be_skipped ~(targets : Targets.Validated
432467
Dune_trace.Event.sandbox `Extract ~start ~stop ~queued:None t.loc ~dir:t.dir)
433468
;;
434469

470+
let move_targets_to_build_dir t ~should_be_skipped ~(targets : Targets.Validated.t) =
471+
match t with
472+
| No_sandbox _ -> Fiber.return ()
473+
| Sandboxed t -> move_real_targets_to_build_dir t ~should_be_skipped ~targets
474+
;;
475+
435476
let failed_to_delete_sandbox dir reason =
436477
User_error.raise
437478
[ Pp.textf "failed to delete sandbox in %s" (Path.Build.to_string_maybe_quoted dir)
438479
; User_error.reason reason
439480
]
440481
;;
441482

442-
let destroy t =
443-
let open Fiber.O in
444-
let+ start, stop, queued =
445-
maybe_async (fun () ->
446-
try Path.rm_rf ~chmod:true (Path.build t.dir) with
447-
| Sys_error e -> failed_to_delete_sandbox t.dir (Pp.verbatim e)
448-
| Unix.Unix_error (error, syscall, arg) ->
449-
failed_to_delete_sandbox
450-
t.dir
451-
(Unix_error.Detailed.pp (Unix_error.Detailed.create error ~syscall ~arg)))
452-
in
453-
Dune_trace.emit ~buffered:true Sandbox (fun () ->
454-
Dune_trace.Event.sandbox `Destroy ~start ~stop ~queued t.loc ~dir:t.dir)
483+
let destroy = function
484+
| No_sandbox { targets } ->
485+
Pending_targets.remove targets;
486+
Fiber.return ()
487+
| Sandboxed t ->
488+
let open Fiber.O in
489+
let+ start, stop, queued =
490+
maybe_async (fun () ->
491+
try Path.rm_rf ~chmod:true (Path.build t.dir) with
492+
| Sys_error e -> failed_to_delete_sandbox t.dir (Pp.verbatim e)
493+
| Unix.Unix_error (error, syscall, arg) ->
494+
failed_to_delete_sandbox
495+
t.dir
496+
(Unix_error.Detailed.pp (Unix_error.Detailed.create error ~syscall ~arg)))
497+
in
498+
Dune_trace.emit ~buffered:true Sandbox (fun () ->
499+
Dune_trace.Event.sandbox `Destroy ~start ~stop ~queued t.loc ~dir:t.dir)
500+
;;
501+
502+
let with_ ~mode corrections ~rule_loc ~dirs ~deps ~rule_dir ~rule_digest ~targets ~f =
503+
match mode with
504+
| None ->
505+
Pending_targets.add targets;
506+
let sandbox = No_sandbox { targets } in
507+
Fiber.finalize ~finally:(fun () -> destroy sandbox) (fun () -> f sandbox)
508+
| Some mode ->
509+
with_live_sandbox_slot ~f:(fun () ->
510+
let open Fiber.O in
511+
let* sandbox =
512+
create_real ~mode corrections ~rule_loc ~dirs ~deps ~rule_dir ~rule_digest
513+
in
514+
let sandbox = Sandboxed sandbox in
515+
(* CR-someday rgrinberg: Dynamic actions may discover dependencies
516+
while this slot is held. If all sandbox slots are held by such
517+
actions, sandboxed rules for the discovered dependencies cannot start.
518+
*)
519+
Fiber.finalize ~finally:(fun () -> destroy sandbox) (fun () -> f sandbox))
455520
;;

src/dune_engine/sandbox.mli

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,31 @@ open Import
44

55
type t
66

7-
val dir : t -> Path.Build.t
7+
(** [is_sandboxed t] is [true] when [t] represents a real sandbox. *)
8+
val is_sandboxed : t -> bool
89

910
(** [map_path t p] returns the path corresponding to [p] inside the sandbox. *)
1011
val map_path : t -> Path.Build.t -> Path.Build.t
1112

12-
(** Run [f] while holding one global live-sandbox slot.
13+
(** Delete targets left behind by interrupted non-sandboxed actions. *)
14+
val cleanup_pending_targets : unit -> unit
1315

14-
The slot should be acquired before creating a sandbox and released after it
15-
has been destroyed, so rules cannot pre-create unbounded sandboxes while
16-
waiting for their actions to run. *)
17-
val with_live_sandbox_slot : f:(unit -> 'a Fiber.t) -> 'a Fiber.t
16+
(** Run [f] with a sandboxing context.
1817
19-
(** Create a new sandbox containing [dirs] and copy or link dependencies [deps]
20-
inside it. *)
21-
val create
22-
: mode:Sandbox_mode.some
18+
[mode = None] is represented as a sandboxing context that maps paths to
19+
themselves and only tracks pending targets for cleanup. It is not affected
20+
by the live-sandbox throttle. *)
21+
val with_
22+
: mode:Sandbox_mode.some option
2323
-> Corrections.t
2424
-> rule_loc:Loc.t
2525
-> dirs:Path.Build.Set.t
2626
-> deps:Path.Set.t
2727
-> rule_dir:Path.Build.t
2828
-> rule_digest:Digest.t
29-
-> t Fiber.t
29+
-> targets:Targets.Validated.t
30+
-> f:(t -> 'a Fiber.t)
31+
-> 'a Fiber.t
3032

3133
(** Move all targets created by the action from the sandbox to the build
3234
directory, skipping the files for which [should_be_skipped] returns [true].
@@ -37,5 +39,3 @@ val move_targets_to_build_dir
3739
-> should_be_skipped:(Path.Build.t -> bool)
3840
-> targets:Targets.Validated.t
3941
-> unit Fiber.t
40-
41-
val destroy : t -> unit Fiber.t

0 commit comments

Comments
 (0)