diff --git a/lib/btrfs_store.ml b/lib/btrfs_store.ml index 5974dffc..c26bcda6 100644 --- a/lib/btrfs_store.ml +++ b/lib/btrfs_store.ml @@ -173,39 +173,56 @@ let get_cache t name = Hashtbl.add t.caches name c; c -let cache ~user t name : (string * (unit -> unit Lwt.t)) Lwt.t = +let cache ?(shared=false) ~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 () = + if shared then + (* Shared mode: return the actual cache directory, no copy-on-write *) 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 + (* 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 () -> - Btrfs.subvolume_delete tmp - in - Lwt.return (tmp, release) + begin match user with + | `Unix { Obuilder_spec.uid; gid } -> + Os.sudo ["chown"; Printf.sprintf "%d:%d" uid gid; snapshot] + | `Windows _ -> assert false (* btrfs not supported on Windows*) + end >>= fun () -> + let release () = Lwt.return_unit in (* No-op for shared caches *) + Lwt.return (snapshot, release) + else + (* Non-shared mode: existing copy-on-write behavior *) + Lwt_mutex.with_lock cache.lock @@ fun () -> + let tmp = Path.cache_tmp t t.next name in + t.next <- t.next + 1; + (* 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 diff --git a/lib/build.ml b/lib/build.ml index b8a0b8fd..ff3944c9 100644 --- a/lib/build.ml +++ b/lib/build.ml @@ -82,8 +82,10 @@ module Make (Raw_store : S.STORE) (Sandbox : S.SANDBOX) (Fetch : S.FETCHER) = st let to_release = ref [] in Lwt.finalize (fun () -> - cache |> Lwt_list.map_s (fun { Obuilder_spec.Cache.id; target; buildkit_options = _ } -> - Store.cache ~user t.store id >|= fun (src, release) -> + cache |> Lwt_list.map_s (fun { Obuilder_spec.Cache.id; target; buildkit_options } -> + let shared = List.mem_assoc "sharing" buildkit_options + && List.assoc "sharing" buildkit_options = "shared" in + Store.cache ~shared ~user t.store id >|= fun (src, release) -> to_release := release :: !to_release; { Config.Mount.ty = `Bind; src; dst = target; readonly = false } ) @@ -367,8 +369,10 @@ module Make_Docker (Raw_store : S.STORE) = struct let to_release = ref [] in Lwt.finalize (fun () -> - cache |> Lwt_list.map_s (fun { Obuilder_spec.Cache.id; target; buildkit_options = _ } -> - Store.cache ~user t.store id >|= fun (src, release) -> + cache |> Lwt_list.map_s (fun { Obuilder_spec.Cache.id; target; buildkit_options } -> + let shared = List.mem_assoc "sharing" buildkit_options + && List.assoc "sharing" buildkit_options = "shared" in + Store.cache ~shared ~user t.store id >|= fun (src, release) -> to_release := release :: !to_release; { Config.Mount.ty = `Volume; src; dst = target; readonly = false } ) diff --git a/lib/db_store.ml b/lib/db_store.ml index a2be43b5..37d33238 100644 --- a/lib/db_store.ml +++ b/lib/db_store.ml @@ -134,7 +134,7 @@ module Make (Raw : S.STORE) = struct let df t = Raw.df t.raw let root t = Raw.root t.raw let cache_stats t = t.cache_hit, t.cache_miss - let cache ~user t = Raw.cache ~user t.raw + let cache ?shared ~user t = Raw.cache ?shared ~user t.raw let delete ?(log=ignore) t id = let rec aux id = diff --git a/lib/db_store.mli b/lib/db_store.mli index 74937a02..f5e42284 100644 --- a/lib/db_store.mli +++ b/lib/db_store.mli @@ -29,6 +29,7 @@ module Make (Raw : S.STORE) : sig val cache_stats : t -> int * int val cache : + ?shared:bool -> user : Obuilder_spec.user -> t -> string -> diff --git a/lib/docker_store.ml b/lib/docker_store.ml index a9a44e24..14dbd1fe 100644 --- a/lib/docker_store.ml +++ b/lib/docker_store.ml @@ -162,41 +162,60 @@ let get_cache t name = Hashtbl.add t.caches name c; c -let cache ~user t name : (string * (unit -> unit Lwt.t)) Lwt.t = +let cache ?(shared=false) ~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 = Cache.cache_tmp t.next name in - t.next <- t.next + 1; let snapshot = Cache.cache name in - (* Create cache if it doesn't already exist. *) - let* () = - let* exists = Cache.exists snapshot in - if not exists then Cache.create snapshot - else Lwt.return_unit - in - (* Create writeable clone. *) - let gen = cache.gen in - let* () = Cache.snapshot ~src:snapshot tmp in - let+ () = match user with - | `Unix { Obuilder_spec.uid; gid } -> - let* tmp = Docker.Cmd.mount_point tmp in - Os.sudo ["chown"; strf "%d:%d" uid gid; tmp] - | `Windows _ -> Lwt.return_unit (* FIXME: does Windows need special treatment? *) - in - let release () = + if shared then + (* Shared mode: return the actual cache volume, no copy-on-write *) Lwt_mutex.with_lock cache.lock @@ fun () -> + (* Create cache if it doesn't already exist. *) let* () = - 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; - let* () = Cache.delete snapshot in - Cache.snapshot ~src:tmp snapshot - ) else Lwt.return_unit + let* exists = Cache.exists snapshot in + if not exists then Cache.create snapshot + else Lwt.return_unit in - Cache.delete tmp - in - Cache.name tmp, release + let+ () = match user with + | `Unix { Obuilder_spec.uid; gid } -> + let* mp = Docker.Cmd.mount_point snapshot in + Os.sudo ["chown"; strf "%d:%d" uid gid; mp] + | `Windows _ -> Lwt.return_unit (* FIXME: does Windows need special treatment? *) + in + let release () = Lwt.return_unit in (* No-op for shared caches *) + Cache.name snapshot, release + else + (* Non-shared mode: existing snapshot behavior *) + Lwt_mutex.with_lock cache.lock @@ fun () -> + let tmp = Cache.cache_tmp t.next name in + t.next <- t.next + 1; + (* Create cache if it doesn't already exist. *) + let* () = + let* exists = Cache.exists snapshot in + if not exists then Cache.create snapshot + else Lwt.return_unit + in + (* Create writeable clone. *) + let gen = cache.gen in + let* () = Cache.snapshot ~src:snapshot tmp in + let+ () = match user with + | `Unix { Obuilder_spec.uid; gid } -> + let* tmp = Docker.Cmd.mount_point tmp in + Os.sudo ["chown"; strf "%d:%d" uid gid; tmp] + | `Windows _ -> Lwt.return_unit (* FIXME: does Windows need special treatment? *) + in + let release () = + Lwt_mutex.with_lock cache.lock @@ fun () -> + let* () = + 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; + let* () = Cache.delete snapshot in + Cache.snapshot ~src:tmp snapshot + ) else Lwt.return_unit + in + Cache.delete tmp + in + Cache.name tmp, release let delete_cache t name = let cache = get_cache t name in diff --git a/lib/overlayfs_store.ml b/lib/overlayfs_store.ml index c4f61f28..47b75d58 100644 --- a/lib/overlayfs_store.ml +++ b/lib/overlayfs_store.ml @@ -243,29 +243,41 @@ let get_cache t name = Hashtbl.add t.caches name c; c -let cache ~user t name = +let cache ?(shared=false) ~user t name = let cache = get_cache t name in - Lwt_mutex.with_lock cache.lock @@ fun () -> - let result, work, merged = Path.cache_result t t.next name in - t.next <- t.next + 1; let master = Path.cache t name in - (* Create cache if it doesn't already exist. *) - (match Os.check_dir master with - | `Missing -> Overlayfs.create ~mode:"1777" ~user [ master ] - | `Present -> Lwt.return_unit) - >>= fun () -> - cache.children <- cache.children + 1; - Overlayfs.create ~mode:"1777" ~user [ result; work; merged ] >>= fun () -> - let lower = String.split_on_char ':' master |> String.concat "\\:" in - Overlayfs.overlay ~lower ~upper:result ~work ~merged >>= fun () -> - let release () = + if shared then + (* Shared mode: return the actual cache directory, no copy-on-write *) Lwt_mutex.with_lock cache.lock @@ fun () -> - cache.children <- cache.children - 1; - Overlayfs.umount ~merged >>= fun () -> - Overlayfs.cp ~src:result ~dst:master >>= fun () -> - Overlayfs.delete [ result; work; merged ] - in - Lwt.return (merged, release) + (* Create cache if it doesn't already exist. *) + (match Os.check_dir master with + | `Missing -> Overlayfs.create ~mode:"1777" ~user [ master ] + | `Present -> Lwt.return_unit) + >>= fun () -> + let release () = Lwt.return_unit in (* No-op for shared caches *) + Lwt.return (master, release) + else + (* Non-shared mode: existing overlay behavior *) + Lwt_mutex.with_lock cache.lock @@ fun () -> + let result, work, merged = Path.cache_result t t.next name in + t.next <- t.next + 1; + (* Create cache if it doesn't already exist. *) + (match Os.check_dir master with + | `Missing -> Overlayfs.create ~mode:"1777" ~user [ master ] + | `Present -> Lwt.return_unit) + >>= fun () -> + cache.children <- cache.children + 1; + Overlayfs.create ~mode:"1777" ~user [ result; work; merged ] >>= fun () -> + let lower = String.split_on_char ':' master |> String.concat "\\:" in + Overlayfs.overlay ~lower ~upper:result ~work ~merged >>= fun () -> + let release () = + Lwt_mutex.with_lock cache.lock @@ fun () -> + cache.children <- cache.children - 1; + Overlayfs.umount ~merged >>= fun () -> + Overlayfs.cp ~src:result ~dst:master >>= fun () -> + Overlayfs.delete [ result; work; merged ] + in + Lwt.return (merged, release) let delete_cache t name = let () = Printf.printf "0\n" in diff --git a/lib/qemu_store.ml b/lib/qemu_store.ml index 2d5ac2f8..641e9636 100644 --- a/lib/qemu_store.ml +++ b/lib/qemu_store.ml @@ -128,31 +128,42 @@ let get_cache t name = Hashtbl.add t.caches name c; c -let cache ~user:_ t name : (string * (unit -> unit Lwt.t)) Lwt.t = +let cache ?(shared=false) ~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 master = Path.cache t name in - (* Create cache if it doesn't already exist. *) - (match Os.check_dir master with - | `Missing -> Qemu_img.create master - | `Present -> Lwt.return ()) >>= fun () -> - cache.children <- cache.children + 1; - let () = Os.ensure_dir tmp in - Os.cp ~src:master tmp >>= fun () -> - let release () = + if shared then + (* Shared mode: return the actual cache directory, no copy-on-write *) + Lwt_mutex.with_lock cache.lock @@ fun () -> + (* Create cache if it doesn't already exist. *) + (match Os.check_dir master with + | `Missing -> Qemu_img.create master + | `Present -> Lwt.return ()) >>= fun () -> + let release () = Lwt.return_unit in (* No-op for shared caches *) + Lwt.return (master, release) + else + (* Non-shared mode: existing copy behavior *) Lwt_mutex.with_lock cache.lock @@ fun () -> - cache.children <- cache.children - 1; - let cache_stat = Unix.stat (Path.image master) in - let tmp_stat = Unix.stat (Path.image tmp) in - (if tmp_stat.st_size > cache_stat.st_size then - Os.cp ~src:tmp master - else - Lwt.return ()) >>= fun () -> - Os.rm ~directory:tmp - in - Lwt.return (tmp, release) + let tmp = Path.cache_tmp t t.next name in + t.next <- t.next + 1; + (* Create cache if it doesn't already exist. *) + (match Os.check_dir master with + | `Missing -> Qemu_img.create master + | `Present -> Lwt.return ()) >>= fun () -> + cache.children <- cache.children + 1; + let () = Os.ensure_dir tmp in + Os.cp ~src:master tmp >>= fun () -> + let release () = + Lwt_mutex.with_lock cache.lock @@ fun () -> + cache.children <- cache.children - 1; + let cache_stat = Unix.stat (Path.image master) in + let tmp_stat = Unix.stat (Path.image tmp) in + (if tmp_stat.st_size > cache_stat.st_size then + Os.cp ~src:tmp master + else + Lwt.return ()) >>= fun () -> + Os.rm ~directory:tmp + in + Lwt.return (tmp, release) let delete_cache t name = let cache = get_cache t name in diff --git a/lib/rsync_store.ml b/lib/rsync_store.ml index f95d2ff0..4197f95c 100644 --- a/lib/rsync_store.ml +++ b/lib/rsync_store.ml @@ -140,39 +140,56 @@ let get_cache t name = Hashtbl.add t.caches name c; c -let cache ~user t name = +let cache ?(shared=false) ~user t name = 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 -> Rsync.create snapshot - | `Present -> Lwt.return_unit - end >>= fun () -> - (* Create writeable clone. *) - let gen = cache.gen in - let { Obuilder_spec.uid; gid } = match user with - | `Unix user -> user - | `Windows _ -> assert false (* rsync not supported on Windows *) - in - (* rsync --chown not supported by the rsync that macOS ships with *) - Rsync.copy_children ~src:snapshot ~dst:tmp () >>= fun () -> - Os.sudo [ "chown"; Printf.sprintf "%d:%d" uid gid; tmp ] >>= fun () -> - let release () = + if shared then + (* Shared mode: return the actual cache directory, no copy-on-write *) + Lwt_mutex.with_lock cache.lock @@ fun () -> + (* Create cache if it doesn't already exist. *) + begin match Os.check_dir snapshot with + | `Missing -> Rsync.create snapshot + | `Present -> Lwt.return_unit + end >>= fun () -> + let { Obuilder_spec.uid; gid } = match user with + | `Unix user -> user + | `Windows _ -> assert false (* rsync not supported on Windows *) + in + Os.sudo [ "chown"; Printf.sprintf "%d:%d" uid gid; snapshot ] >>= fun () -> + let release () = Lwt.return_unit in (* No-op for shared caches *) + Lwt.return (snapshot, release) + else + (* Non-shared mode: existing rsync behavior *) 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; - Rsync.delete snapshot >>= fun () -> - Rsync.rename ~src:tmp ~dst:snapshot - ) else Lwt.return_unit - end - in - Lwt.return (tmp, release) + let tmp = Path.cache_tmp t t.next name in + t.next <- t.next + 1; + (* Create cache if it doesn't already exist. *) + begin match Os.check_dir snapshot with + | `Missing -> Rsync.create snapshot + | `Present -> Lwt.return_unit + end >>= fun () -> + (* Create writeable clone. *) + let gen = cache.gen in + let { Obuilder_spec.uid; gid } = match user with + | `Unix user -> user + | `Windows _ -> assert false (* rsync not supported on Windows *) + in + (* rsync --chown not supported by the rsync that macOS ships with *) + Rsync.copy_children ~src:snapshot ~dst:tmp () >>= fun () -> + Os.sudo [ "chown"; Printf.sprintf "%d:%d" uid gid; tmp ] >>= 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; + Rsync.delete snapshot >>= fun () -> + Rsync.rename ~src:tmp ~dst:snapshot + ) else Lwt.return_unit + end + in + Lwt.return (tmp, release) let delete_cache t name = diff --git a/lib/s.ml b/lib/s.ml index 345c5faf..04c839b0 100644 --- a/lib/s.ml +++ b/lib/s.ml @@ -49,18 +49,22 @@ module type STORE = sig state related to this store (e.g. an sqlite3 database). *) val cache : + ?shared:bool -> user:Obuilder_spec.user -> t -> string -> (string * (unit -> unit Lwt.t)) Lwt.t - (** [cache ~user t name] creates a writeable copy of the latest snapshot of the + (** [cache ?shared ~user t name] creates a writeable copy of the latest snapshot of the cache [name]. It returns the path of this fresh copy and a function which must be called to free it when done. If the cache [name] does not exist, it is first created (as an empty directory, and owned by [user]). When the copy is released, it is snapshotted to become the new latest version of the cache, unless the cache has already been updated since - it was snapshotted, in which case this writeable copy is simply discarded. *) + it was snapshotted, in which case this writeable copy is simply discarded. + @param shared If [true], returns the actual cache directory for concurrent access. + Jobs must implement their own locking. The release function becomes a no-op. + Defaults to [false] for backward compatibility. *) val delete_cache : t -> string -> (unit, [> `Busy]) Lwt_result.t (** [delete_cache t name] removes the cache [name], if present. diff --git a/lib/xfs_store.ml b/lib/xfs_store.ml index 992a0681..14e71ebe 100644 --- a/lib/xfs_store.ml +++ b/lib/xfs_store.ml @@ -108,39 +108,56 @@ let get_cache t name = Hashtbl.add t.caches name c; c -let cache ~user t name = +let cache ?(shared=false) ~user t name = 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 -> Xfs.create snapshot >>= fun () -> - let { Obuilder_spec.uid; gid } = match user with - | `Unix user -> user - | `Windows _ -> assert false (* xfs not supported on Windows *) - in - Os.sudo [ "chown"; Printf.sprintf "%d:%d" uid gid; snapshot ] - | `Present -> Lwt.return_unit - end >>= fun () -> - (* Create writeable clone. *) - let gen = cache.gen in - Xfs.cp ~src:snapshot ~dst:tmp >>= fun () -> - let release () = + if shared then + (* Shared mode: return the actual cache directory, no copy-on-write *) + Lwt_mutex.with_lock cache.lock @@ fun () -> + (* Create cache if it doesn't already exist. *) + begin match Os.check_dir snapshot with + | `Missing -> Xfs.create snapshot >>= fun () -> + let { Obuilder_spec.uid; gid } = match user with + | `Unix user -> user + | `Windows _ -> assert false (* xfs not supported on Windows *) + in + Os.sudo [ "chown"; Printf.sprintf "%d:%d" uid gid; snapshot ] + | `Present -> Lwt.return_unit + end >>= fun () -> + let release () = Lwt.return_unit in (* No-op for shared caches *) + Lwt.return (snapshot, release) + else + (* Non-shared mode: existing reflink behavior *) 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; - Xfs.delete snapshot >>= fun () -> - Xfs.rename ~src:tmp ~dst:snapshot - ) else - Xfs.delete tmp - end - in - Lwt.return (tmp, release) + let tmp = Path.cache_tmp t t.next name in + t.next <- t.next + 1; + (* Create cache if it doesn't already exist. *) + begin match Os.check_dir snapshot with + | `Missing -> Xfs.create snapshot >>= fun () -> + let { Obuilder_spec.uid; gid } = match user with + | `Unix user -> user + | `Windows _ -> assert false (* xfs not supported on Windows *) + in + Os.sudo [ "chown"; Printf.sprintf "%d:%d" uid gid; snapshot ] + | `Present -> Lwt.return_unit + end >>= fun () -> + (* Create writeable clone. *) + let gen = cache.gen in + Xfs.cp ~src:snapshot ~dst:tmp >>= 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; + Xfs.delete snapshot >>= fun () -> + Xfs.rename ~src:tmp ~dst:snapshot + ) else + Xfs.delete tmp + end + in + Lwt.return (tmp, release) let delete_cache t name = let cache = get_cache t name in diff --git a/lib/zfs_store.ml b/lib/zfs_store.ml index a810c18a..8ce5d84a 100644 --- a/lib/zfs_store.ml +++ b/lib/zfs_store.ml @@ -307,23 +307,38 @@ let get_tmp_ds t name = - We might crash before making the main@snap tag. If main is missing this tag, it is safe to create it, since we must have been just about to do that. *) -let cache ~user t name : (string * (unit -> unit Lwt.t)) Lwt.t = +let cache ?(shared=false) ~user t name : (string * (unit -> unit Lwt.t)) Lwt.t = let cache = get_cache t name in - Lwt_mutex.with_lock cache.lock @@ fun () -> - Log.debug (fun f -> f "zfs: get cache %S" (name :> string)); - let gen = cache.gen in let main_ds = Dataset.cache name in - let tmp_ds = get_tmp_ds t name in - (* Create the cache as an empty directory if it doesn't exist. *) - Dataset.if_missing t main_ds (fun () -> Zfs.create t main_ds) >>= fun () -> - (* Ensure we have the snapshot. This is needed on first creation, and - also to recover from crashes. *) - Dataset.if_missing t main_ds ~snapshot:default_snapshot (fun () -> - Zfs.chown ~user t main_ds >>= fun () -> - Zfs.snapshot t main_ds ~snapshot:default_snapshot - ) >>= fun () -> - cache.n_clones <- cache.n_clones + 1; - Zfs.clone t ~src:main_ds ~snapshot:default_snapshot tmp_ds >>= fun () -> + if shared then + (* Shared mode: return the actual cache directory, no copy-on-write *) + Lwt_mutex.with_lock cache.lock @@ fun () -> + Log.debug (fun f -> f "zfs: get cache %S (shared)" (name :> string)); + (* Create the cache as an empty directory if it doesn't exist. *) + Dataset.if_missing t main_ds (fun () -> Zfs.create t main_ds) >>= fun () -> + (* Ensure we have the snapshot. This is needed on first creation. *) + Dataset.if_missing t main_ds ~snapshot:default_snapshot (fun () -> + Zfs.chown ~user t main_ds >>= fun () -> + Zfs.snapshot t main_ds ~snapshot:default_snapshot + ) >>= fun () -> + let release () = Lwt.return_unit in (* No-op for shared caches *) + Lwt.return (Dataset.path t main_ds, release) + else + (* Non-shared mode: existing clone behavior *) + Lwt_mutex.with_lock cache.lock @@ fun () -> + Log.debug (fun f -> f "zfs: get cache %S" (name :> string)); + let gen = cache.gen in + let tmp_ds = get_tmp_ds t name in + (* Create the cache as an empty directory if it doesn't exist. *) + Dataset.if_missing t main_ds (fun () -> Zfs.create t main_ds) >>= fun () -> + (* Ensure we have the snapshot. This is needed on first creation, and + also to recover from crashes. *) + Dataset.if_missing t main_ds ~snapshot:default_snapshot (fun () -> + Zfs.chown ~user t main_ds >>= fun () -> + Zfs.snapshot t main_ds ~snapshot:default_snapshot + ) >>= fun () -> + cache.n_clones <- cache.n_clones + 1; + Zfs.clone t ~src:main_ds ~snapshot:default_snapshot tmp_ds >>= fun () -> let release () = Lwt_mutex.with_lock cache.lock @@ fun () -> Log.debug (fun f -> f "zfs: release cache %S" (name :> string));