Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions lib/btrfs_store.ml
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,13 @@ let build t ?base ~id fn =
let result t id =
let dir = Path.result t id in
match Os.check_dir dir with
| `Present -> Some dir
| `Missing -> None
| `Present -> Lwt.return_some dir
| `Missing -> Lwt.return_none

let log_file t id =
result t id >|= function
| Some dir -> dir / "log"
| None -> (Path.result_tmp t id) / "log"

let get_cache t name =
match Hashtbl.find_opt t.caches name with
Expand Down
6 changes: 3 additions & 3 deletions lib/build.ml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ module Make (Raw_store : S.STORE) (Sandbox : S.SANDBOX) (Fetch : S.FETCHER) = st
match Scope.find_opt name scope with
| None -> Fmt.failwith "Unknown build %S" name (* (shouldn't happen; gets caught earlier) *)
| Some id ->
match Store.result t.store id with
Store.result t.store id >>= function
| None ->
Lwt_result.fail (`Msg (Fmt.str "Build result %S not found" id))
| Some dir ->
Expand Down Expand Up @@ -233,8 +233,8 @@ module Make (Raw_store : S.STORE) (Sandbox : S.SANDBOX) (Fetch : S.FETCHER) = st
(Sexplib.Sexp.to_string_hum Saved_context.(sexp_of_t {env})) >>= fun () ->
Lwt_result.return ()
)
>>!= fun id ->
let path = Option.get (Store.result t.store id) in
>>!= fun id -> Store.result t.store id
>|= Option.get >>= fun path ->
let { Saved_context.env } = Saved_context.t_of_sexp (Sexplib.Sexp.load_sexp (path / "env")) in
Lwt_result.return (id, env)

Expand Down
48 changes: 21 additions & 27 deletions lib/build_log.ml
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,13 @@ let max_chunk_size = 4096

type t = {
mutable state : [
| `Open of Lwt_unix.file_descr * unit Lwt_condition.t (* Fires after writing more data. *)
| `Open of string * Lwt_unix.file_descr * unit Lwt_condition.t (* Fires after writing more data. *)
| `Readonly of string
| `Empty
| `Finished
];
mutable len : int;
}

let with_dup fd fn =
let fd = Lwt_unix.dup fd in
Lwt_unix.set_close_on_exec fd;
Lwt.finalize
(fun () -> fn fd)
(fun () -> Lwt_unix.close fd)

let catch_cancel fn =
Lwt.catch fn
(function
Expand All @@ -27,34 +19,39 @@ let catch_cancel fn =
)

let tail ?switch t dst =
match t.state with
| `Finished -> invalid_arg "tail: log is finished!"
| `Readonly path ->
let readonly_tail path buf i =
Lwt_io.(with_file ~mode:input) path @@ fun ch ->
let buf = Bytes.create max_chunk_size in
Lwt_io.set_position ch (Int64.of_int i) >>= fun () ->
let rec aux () =
Lwt_io.read_into ch buf 0 max_chunk_size >>= function
| 0 -> Lwt_result.return ()
| n -> dst (Bytes.sub_string buf 0 n); aux ()
in
aux ()
in
match t.state with
| `Readonly path ->
let buf = Bytes.create max_chunk_size in
catch_cancel @@ fun () ->
let th = aux () in
let th = readonly_tail path buf 0 in
Lwt_switch.add_hook_or_exec switch (fun () -> Lwt.cancel th; Lwt.return_unit) >>= fun () ->
th
| `Empty -> Lwt_result.return ()
| `Open (fd, cond) ->
(* Dup [fd], which can still work after [fd] is closed. *)
with_dup fd @@ fun fd ->
| `Open (_, fd, cond) ->
let buf = Bytes.create max_chunk_size in
let rec aux i =
match switch with
| Some sw when not (Lwt_switch.is_on sw) -> Lwt_result.fail `Cancelled
| _ ->
let avail = min (t.len - i) max_chunk_size in
if avail > 0 then (
Lwt_unix.pread fd ~file_offset:i buf 0 avail >>= fun n ->
dst (Bytes.sub_string buf 0 n);
aux (i + avail)
match t.state with
| `Open _ ->
Lwt_unix.pread fd ~file_offset:i buf 0 avail >>= fun n ->
dst (Bytes.sub_string buf 0 n);
aux (i + avail)
| `Readonly path -> readonly_tail path buf i
| _ -> Lwt_result.return ()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| _ -> Lwt_result.return ()
| `Empty -> Lwt_result.return ()

) else (
match t.state with
| `Open _ -> Lwt_condition.wait cond >>= fun () -> aux i
Expand All @@ -70,28 +67,25 @@ let create path =
Lwt_unix.openfile path Lwt_unix.[O_CREAT; O_TRUNC; O_RDWR; O_CLOEXEC] 0o666 >|= fun fd ->
let cond = Lwt_condition.create () in
{
state = `Open (fd, cond);
state = `Open (path, fd, cond);
len = 0;
}

let finish t =
match t.state with
| `Finished -> invalid_arg "Log is already finished!"
| `Open (fd, cond) ->
t.state <- `Finished;
| `Open (path, fd, cond) ->
t.state <- `Readonly path;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this has the same problem: a reader will open the path in the temporary clone, preventing it from being removed (unless a read-only FD is OK somehow?).

Instead, I think this needs to transition to a new Finalising of unit Lwt.t state or similar, where the promise resolves once the log is ready for reading in its new location.

Lwt_unix.close fd >|= fun () ->
Lwt_condition.broadcast cond ()
| `Readonly _ ->
t.state <- `Finished;
Lwt.return_unit
| `Empty ->
Lwt.return_unit (* Empty can be reused *)

let write t data =
match t.state with
| `Finished -> invalid_arg "write: log is finished!"
| `Readonly _ | `Empty -> invalid_arg "Log is read-only!"
| `Open (fd, cond) ->
| `Open (_, fd, cond) ->
let len = String.length data in
Os.write_all fd (Bytes.of_string data) 0 len >>= fun () ->
t.len <- t.len + len;
Expand Down
8 changes: 4 additions & 4 deletions lib/db_store.ml
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ module Make (Raw : S.STORE) = struct
or by doing a new build using [fn]. We only run one instance of this
at a time for a single [id]. *)
let get_build t ~base ~id ~cancelled ~set_log fn =
match Raw.result t.raw id with
| Some dir ->
Raw.result t.raw id >>= function
| Some _ ->
let now = Unix.(gmtime (gettimeofday ())) in
Dao.set_used t.dao ~id ~now;
let log_file = dir / "log" in
Raw.log_file t.raw id >>= fun log_file ->
begin
if Sys.file_exists log_file then Build_log.of_saved log_file
else Lwt.return Build_log.empty
Expand All @@ -59,7 +59,7 @@ module Make (Raw : S.STORE) = struct
Lwt_result.return (`Loaded, id)
| None ->
Raw.build t.raw ?base ~id (fun dir ->
let log_file = dir / "log" in
Raw.log_file t.raw id >>= fun log_file ->
if Sys.file_exists log_file then Unix.unlink log_file;
Build_log.create log_file >>= fun log ->
Lwt.wakeup set_log log;
Expand Down
2 changes: 1 addition & 1 deletion lib/db_store.mli
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module Make (Raw : S.STORE) : sig

val prune : ?log:(S.id -> unit) -> t -> before:Unix.tm -> int -> int Lwt.t

val result : t -> S.id -> string option
val result : t -> S.id -> string option Lwt.t

val cache :
user : Obuilder_spec.user ->
Expand Down
9 changes: 7 additions & 2 deletions lib/rsync_store.ml
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,13 @@ let delete t id =
let result t id =
let dir = Path.result t id in
match Os.check_dir dir with
| `Present -> Some dir
| `Missing -> None
| `Present -> Lwt.return_some dir
| `Missing -> Lwt.return_none

let log_file t id =
result t id >|= function
| Some dir -> dir / "log"
| None -> (Path.result_tmp t id) / "log"

let state_dir t = t.path / Path.state_dirname

Expand Down
6 changes: 5 additions & 1 deletion lib/s.ml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ module type STORE = sig
val delete : t -> id -> unit Lwt.t
(** [delete t id] removes [id] from the store, if present. *)

val result : t -> id -> string option
val result : t -> id -> string option Lwt.t
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this need to be async? It seems useful to have an atomic way of finding out whether something exists in the store. Otherwise, how do we know the result is still valid by the time it has returned?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the Docker backend I'm making a call to Docker to check whether the result as a Docker image exists. I use functions from Os which calls Lwt_process.exec and they're asynchronous. I could call Unix.create_process instead and wait for the termination to fix the TOCTOU.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move that to a separate PR then. It doesn't have anything to do with the build log problem.

(** [result t id] is the path of the build result for [id], if present. *)

val log_file : t -> id -> string Lwt.t
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want a different log location for each store?

(** [log_file t id] is the path of the build logs for [id]. The file may
not exist if the build has never been run, or failed. *)

val state_dir : t -> string
(** [state_dir] is the path of a directory which can be used to store mutable
state related to this store (e.g. an sqlite3 database). *)
Expand Down
12 changes: 10 additions & 2 deletions lib/zfs_store.ml
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,16 @@ let build t ?base ~id fn =
let result t id =
let ds = Dataset.result id in
let path = Dataset.path t ds ~snapshot:default_snapshot in
if Sys.file_exists path then Some path
else None
if Sys.file_exists path then Lwt.return_some path
else Lwt.return_none

let log_file t id =
result t id >|= function
| Some dir -> Filename.concat dir "log"
| None ->
let ds = Dataset.result id in
let clone = Dataset.path t ds in
Filename.concat clone "log"

let get_cache t name =
match Hashtbl.find_opt t.caches name with
Expand Down
17 changes: 9 additions & 8 deletions stress/stress.ml
Original file line number Diff line number Diff line change
Expand Up @@ -21,38 +21,39 @@ module Fetcher = Docker

module Test(Store : S.STORE) = struct
let assert_output expected t id =
match Store.result t id with
Store.result t id >>= function
| None -> Fmt.failwith "%S not in store!" id
| Some path ->
let ch = open_in (path / "output") in
let data = really_input_string ch (in_channel_length ch) in
close_in ch;
assert_str expected data
assert_str expected data;
Lwt.return_unit

let test_store t =
assert (Store.result t "unknown" = None);
Store.result t "unknown" >>= fun r -> assert (r = None);
(* Build without a base *)
Store.delete t "base" >>= fun () ->
Store.build t ~id:"base" (fun tmpdir -> write ~path:(tmpdir / "output") "ok" >|= Result.ok) >>= fun r ->
assert (r = Ok ());
assert_output "ok" t "base";
assert_output "ok" t "base" >>= fun () ->
(* Build with a base *)
Store.delete t "sub" >>= fun () ->
Store.build t ~base:"base" ~id:"sub" (fun tmpdir ->
read (tmpdir / "output") >>= fun orig ->
write ~path:(tmpdir / "output") (orig ^ "+") >|= Result.ok
) >>= fun r ->
assert (r = Ok ());
assert_output "ok+" t "sub";
assert_output "ok+" t "sub" >>= fun () ->
(* Test deletion *)
assert (Store.result t "sub" <> None);
Store.result t "sub" >>= fun r -> assert (r <> None);
Store.delete t "sub" >>= fun () ->
assert (Store.result t "sub" = None);
Store.result t "sub" >>= fun r -> assert (r = None);
(* A failing build isn't saved *)
Store.delete t "fail" >>= fun () ->
Store.build t ~id:"fail" (fun _tmpdir -> Lwt_result.fail `Failed) >>= fun r ->
assert (r = Error `Failed);
assert (Store.result t "fail" = None);
Store.result t "fail" >>= fun r -> assert (r = None);
Lwt.return_unit

let test_cache t =
Expand Down
11 changes: 8 additions & 3 deletions test/mock_store.ml
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,13 @@ let path t id = t.dir / id
let result t id =
let dir = path t id in
match Os.check_dir dir with
| `Present -> Some dir
| `Missing -> None
| `Present -> Lwt.return_some dir
| `Missing -> Lwt.return_none

let log_file t id =
result t id >|= function
| Some dir -> dir / "log"
| None -> t.dir / (id ^ "-tmp") / "log"

let rec finish t =
if t.builds > 0 then (
Expand All @@ -80,7 +85,7 @@ let with_store fn =
(fun () -> finish t)

let delete t id =
match result t id with
result t id >>= function
| Some path -> rm_r path; Lwt.return_unit
| None -> Lwt.return_unit

Expand Down