Skip to content

Commit fde3bb0

Browse files
committed
Overlayfs backend
1 parent c0eb046 commit fde3bb0

File tree

6 files changed

+336
-9
lines changed

6 files changed

+336
-9
lines changed

.github/workflows/main.sh

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,26 @@ sudo chmod a+x /usr/local/bin/uname
1616
opam exec -- make
1717

1818
case "$1" in
19+
overlayfs)
20+
sudo chmod a+x /usr/local/bin/runc
21+
22+
sudo mkdir /overlayfs
23+
sudo mount -t tmpfs -o size=10G tmpfs /overlayfs
24+
sudo chown "$(whoami)" /overlayfs
25+
26+
opam exec -- dune exec -- obuilder healthcheck --store=overlayfs:/overlayfs
27+
opam exec -- dune exec -- ./stress/stress.exe --store=overlayfs:/overlayfs
28+
29+
# Populate the caches from our own GitHub Actions cache
30+
mkdir -p /overlayfs/cache/c-opam-archives
31+
cp -r ~/.opam/download-cache/* /overlayfs/cache/c-opam-archives/
32+
sudo chown -R 1000:1000 /overlayfs/cache/c-opam-archives
33+
34+
opam exec -- dune exec -- obuilder build -f example.spec . --store=overlayfs:/overlayfs --color=always
35+
36+
sudo umount /overlayfs
37+
;;
38+
1939
xfs)
2040
sudo chmod a+x /usr/local/bin/runc
2141

@@ -165,6 +185,6 @@ case "$1" in
165185
;;
166186

167187
*)
168-
printf "Usage: .run-gha-tests.sh [btrfs|rsync_hardlink|rsync_copy|zfs]" >&2
188+
printf "Usage: .run-gha-tests.sh [btrfs|rsync_hardlink|rsync_copy|zfs|overlayfs]" >&2
169189
exit 1
170190
esac

.github/workflows/main.yml

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,9 @@ jobs:
2323
runs-on: ${{ matrix.os }}
2424

2525
steps:
26-
# The ppa is needed because of https://www.mail-archive.com/ubuntu-bugs@lists.ubuntu.com/msg5972997.html
27-
- run: |
28-
sudo add-apt-repository ppa:jonathonf/zfs && \
29-
sudo apt-get --allow-releaseinfo-change update
30-
3126
- uses: awalsh128/cache-apt-pkgs-action@latest
3227
with:
33-
packages: btrfs-progs zfs-dkms zfsutils-linux xfsprogs
28+
packages: btrfs-progs zfsutils-linux xfsprogs
3429
version: 2
3530

3631
- name: Checkout code
@@ -55,6 +50,7 @@ jobs:
5550
run: |
5651
sudo wget https://github.com/opencontainers/runc/releases/download/$RUNC_VERSION/runc.amd64 -O /usr/local/bin/runc
5752
53+
- run: $GITHUB_WORKSPACE/.github/workflows/main.sh overlayfs
5854
- run: $GITHUB_WORKSPACE/.github/workflows/main.sh btrfs
5955
- run: $GITHUB_WORKSPACE/.github/workflows/main.sh zfs
6056
- run: $GITHUB_WORKSPACE/.github/workflows/main.sh xfs

lib/os.ml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,11 @@ let ensure_dir ?(mode=0o777) path =
218218
| `Present -> ()
219219
| `Missing -> Unix.mkdir path mode
220220

221+
let read_link x =
222+
match Unix.readlink x with
223+
| s -> Some s
224+
| exception Unix.Unix_error(Unix.ENOENT, _, _) -> None
225+
221226
let rm ~directory =
222227
let pp _ ppf = Fmt.pf ppf "[ RM ]" in
223228
sudo_result ~pp:(pp "RM") ["rm"; "-r"; directory ] >>= fun t ->
@@ -287,3 +292,12 @@ let free_space_percent root_dir =
287292
let used = Int64.sub vfs.f_blocks vfs.f_bfree in
288293
100. -. 100. *. (Int64.to_float used) /. Int64.(to_float (add used vfs.f_bavail))
289294

295+
let read_lines name process =
296+
let ic = open_in name in
297+
let try_read () =
298+
try Some (input_line ic) with End_of_file -> None in
299+
let rec loop acc = match try_read () with
300+
| Some s -> loop ((process s) :: acc)
301+
| None -> close_in ic; acc in
302+
loop []
303+

lib/overlayfs_store.ml

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
(*
2+
Overlayfs creates a writable layer on top of an existing file system. e.g.
3+
4+
mkdir {lower,upper,work,merge}
5+
mount -t overlay overlay -olowerdir=./lower,upperdir=./upper,workdir=./work ./merge
6+
7+
./lower would be our base image, ./upper is overlayed on top of ./lower resulting in ./merge.
8+
./merge is r/w, with the writes held in ./upper.
9+
./work is a temporary working directory.
10+
11+
Overlayfs supports lowerdir being another overlayfs. Sadly, the kernel source limits the depth of the stack to 2.
12+
13+
#define FILESYSTEM_MAX_STACK_DEPTH 2
14+
15+
However, lowerdir maybe a colon seperated list of lower layers which are stacked left to right.
16+
17+
mount -t overlay overlay -olowerdir=./l1:./l2:./l3,upperdir=./upper,workdir=./work ./merge
18+
19+
l1 being the lowerest layer with l2 being the middle layer with l3 on top.
20+
21+
The layer stacking order is maintained using a symlink "parent" created in each lowerdir pointing to the parent.
22+
23+
Overlayfs can be used on top of many other filesystem including ext4, xfs and tmpfs. 128GB tmpfs could be created as follows:
24+
25+
mount -t tmpfs -o size=128g tmpfs /var/cache/obuilder
26+
ocluster-worker ... --obuilder-store=overlayfs:/var/cache/obuilder
27+
*)
28+
29+
open Lwt.Infix
30+
31+
type cache = {
32+
lock : Lwt_mutex.t;
33+
mutable children : int;
34+
}
35+
36+
type t = {
37+
path : string;
38+
caches : (string, cache) Hashtbl.t;
39+
mutable next : int;
40+
}
41+
42+
let ( / ) = Filename.concat
43+
44+
module Overlayfs = struct
45+
let create ?mode ?user dirs =
46+
match mode with
47+
| None -> Os.exec ([ "mkdir"; "-p" ] @ dirs)
48+
| Some mode -> Os.exec ([ "mkdir"; "-p"; "-m"; mode ] @ dirs) >>= fun () ->
49+
match user with
50+
| None -> Lwt.return_unit
51+
| Some `Unix user ->
52+
let { Obuilder_spec.uid; gid } = user in
53+
Os.sudo ([ "chown"; Printf.sprintf "%d:%d" uid gid; ] @ dirs)
54+
| Some `Windows _ -> assert false (* overlayfs not supported on Windows *)
55+
56+
let delete dirs =
57+
match dirs with
58+
| [] -> Lwt.return_unit
59+
| d -> Os.sudo ([ "rm"; "-rf" ] @ d)
60+
61+
let rename ~src ~dst =
62+
Os.sudo [ "mv"; src; dst ]
63+
64+
let overlay ~lower ~upper ~work ~merged =
65+
Os.sudo [ "mount"; "-t"; "overlay"; "overlay"; "-olowerdir=" ^ lower ^ ",upperdir=" ^ upper ^ ",workdir=" ^ work; merged; ]
66+
67+
let cp ~src ~dst = Os.sudo [ "cp"; "-plRduTf"; src; dst ]
68+
(*
69+
* -p same as --preserve=mode,ownership,timestamps
70+
* -l hard link files instead of copying
71+
* -R copy directories recursively
72+
* -d same as --no-dereference --preserve=links
73+
* -u copy only when the SOURCE file is newer than the destination file or when the destination file is missing
74+
* -T treat DEST as a normal file
75+
*)
76+
77+
let umount ~merged = Os.sudo [ "umount"; merged ]
78+
end
79+
80+
module Path = struct
81+
let state_dirname = "state"
82+
let cache_dirname = "cache"
83+
let cache_result_dirname = "cache-result"
84+
let cache_work_dirname = "cache-work"
85+
let cache_merged_dirname = "cache-merged"
86+
let result_dirname = "result"
87+
let in_progress_dirname = "in-progress"
88+
let merged_dirname = "merged"
89+
let work_dirname = "work"
90+
91+
let dirs root =
92+
List.map (( / ) root)
93+
[ state_dirname;
94+
cache_dirname;
95+
cache_result_dirname;
96+
cache_work_dirname;
97+
cache_merged_dirname;
98+
result_dirname;
99+
in_progress_dirname;
100+
merged_dirname;
101+
work_dirname; ]
102+
103+
let result t id = t.path / result_dirname / id
104+
let in_progress t id = t.path / in_progress_dirname / id
105+
let merged t id = t.path / merged_dirname / id
106+
let work t id = t.path / work_dirname / id
107+
108+
let cache t name = t.path / cache_dirname / name
109+
let cache_result t n name =
110+
( t.path / cache_result_dirname / name ^ "-" ^ Int.to_string n,
111+
t.path / cache_work_dirname / name ^ "-" ^ Int.to_string n,
112+
t.path / cache_merged_dirname / name ^ "-" ^ Int.to_string n)
113+
end
114+
115+
let root t = t.path
116+
117+
let df t =
118+
Lwt_process.pread ("", [| "df"; "-k"; "--output=used,size"; t.path |])
119+
>>= fun output ->
120+
let used, blocks =
121+
String.split_on_char '\n' output
122+
|> List.filter_map (fun s ->
123+
match Scanf.sscanf s " %Ld %Ld " (fun used blocks -> (used, blocks)) with
124+
| used, blocks -> Some (Int64.to_float used, Int64.to_float blocks)
125+
| (exception Scanf.Scan_failure _) | (exception End_of_file) -> None)
126+
|> List.fold_left (fun (used, blocks) (u, b) -> (used +. u, blocks +. b)) (0., 0.)
127+
in
128+
Lwt.return (100. -. (100. *. (used /. blocks)))
129+
130+
let create ~path =
131+
Overlayfs.create (Path.dirs path) >>= fun () ->
132+
let parse_mtab s =
133+
match Scanf.sscanf s "%s %s %s %s %s %s" (fun _ mp _ _ _ _ -> mp) with
134+
| x -> Some x
135+
| (exception Scanf.Scan_failure _) | (exception End_of_file) -> None
136+
in
137+
let mounts =
138+
Os.read_lines "/etc/mtab" parse_mtab
139+
|> List.filter_map (function
140+
| Some x ->
141+
if String.length x > String.length path
142+
&& String.starts_with ~prefix:path x
143+
then Some x
144+
else None
145+
| None -> None)
146+
in
147+
Lwt_list.iter_s
148+
(fun merged ->
149+
Log.warn (fun f -> f "Unmounting left-over folder %S" merged);
150+
Overlayfs.umount ~merged)
151+
mounts
152+
>>= fun () ->
153+
Lwt_list.iter_s
154+
(fun path ->
155+
Sys.readdir path |> Array.to_list
156+
|> List.map (Filename.concat path)
157+
|> Overlayfs.delete)
158+
[ path / Path.in_progress_dirname;
159+
path / Path.merged_dirname;
160+
path / Path.cache_result_dirname;
161+
path / Path.cache_work_dirname;
162+
path / Path.cache_merged_dirname;
163+
path / Path.work_dirname; ]
164+
>|= fun () -> { path; caches = Hashtbl.create 10; next = 0 }
165+
166+
let build t ?base ~id fn =
167+
Log.debug (fun f -> f "overlayfs: build %S" id);
168+
let result = Path.result t id in
169+
let in_progress = Path.in_progress t id in
170+
let merged = Path.merged t id in
171+
let work = Path.work t id in
172+
Overlayfs.create [ in_progress; work; merged ] >>= fun () ->
173+
let _ = Option.map (Path.in_progress t) base in
174+
(match base with
175+
| None ->
176+
Lwt.return_unit
177+
| Some src ->
178+
let src = Path.result t src in
179+
Unix.symlink src (in_progress / "parent");
180+
Unix.symlink (src / "env") (in_progress / "env");
181+
let rec ancestors src = src :: (match Os.read_link (src / "parent") with
182+
| Some p -> ancestors p
183+
| None -> [])
184+
in
185+
let lower = ancestors src |> String.concat ":" in
186+
Overlayfs.overlay ~lower ~upper:in_progress ~work ~merged)
187+
>>= fun () ->
188+
Lwt.try_bind
189+
(fun () -> match base with
190+
| None -> fn in_progress
191+
| Some _ -> fn merged)
192+
(fun r ->
193+
(match base with
194+
| None -> Lwt.return_unit
195+
| Some _ -> Overlayfs.umount ~merged)
196+
>>= fun () ->
197+
(match r with
198+
| Ok () ->
199+
Overlayfs.rename ~src:in_progress ~dst:result >>= fun () ->
200+
Overlayfs.delete [ merged; work ]
201+
| Error _ -> Overlayfs.delete [ merged; work; in_progress ])
202+
>>= fun () -> Lwt.return r)
203+
(fun ex ->
204+
Log.warn (fun f -> f "Uncaught exception from %S build function: %a" id Fmt.exn ex);
205+
Overlayfs.delete [ merged; work; in_progress ] >>= fun () -> Lwt.fail ex)
206+
207+
let delete t id =
208+
let path = Path.result t id in
209+
let results = t.path / Path.result_dirname in
210+
let rec decendants parent =
211+
Sys.readdir results
212+
|> Array.to_list
213+
|> List.map (Filename.concat results)
214+
|> List.filter (fun dir ->
215+
match Os.read_link (dir / "parent") with
216+
| Some p -> p = parent
217+
| None -> false)
218+
|> List.map decendants
219+
|> List.flatten
220+
|> List.append [ parent ]
221+
in decendants path
222+
|> Overlayfs.delete
223+
224+
let result t id =
225+
let dir = Path.result t id in
226+
match Os.check_dir dir with
227+
| `Present -> Lwt.return_some dir
228+
| `Missing -> Lwt.return_none
229+
230+
let log_file t id =
231+
result t id >|= function
232+
| Some dir -> dir / "log"
233+
| None -> Path.in_progress t id / "log"
234+
235+
let state_dir t = t.path / Path.state_dirname
236+
237+
let get_cache t name =
238+
match Hashtbl.find_opt t.caches name with
239+
| Some c -> c
240+
| None ->
241+
let c = { lock = Lwt_mutex.create (); children = 0 } in
242+
Hashtbl.add t.caches name c;
243+
c
244+
245+
let cache ~user t name =
246+
let cache = get_cache t name in
247+
Lwt_mutex.with_lock cache.lock @@ fun () ->
248+
let result, work, merged = Path.cache_result t t.next name in
249+
t.next <- t.next + 1;
250+
let master = Path.cache t name in
251+
(* Create cache if it doesn't already exist. *)
252+
(match Os.check_dir master with
253+
| `Missing -> Overlayfs.create ~mode:"1777" ~user [ master ]
254+
| `Present -> Lwt.return_unit)
255+
>>= fun () ->
256+
cache.children <- cache.children + 1;
257+
Overlayfs.create ~mode:"1777" ~user [ result; work; merged ] >>= fun () ->
258+
let lower = String.split_on_char ':' master |> String.concat "\\:" in
259+
Overlayfs.overlay ~lower ~upper:result ~work ~merged >>= fun () ->
260+
let release () =
261+
Lwt_mutex.with_lock cache.lock @@ fun () ->
262+
cache.children <- cache.children - 1;
263+
Overlayfs.umount ~merged >>= fun () ->
264+
Overlayfs.cp ~src:result ~dst:master >>= fun () ->
265+
Overlayfs.delete [ result; work; merged ]
266+
in
267+
Lwt.return (merged, release)
268+
269+
let delete_cache t name =
270+
let () = Printf.printf "0\n" in
271+
let cache = get_cache t name in
272+
let () = Printf.printf "1\n" in
273+
Lwt_mutex.with_lock cache.lock @@ fun () ->
274+
let () = Printf.printf "2\n" in
275+
(* Ensures in-progress writes will be discarded *)
276+
if cache.children > 0
277+
then Lwt_result.fail `Busy
278+
else
279+
Overlayfs.delete [ Path.cache t name ] >>= fun () ->
280+
let () = Printf.printf "3\n" in
281+
Lwt.return (Ok ())
282+
283+
let complete_deletes _t = Lwt.return_unit

lib/overlayfs_store.mli

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
(** Store build results using rsync. *)
2+
3+
include S.STORE
4+
5+
val create : path:string -> t Lwt.t
6+
(** [create ~path] creates a new xfs store where everything will
7+
be stored under [path]. *)

0 commit comments

Comments
 (0)