Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Page
Library
Module
Module type
Parameter
Class
Class type
Source
btrfs_store.ml1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208open Lwt.Infix let strf = Printf.sprintf let running_as_root = Unix.getuid () = 0 (* Represents a persistent cache. You must hold a cache's lock when removing or updating its entry in "cache", and must assume this may happen at any time when not holding it. The generation counter is used to check whether the cache has been updated since being cloned. The counter starts from zero when the in-memory cache value is created (i.e. you cannot compare across restarts). *) type cache = { lock : Lwt_mutex.t; mutable gen : int; } type t = { root : string; (* The top-level directory (containing `result`, etc). *) caches : (string, cache) Hashtbl.t; mutable next : int; (* Used to generate unique temporary IDs. *) } let ( / ) = Filename.concat module Btrfs = struct let btrfs ?(sudo=false) args = let args = "btrfs" :: args in let args = if sudo && not running_as_root then "sudo" :: args else args in Os.exec ~stdout:`Dev_null args let subvolume_create path = assert (not (Sys.file_exists path)); btrfs ["subvolume"; "create"; "--"; path] let subvolume_delete path = btrfs ~sudo:true ["subvolume"; "delete"; "--"; path] let subvolume_sync path = btrfs ~sudo:true ["subvolume"; "sync"; "--"; path] let subvolume_snapshot mode ~src dst = assert (not (Sys.file_exists dst)); let readonly = match mode with | `RO -> ["-r"] | `RW -> [] in btrfs ~sudo:true (["subvolume"; "snapshot"] @ readonly @ ["--"; src; dst]) end let delete_snapshot_if_exists path = match Os.check_dir path with | `Missing -> Lwt.return_unit | `Present -> Btrfs.subvolume_delete path module Path = struct (* A btrfs store contains several subdirectories: - result: completed builds, named by ID - result-tmp: in-progress builds - state: for sqlite DB, etc - cache: the latest version of each cache, by cache ID - cache-tmp: in-progress updates to caches result-tmp and cache-tmp are wiped at start-up. *) let result t id = t.root / "result" / id let result_tmp t id = t.root / "result-tmp" / id let state t = t.root / "state" let cache t name = t.root / "cache" / Escape.cache name let cache_tmp t i name = t.root / "cache-tmp" / strf "%d-%s" i (Escape.cache name) end let delete t id = delete_snapshot_if_exists (Path.result t id) let purge path = Sys.readdir path |> Array.to_list |> Lwt_list.iter_s (fun item -> let item = path / item in Log.warn (fun f -> f "Removing left-over temporary item %S" item); Btrfs.subvolume_delete item ) let check_kernel_version () = Os.pread ["uname"; "-r"] >>= fun kver -> match String.split_on_char '.' kver with | maj :: min :: _ -> begin match int_of_string_opt maj, int_of_string_opt min with | Some maj, Some min when (maj, min) >= (5, 8) -> Lwt.return_unit | Some maj, Some min -> Lwt.fail_with (Fmt.str "You need at least linux 5.8 to use the btrfs backend, \ but current kernel version is '%d.%d'" maj min) | _, _ -> Fmt.failwith "Could not parse kernel version %S" kver end | _ -> Fmt.failwith "Could not parse output of 'uname -r' (%S)" kver let root t = t.root let create root = check_kernel_version () >>= fun () -> Os.ensure_dir (root / "result"); Os.ensure_dir (root / "result-tmp"); Os.ensure_dir (root / "state"); Os.ensure_dir (root / "cache"); Os.ensure_dir (root / "cache-tmp"); purge (root / "result-tmp") >>= fun () -> purge (root / "cache-tmp") >>= fun () -> Lwt.return { root; caches = Hashtbl.create 10; next = 0 } let build t ?base ~id fn = let result = Path.result t id in let result_tmp = Path.result_tmp t id in assert (not (Sys.file_exists result)); (* Builder should have checked first *) begin match base with | None -> Btrfs.subvolume_create result_tmp | Some base -> Btrfs.subvolume_snapshot `RW ~src:(Path.result t base) result_tmp end >>= fun () -> Lwt.try_bind (fun () -> fn result_tmp) (fun r -> begin match r with | Ok () -> Btrfs.subvolume_snapshot `RO ~src:result_tmp result | Error _ -> Lwt.return_unit end >>= fun () -> Btrfs.subvolume_delete result_tmp >>= fun () -> Lwt.return r ) (fun ex -> Log.warn (fun f -> f "Uncaught exception from %S build function: %a" id Fmt.exn ex); Btrfs.subvolume_delete result_tmp >>= fun () -> Lwt.fail ex ) let result t id = let dir = Path.result t id in match Os.check_dir dir with | `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 | Some c -> c | None -> let c = { lock = Lwt_mutex.create (); gen = 0 } in Hashtbl.add t.caches name c; c let cache ~user t name : (string * (unit -> unit Lwt.t)) Lwt.t = let cache = get_cache t name in Lwt_mutex.with_lock cache.lock @@ fun () -> let tmp = Path.cache_tmp t t.next name in t.next <- t.next + 1; let snapshot = Path.cache t name in (* Create cache if it doesn't already exist. *) begin match Os.check_dir snapshot with | `Missing -> Btrfs.subvolume_create snapshot | `Present -> Lwt.return_unit end >>= fun () -> (* Create writeable clone. *) let gen = cache.gen in Btrfs.subvolume_snapshot `RW ~src:snapshot tmp >>= fun () -> begin match user with | `Unix { Obuilder_spec.uid; gid } -> Os.sudo ["chown"; Printf.sprintf "%d:%d" uid gid; tmp] | `Windows _ -> assert false (* btrfs not supported on Windows*) end >>= fun () -> let release () = Lwt_mutex.with_lock cache.lock @@ fun () -> begin if cache.gen = gen then ( (* The cache hasn't changed since we cloned it. Update it. *) (* todo: check if it has actually changed. *) cache.gen <- cache.gen + 1; Btrfs.subvolume_delete snapshot >>= fun () -> Btrfs.subvolume_snapshot `RO ~src:tmp snapshot ) else Lwt.return_unit end >>= fun () -> Btrfs.subvolume_delete tmp in Lwt.return (tmp, release) let delete_cache t name = let cache = get_cache t name in Lwt_mutex.with_lock cache.lock @@ fun () -> cache.gen <- cache.gen + 1; (* Ensures in-progress writes will be discarded *) let snapshot = Path.cache t name in if Sys.file_exists snapshot then ( Btrfs.subvolume_delete snapshot >>= fun () -> Lwt_result.return () ) else Lwt_result.return () let state_dir = Path.state let complete_deletes t = Btrfs.subvolume_sync t.root