d60aac6c Andy McCall 23 days ago ↶
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
82cf6e91 Andy McCall 23 days ago ↶
20
2594278f Andy McCall 17 days ago ↶
21
22
23
24
25
26
27
28
29
30
31
32
33
34
077a6524 Andy McCall 16 days ago ↶
35
36
37
38
39
40
41
42
43
44
45
d60aac6c Andy McCall 23 days ago ↶
46
47
use anyhow:: { Context, Result } ;
48
use flow_core:: pipeline:: Mount;
077a6524 Andy McCall 16 days ago ↶
49
use flow_init:: prune:: Pruned;
2594278f Andy McCall 17 days ago ↶
50
use std:: os:: unix:: fs:: { MetadataExt, PermissionsExt} ;
d60aac6c Andy McCall 23 days ago ↶
51
use std:: path:: { Path, PathBuf} ;
52
use std:: time:: SystemTime;
53
54
55
pub const DEFAULT_GB : u64 = 64 ;
82cf6e91 Andy McCall 23 days ago ↶
56
57
58
59
pub type Sources = std:: collections:: BTreeMap< String, ( String, i64 ) > ;
60
61
62
pub struct Taken {
63
64
pub how : String,
65
66
pub sources : Option< Sources> ,
2594278f Andy McCall 17 days ago ↶
67
}
68
69
70
# [ derive ( Clone, Copy, Debug, PartialEq, Eq ) ]
71
pub enum Form {
72
73
Image,
74
75
Dir,
76
}
77
78
impl Form {
79
fn ext ( self ) -> & 'static str {
80
match self {
81
Form:: Image => " ext4" ,
82
Form:: Dir => " tree" ,
83
}
84
}
85
}
86
87
88
fn form_of ( p : & Path) -> Option< Form> {
89
match p.extension ( ) ? .to_str ( ) ? {
90
" ext4" => Some ( Form:: Image) ,
91
" tree" => Some ( Form:: Dir) ,
92
_ => None ,
93
}
82cf6e91 Andy McCall 23 days ago ↶
94
}
95
96
fn sources_of ( disk : & Path) -> PathBuf {
2594278f Andy McCall 17 days ago ↶
97
match form_of ( disk) {
98
Some ( Form:: Dir) => disk.with_extension ( " tree-sources" ) ,
99
_ => disk.with_extension ( " sources" ) ,
100
}
82cf6e91 Andy McCall 23 days ago ↶
101
}
102
103
fn read_sources ( disk : & Path) -> Option< Sources> {
104
serde_json:: from_slice( & std:: fs:: read( sources_of ( disk) ) .ok ( ) ? ) .ok ( )
077a6524 Andy McCall 16 days ago ↶
105
}
106
107
108
109
pub struct Prune < 'a > {
110
111
112
pub started : SystemTime,
113
114
pub percent : u8 ,
115
116
pub guest : Option< & 'a Pruned> ,
82cf6e91 Andy McCall 23 days ago ↶
117
}
d60aac6c Andy McCall 23 days ago ↶
118
119
120
pub struct Pool {
121
root : PathBuf,
122
limit : u64 ,
123
}
124
2594278f Andy McCall 17 days ago ↶
125
126
d60aac6c Andy McCall 23 days ago ↶
127
pub struct Ctx < 'a > {
128
pub repo : & 'a str ,
129
pub bookmark : & 'a str ,
130
pub job : & 'a str ,
131
pub image : & 'a str ,
2594278f Andy McCall 17 days ago ↶
132
pub form : Form,
d60aac6c Andy McCall 23 days ago ↶
133
}
134
82cf6e91 Andy McCall 23 days ago ↶
135
use flow_core:: api:: segment;
2594278f Andy McCall 17 days ago ↶
136
137
138
139
140
static CHANGING : std:: sync:: Mutex< ( ) > = std:: sync:: Mutex:: new( ( ) ) ;
d60aac6c Andy McCall 23 days ago ↶
141
2594278f Andy McCall 17 days ago ↶
142
fn changing ( ) -> std:: sync:: MutexGuard< 'static , ( ) > {
143
CHANGING .lock ( ) .unwrap_or_else ( | p | p.into_inner ( ) )
144
}
145
d60aac6c Andy McCall 23 days ago ↶
146
impl Pool {
147
pub fn new ( home_root : & Path, gb : Option< u64 > ) -> Pool {
148
Pool { root: home_root.join ( " cache" ) , limit: gb.unwrap_or ( DEFAULT_GB ) .saturating_mul ( 1 < < 30 ) }
149
}
150
151
fn cache_dir ( & self , ctx : & Ctx, name : & str ) -> PathBuf {
152
self .root.join ( segment ( ctx.repo) ) .join ( segment ( ctx.job) ) .join ( segment ( name) )
153
}
154
155
156
157
pub fn key ( ctx : & Ctx, m : & Mount, hash_of : impl Fn( &str ) -> Option< String> ) -> String {
158
let mut h = blake3:: Hasher:: new( ) ;
159
for part in [ ctx.image, ctx.job, m.cache.as_deref ( ) .unwrap_or ( " " ) ] {
160
h.update ( part.as_bytes ( ) ) ;
161
h.update ( b " \n " ) ;
162
}
163
for k in & m.key {
164
h.update ( k.as_bytes ( ) ) ;
165
h.update ( b " =" ) ;
166
h.update ( hash_of ( k) .unwrap_or_else ( | | " absent" .into ( ) ) .as_bytes ( ) ) ;
167
h.update ( b " \n " ) ;
168
}
169
h.finalize ( ) .to_hex ( ) [ .. 32 ] .to_string ( )
170
}
171
2594278f Andy McCall 17 days ago ↶
172
173
174
175
176
177
82cf6e91 Andy McCall 23 days ago ↶
178
pub fn take ( & self , ctx : & Ctx, m : & Mount, key : & str , dest : & Path) -> Option< Taken> {
d60aac6c Andy McCall 23 days ago ↶
179
let name = m.cache.as_deref ( ) ? ;
2594278f Andy McCall 17 days ago ↶
180
let _changing = changing ( ) ;
d60aac6c Andy McCall 23 days ago ↶
181
let dir = self .cache_dir ( ctx, name) ;
182
let size = m.size_mib as u64 * ( 1 < < 20 ) ;
2594278f Andy McCall 17 days ago ↶
183
let form = ctx.form;
184
let fits = | p : & Path| match form {
185
Form:: Image => std:: fs:: metadata( p) .is_ok_and ( | md | md.len ( ) = = size ) ,
186
Form:: Dir => std:: fs:: symlink_metadata( p) .is_ok_and ( | md | md.is_dir ( ) ) ,
187
} ;
188
let ext = form.ext ( ) ;
d60aac6c Andy McCall 23 days ago ↶
189
let mine = dir.join ( segment ( ctx.bookmark) ) ;
2594278f Andy McCall 17 days ago ↶
190
let exact = mine.join ( format! ( " {key} .{ext} " ) ) ;
82cf6e91 Andy McCall 23 days ago ↶
191
192
if fits ( & exact) {
193
let sources = read_sources ( & exact) ;
194
if std:: fs:: rename( & exact, dest) .is_ok ( ) {
195
let _ = std:: fs:: remove_file( sources_of ( & exact) ) ;
196
return Some ( Taken { how: format! ( " cache {name} : this bookmark's, same key" ) , sources } ) ;
197
}
d60aac6c Andy McCall 23 days ago ↶
198
}
2594278f Andy McCall 17 days ago ↶
199
if let Some ( p) = newest ( & mine, form, | p | fits ( p) ) {
82cf6e91 Andy McCall 23 days ago ↶
200
let sources = read_sources ( & p) ;
d60aac6c Andy McCall 23 days ago ↶
201
if std:: fs:: rename( & p, dest) .is_ok ( ) {
82cf6e91 Andy McCall 23 days ago ↶
202
let _ = std:: fs:: remove_file( sources_of ( & p) ) ;
203
return Some ( Taken { how: format! ( " cache {name} : this bookmark's, an older key" ) , sources } ) ;
d60aac6c Andy McCall 23 days ago ↶
204
}
205
}
206
let others: Vec< PathBuf> =
207
std:: fs:: read_dir( & dir) .into_iter ( ) .flatten ( ) .flatten ( ) .map ( | e | e.path ( ) ) .filter ( | p | p.is_dir ( ) & & * p ! = mine ) .collect ( ) ;
208
let pick = others
209
.iter ( )
2594278f Andy McCall 17 days ago ↶
210
.map ( | d | d.join ( format! ( " {key} .{ext} " ) ) )
d60aac6c Andy McCall 23 days ago ↶
211
.filter ( | p | fits ( p) )
212
.max_by_key ( | p | modified ( p) )
213
.map ( | p | ( p, " same key" ) )
2594278f Andy McCall 17 days ago ↶
214
.or_else ( | | {
215
others.iter ( ) .filter_map ( | d | newest ( d, form, | p | fits ( p) ) ) .max_by_key ( | p | modified ( p) ) .map ( | p | ( p, " an older key" ) )
216
} ) ;
d60aac6c Andy McCall 23 days ago ↶
217
if let Some ( ( p, which) ) = pick {
218
let from = p.parent ( ) .and_then ( | d | d.file_name ( ) ) .map ( | n | n.to_string_lossy ( ) .into_owned ( ) ) .unwrap_or_default ( ) ;
2594278f Andy McCall 17 days ago ↶
219
if copy ( form, & p, dest) .is_ok ( ) {
82cf6e91 Andy McCall 23 days ago ↶
220
return Some ( Taken {
221
how: format! ( " cache {name} : copied from another bookmark's ({from} ), {which} " ) ,
222
sources: read_sources ( & p) ,
223
} ) ;
d60aac6c Andy McCall 23 days ago ↶
224
}
225
}
226
None
227
}
228
229
230
82cf6e91 Andy McCall 23 days ago ↶
231
pub fn keep ( & self , ctx : & Ctx, m : & Mount, key : & str , disk : & Path, sources : & Sources) -> Result< ( ) > {
d60aac6c Andy McCall 23 days ago ↶
232
let Some ( name) = m.cache.as_deref ( ) else { return Ok ( ( ) ) } ;
2594278f Andy McCall 17 days ago ↶
233
let _changing = changing ( ) ;
234
235
236
if ctx.form = = Form:: Dir {
237
seal ( disk) .with_context ( | | format! ( " seal {} " , disk.display ( ) ) ) ? ;
238
}
d60aac6c Andy McCall 23 days ago ↶
239
let mine = self .cache_dir ( ctx, name) .join ( segment ( ctx.bookmark) ) ;
2594278f Andy McCall 17 days ago ↶
240
std:: os:: unix:: fs:: DirBuilderExt:: mode( std:: fs:: DirBuilder:: new( ) .recursive ( true ) , 0o700 ) .create ( & mine) ? ;
241
std:: fs:: set_permissions( & self .root, std:: fs:: Permissions:: from_mode( 0o700 ) ) ? ;
242
let dest = mine.join ( format! ( " {key} .{} " , ctx.form.ext ( ) ) ) ;
243
if ctx.form = = Form:: Dir {
244
245
crate :: workspace:: remove_tree( & dest) ? ;
246
}
d60aac6c Andy McCall 23 days ago ↶
247
if std:: fs:: rename( disk, & dest) .is_err ( ) {
2594278f Andy McCall 17 days ago ↶
248
copy ( ctx.form, disk, & dest) .with_context ( | | format! ( " keep {} " , disk.display ( ) ) ) ? ;
d60aac6c Andy McCall 23 days ago ↶
249
}
250
2594278f Andy McCall 17 days ago ↶
251
std:: fs:: File:: open( & dest) ? .set_modified ( SystemTime:: now( ) ) ? ;
252
let sidecar = sources_of ( & dest) ;
253
let tmp = sidecar.with_extension ( " sources-part" ) ;
254
let _ = std:: fs:: remove_file( & tmp) ;
255
{
256
use std:: io:: Write;
257
use std:: os:: unix:: fs:: OpenOptionsExt;
258
std:: fs:: OpenOptions:: new( ) .write ( true ) .create_new ( true ) .mode ( 0o600 ) .open ( & tmp) ? .write_all ( & serde_json:: to_vec( sources) ? ) ? ;
259
}
260
std:: fs:: rename( & tmp, sidecar) ? ;
d60aac6c Andy McCall 23 days ago ↶
261
262
263
self .evict ( ) ;
264
Ok ( ( ) )
2594278f Andy McCall 17 days ago ↶
265
}
266
077a6524 Andy McCall 16 days ago ↶
267
268
269
270
271
# [ allow ( clippy::too_many_arguments ) ]
272
pub fn settle (
273
& self ,
274
ctx : & Ctx,
275
m : & Mount,
276
key : & str ,
277
disk : & Path,
278
sources : & Sources,
279
passed : bool ,
280
prune : & Prune,
281
) -> Result< Option< String> > {
2594278f Andy McCall 17 days ago ↶
282
if passed {
077a6524 Andy McCall 16 days ago ↶
283
let Some ( name) = m.cache.as_deref ( ) else { return Ok ( None ) } ;
284
let pruned = pruned ( ctx.form, m, disk, prune) ? ;
285
let line = describe ( name, & pruned) ;
286
if pruned.fits ( ) {
287
self .keep ( ctx, m, key, disk, sources) ? ;
288
} else {
289
discard ( ctx.form, disk) ? ;
290
}
291
return Ok ( Some ( line) ) ;
2594278f Andy McCall 17 days ago ↶
292
}
077a6524 Andy McCall 16 days ago ↶
293
discard ( ctx.form, disk) ? ;
294
Ok ( None )
82cf6e91 Andy McCall 23 days ago ↶
295
}
296
297
298
1e6b9fb1 Andy McCall 22 days ago ↶
299
300
2594278f Andy McCall 17 days ago ↶
301
302
1e6b9fb1 Andy McCall 22 days ago ↶
303
pub fn slot_times ( & self ) -> std:: collections:: BTreeMap< String, u64 > {
304
let mut out = std:: collections:: BTreeMap:: new( ) ;
82cf6e91 Andy McCall 23 days ago ↶
305
for repo in std:: fs:: read_dir( & self .root) .into_iter ( ) .flatten ( ) .flatten ( ) {
306
for job in std:: fs:: read_dir( repo.path ( ) ) .into_iter ( ) .flatten ( ) .flatten ( ) {
1e6b9fb1 Andy McCall 22 days ago ↶
307
let mut newest: Option< SystemTime> = None ;
2594278f Andy McCall 17 days ago ↶
308
309
kept ( & job.path ( ) , 2 , & mut | p, _ | {
310
let t = modified ( p) ;
311
newest = Some ( newest.map_or ( t, | n | n.max ( t) ) ) ;
1e6b9fb1 Andy McCall 22 days ago ↶
312
} ) ;
313
if let Some ( t) = newest {
314
let ms = t.duration_since ( SystemTime:: UNIX_EPOCH ) .map ( | d | d.as_millis ( ) as u64 ) .unwrap_or ( 0 ) ;
315
out.insert ( format! ( " {} /{} " , repo.file_name ( ) .to_string_lossy ( ) , job.file_name ( ) .to_string_lossy ( ) ) , ms) ;
82cf6e91 Andy McCall 23 days ago ↶
316
}
317
}
318
}
319
out
d60aac6c Andy McCall 23 days ago ↶
320
}
321
2594278f Andy McCall 17 days ago ↶
322
323
324
325
fn evict ( & self ) -> u64 {
d60aac6c Andy McCall 23 days ago ↶
326
let mut disks: Vec< ( SystemTime, u64 , PathBuf) > = Vec :: new( ) ;
2594278f Andy McCall 17 days ago ↶
327
328
kept ( & self .root, 4 , & mut | p, form| {
329
if let Ok ( md) = std:: fs:: symlink_metadata( p) {
330
let bytes = match form {
331
Form:: Image => md.blocks ( ) * 512 ,
332
Form:: Dir => tree_bytes ( p) ,
333
} ;
334
disks.push ( ( md.modified ( ) .unwrap_or ( SystemTime:: UNIX_EPOCH ) , bytes, p.to_path_buf ( ) ) ) ;
d60aac6c Andy McCall 23 days ago ↶
335
}
336
} ) ;
337
let mut total: u64 = disks.iter ( ) .map ( | d | d.1 ) .sum ( ) ;
338
disks.sort ( ) ;
339
let mut freed = 0 ;
340
for ( _ , bytes, p) in disks {
341
if total < = self .limit {
342
break ;
343
}
2594278f Andy McCall 17 days ago ↶
344
let gone = match form_of ( & p) {
345
Some ( Form:: Dir) => crate :: workspace:: remove_tree( & p) ,
346
_ => std:: fs:: remove_file( & p) ,
347
} ;
348
if gone.is_ok ( ) {
82cf6e91 Andy McCall 23 days ago ↶
349
let _ = std:: fs:: remove_file( sources_of ( & p) ) ;
d60aac6c Andy McCall 23 days ago ↶
350
total - = bytes;
351
freed + = bytes;
352
}
353
}
354
freed
077a6524 Andy McCall 16 days ago ↶
355
}
356
}
357
358
359
fn discard ( form : Form, disk : & Path) -> Result< ( ) > {
360
let gone = match form {
361
Form:: Dir => crate :: workspace:: remove_tree( disk) ,
362
Form:: Image => std:: fs:: remove_file( disk) .or_else ( | e | if e.kind ( ) = = std:: io:: ErrorKind:: NotFound { Ok ( ( ) ) } else { Err ( e) } ) ,
363
} ;
364
gone.with_context ( | | format! ( " discard {} " , disk.display ( ) ) )
365
}
366
367
368
369
370
371
372
pub fn forget_reads ( form : Form, disk : & Path) -> Result< ( ) > {
373
if form = = Form:: Dir & & disk.is_dir ( ) {
374
flow_init:: prune:: forget_reads( disk) .with_context ( | | format! ( " set the atimes in {} " , disk.display ( ) ) ) ? ;
375
}
376
Ok ( ( ) )
377
}
378
379
380
381
fn pruned ( form : Form, m : & Mount, disk : & Path, prune : & Prune) -> Result< Pruned> {
382
let size = m.size_mib as u64 * ( 1 < < 20 ) ;
383
let percent = prune.percent;
384
let unpruned = | before : u64 , why : String| Pruned { size, percent, before, after: before, unpruned: Some ( why) , .. Pruned:: default( ) } ;
385
Ok ( match form {
386
Form:: Dir => {
387
let before = tree_bytes ( disk) ;
388
if ! flow_init:: prune:: records_reads( disk) {
389
unpruned ( before, " this runner's file system records no reads (noatime), so what the job used cannot be told" .into ( ) )
390
} else {
391
flow_init:: prune:: prune( disk, prune.started, & [ ] , size, before, percent)
392
.unwrap_or_else ( | e | unpruned ( tree_bytes ( disk) , format! ( " pruning failed: {e} " ) ) )
393
}
394
}
395
Form:: Image => match prune.guest.filter ( | g | g.size > 0 ) {
396
Some ( g) => g.clone ( ) ,
397
None => {
398
let ( before, size) = image_usage ( disk) ? ;
399
let why = prune.guest.and_then ( | g | g.unpruned.clone ( ) ) .unwrap_or_else ( | | " the guest did not prune it" .into ( ) ) ;
400
Pruned { size, .. unpruned ( before, why) }
401
}
402
} ,
403
} )
404
}
405
406
407
fn image_usage ( image : & Path) -> Result< ( u64 , u64 ) > {
408
let out = std:: process:: Command:: new( " dumpe2fs" ) .arg ( " -h" ) .arg ( image) .output ( ) .context ( " run dumpe2fs (e2fsprogs)" ) ? ;
409
if ! out.status.success ( ) {
410
anyhow:: bail! ( " dumpe2fs {}: {}" , image.display ( ) , String :: from_utf8_lossy( & out.stderr) .trim ( ) ) ;
411
}
412
superblock_usage ( & String :: from_utf8_lossy( & out.stdout) ) .with_context ( | | format! ( " dumpe2fs {} : no block counts" , image.display ( ) ) )
413
}
414
415
fn superblock_usage ( header : & str ) -> Option< ( u64 , u64 ) > {
416
let field = | name : & str | -> Option< u64 > {
417
header.lines ( ) .find_map ( | l | l.strip_prefix ( name) ? .strip_prefix ( ' :' ) ) .and_then ( | v | v.trim ( ) .parse ( ) .ok ( ) )
418
} ;
419
let ( count, free, block) = ( field ( " Block count" ) ? , field ( " Free blocks" ) ? , field ( " Block size" ) ? ) ;
420
Some ( ( count.saturating_sub ( free) * block, count * block) )
421
}
422
423
424
425
pub fn describe ( name : & str , p : & Pruned) -> String {
426
use crate :: images:: human;
427
let cap = format! ( " {} % of its {} disk" , p.percent, human ( p.size) ) ;
428
let pruned = format! ( " {} in {} file{} this run did not use" , human ( p.freed) , p.files, if p.files = = 1 { " " } else { " s" } ) ;
429
if p.fits ( ) & & p.files > 0 {
430
format! ( " cache {name} : kept {} of {} (pruned {pruned} ; the cap is {cap} )" , human ( p.after) , human ( p.before) )
431
} else if p.fits ( ) {
432
format! ( " cache {name} : kept {} , under its cap of {cap} " , human ( p.after) )
433
} else {
434
let after = if p.files > 0 { format! ( " {} after pruning {pruned} " , human ( p.after) ) } else { human ( p.after) } ;
435
let why = p.unpruned.as_ref ( ) .map ( | u | format! ( " ({u} )" ) ) .unwrap_or_default ( ) ;
436
format! (
437
" cache {name} : not kept: {after} is over its cap of {} ({cap} ){why} ; the next run starts empty, and a larger `size` on the mount gives the cache room" ,
438
human ( p.cap ( ) )
439
)
d60aac6c Andy McCall 23 days ago ↶
440
}
441
}
442
2594278f Andy McCall 17 days ago ↶
443
444
445
446
pub fn disk_path ( form : Form, scratch : & Path, n : usize ) -> PathBuf {
447
match form {
448
Form:: Image => scratch.join ( format! ( " mount-{n} .ext4" ) ) ,
449
Form:: Dir => crate :: syscall:: disk_dir( scratch, n) ,
450
}
451
}
452
d60aac6c Andy McCall 23 days ago ↶
453
fn modified ( p : & Path) -> SystemTime {
2594278f Andy McCall 17 days ago ↶
454
std:: fs:: symlink_metadata( p) .and_then ( | m | m.modified ( ) ) .unwrap_or ( SystemTime:: UNIX_EPOCH )
d60aac6c Andy McCall 23 days ago ↶
455
}
456
2594278f Andy McCall 17 days ago ↶
457
fn newest ( dir : & Path, form : Form, ok : impl Fn( &Path ) -> bool ) -> Option< PathBuf> {
458
std:: fs:: read_dir( dir) .ok ( ) ? .flatten ( ) .map ( | e | e.path ( ) ) .filter ( | p | form_of ( p) = = Some ( form) & & ok ( p) ) .max_by_key ( | p | modified ( p) )
d60aac6c Andy McCall 23 days ago ↶
459
}
460
2594278f Andy McCall 17 days ago ↶
461
462
463
fn kept ( dir : & Path, depth : usize , f : & mut dyn FnMut( &Path , Form ) ) {
d60aac6c Andy McCall 23 days ago ↶
464
for e in std:: fs:: read_dir( dir) .into_iter ( ) .flatten ( ) .flatten ( ) {
465
let p = e.path ( ) ;
2594278f Andy McCall 17 days ago ↶
466
let Ok ( t) = e.file_type ( ) else { continue } ;
467
if depth = = 0 {
468
match form_of ( & p) {
469
Some ( Form:: Image) if t.is_file ( ) => f ( & p, Form:: Image) ,
470
Some ( Form:: Dir) if t.is_dir ( ) => f ( & p, Form:: Dir) ,
471
_ => { }
472
}
473
} else if t.is_dir ( ) {
474
kept ( & p, depth - 1 , f) ;
475
}
476
}
477
}
478
479
480
481
fn tree_bytes ( dir : & Path) -> u64 {
482
fn walk ( p : & Path, seen : & mut std:: collections:: HashSet< ( u64 , u64 ) > ) -> u64 {
483
let Ok ( md) = std:: fs:: symlink_metadata( p) else { return 0 } ;
484
if md.nlink ( ) > 1 & & ! md.is_dir ( ) & & ! seen.insert ( ( md.dev ( ) , md.ino ( ) ) ) {
485
return 0 ;
486
}
487
let mut bytes = md.blocks ( ) * 512 ;
488
if md.is_dir ( ) {
489
for e in std:: fs:: read_dir( p) .into_iter ( ) .flatten ( ) .flatten ( ) {
490
bytes + = walk ( & e.path ( ) , seen) ;
491
}
492
}
493
bytes
494
}
495
walk ( dir, & mut Default :: default( ) )
496
}
497
498
499
500
501
502
503
fn seal ( p : & Path) -> std:: io:: Result< ( ) > {
504
let md = std:: fs:: symlink_metadata( p) ? ;
505
if md.file_type ( ) .is_symlink ( ) {
506
return Ok ( ( ) ) ;
507
}
508
let mode = md.mode ( ) & 0o7777 ;
509
let want = ( mode & ! 0o7022 ) | if md.is_dir ( ) { 0o700 } else { 0o400 } ;
510
if want ! = mode {
511
std:: fs:: set_permissions( p, std:: fs:: Permissions:: from_mode( want) ) ? ;
512
}
513
if md.is_dir ( ) {
514
for e in std:: fs:: read_dir( p) ? {
515
seal ( & e? .path ( ) ) ? ;
d60aac6c Andy McCall 23 days ago ↶
516
}
2594278f Andy McCall 17 days ago ↶
517
}
518
Ok ( ( ) )
519
}
520
521
fn copy ( form : Form, from : & Path, to : & Path) -> Result< ( ) > {
522
match form {
523
Form:: Image => sparse_copy ( from, to) ,
524
Form:: Dir => tree_copy ( from, to) ,
d60aac6c Andy McCall 23 days ago ↶
525
}
526
}
527
528
529
fn sparse_copy ( from : & Path, to : & Path) -> Result< ( ) > {
530
let tmp = to.with_extension ( " part" ) ;
531
let st = std:: process:: Command:: new( " cp" ) .arg ( " --sparse=always" ) .arg ( from) .arg ( & tmp) .status ( ) .context ( " run cp" ) ? ;
532
if ! st.success ( ) {
533
let _ = std:: fs:: remove_file( & tmp) ;
534
anyhow:: bail! ( " cp {} failed" , from.display ( ) ) ;
535
}
536
std:: fs:: rename( & tmp, to) ? ;
537
Ok ( ( ) )
538
}
539
2594278f Andy McCall 17 days ago ↶
540
541
542
fn tree_copy ( from : & Path, to : & Path) -> Result< ( ) > {
543
let tmp = to.with_extension ( " tree-part" ) ;
544
crate :: workspace:: remove_tree( & tmp) ? ;
545
let st = std:: process:: Command:: new( " cp" )
546
.args ( [ " -a" , " --reflink=auto" , " --no-target-directory" ] )
547
.arg ( from)
548
.arg ( & tmp)
549
.status ( )
550
.context ( " run cp" ) ? ;
551
if ! st.success ( ) {
552
let _ = crate :: workspace:: remove_tree( & tmp) ;
553
anyhow:: bail! ( " cp {} failed" , from.display ( ) ) ;
554
}
555
std:: fs:: rename( & tmp, to) .inspect_err ( | _| {
556
let _ = crate :: workspace:: remove_tree( & tmp) ;
557
} ) ? ;
558
Ok ( ( ) )
559
}
560
d60aac6c Andy McCall 23 days ago ↶
561
# [ cfg ( test ) ]
562
mod tests {
563
use super :: * ;
564
use flow_core:: pipeline:: MountSource;
565
077a6524 Andy McCall 16 days ago ↶
566
567
fn keep_all ( ) -> Prune< 'static > {
568
Prune { started: SystemTime:: now( ) , percent: 100 , guest: None }
569
}
570
d60aac6c Andy McCall 23 days ago ↶
571
fn mount ( key : & [& str ]) -> Mount {
572
Mount {
573
source: MountSource:: Disk,
574
target: " /src/target" .into ( ) ,
575
size_mib: 1 ,
576
cache: Some ( " target" .into ( ) ) ,
577
key: key.iter ( ) .map ( | s | s.to_string ( ) ) .collect ( ) ,
578
}
579
}
580
581
fn disk ( path : & Path, fill : & [u8 ]) {
582
let f = std:: fs:: File:: create( path) .unwrap ( ) ;
583
f.set_len ( 1 < < 20 ) .unwrap ( ) ;
584
std:: os:: unix:: fs:: FileExt:: write_at( & f, fill, 0 ) .unwrap ( ) ;
585
}
586
587
fn first ( path : & Path) -> Vec< u8 > {
588
std:: fs:: read( path) .unwrap ( ) [ .. 4 ] .to_vec ( )
589
}
590
591
# [ test ]
592
fn a_kept_disk_starts_the_next_job_of_its_bookmark_then_others_copy_it ( ) {
593
let home = tempfile:: tempdir( ) .unwrap ( ) ;
594
let work = tempfile:: tempdir( ) .unwrap ( ) ;
595
let pool = Pool:: new( home.path ( ) , None ) ;
596
let m = mount ( & [ " Cargo.lock" ] ) ;
2594278f Andy McCall 17 days ago ↶
597
let main = Ctx { repo: " andy/hub" , bookmark: " main" , job: " test" , image: " img" , form: Form:: Image } ;
d60aac6c Andy McCall 23 days ago ↶
598
let key = Pool:: key( & main, & m, | _| Some ( " lock-1" .into ( ) ) ) ;
599
82cf6e91 Andy McCall 23 days ago ↶
600
assert! ( pool.take ( & main, & m, & key, & work.path ( ) .join ( " a.ext4" ) ) .is_none ( ) ) ;
d60aac6c Andy McCall 23 days ago ↶
601
602
disk ( & work.path ( ) .join ( " a.ext4" ) , b " MAIN" ) ;
82cf6e91 Andy McCall 23 days ago ↶
603
let built: Sources = [ ( " src/lib.rs" .to_string ( ) , ( " h1" .to_string ( ) , 1_000 ) ) ] .into ( ) ;
604
pool.keep ( & main, & m, & key, & work.path ( ) .join ( " a.ext4" ) , & built) .unwrap ( ) ;
d60aac6c Andy McCall 23 days ago ↶
605
assert! ( ! work.path ( ) .join ( " a.ext4" ) .exists ( ) , " moved, not copied" ) ;
82cf6e91 Andy McCall 23 days ago ↶
606
let taken = pool.take ( & main, & m, & key, & work.path ( ) .join ( " b.ext4" ) ) .unwrap ( ) ;
607
assert! ( taken.how.contains ( " same key" ) , " {}" , taken.how) ;
608
assert_eq! ( taken.sources.as_ref ( ) , Some ( & built) , " its sources come with it (#52)" ) ;
d60aac6c Andy McCall 23 days ago ↶
609
assert_eq! ( first ( & work.path ( ) .join ( " b.ext4" ) ) , b " MAIN" ) ;
82cf6e91 Andy McCall 23 days ago ↶
610
pool.keep ( & main, & m, & key, & work.path ( ) .join ( " b.ext4" ) , & built) .unwrap ( ) ;
d60aac6c Andy McCall 23 days ago ↶
611
612
let key2 = Pool:: key( & main, & m, | _| Some ( " lock-2" .into ( ) ) ) ;
613
assert_ne! ( key, key2) ;
82cf6e91 Andy McCall 23 days ago ↶
614
let taken = pool.take ( & main, & m, & key2, & work.path ( ) .join ( " c.ext4" ) ) .unwrap ( ) ;
615
assert! ( taken.how.contains ( " older key" ) , " {}" , taken.how) ;
616
assert_eq! ( taken.sources.as_ref ( ) , Some ( & built) ) ;
617
pool.keep ( & main, & m, & key2, & work.path ( ) .join ( " c.ext4" ) , & built) .unwrap ( ) ;
1e6b9fb1 Andy McCall 22 days ago ↶
618
assert_eq! (
619
pool.slot_times ( ) .into_keys ( ) .collect:: < Vec< _ > > ( ) ,
620
vec! [ flow_core:: api:: cache_slot( " andy/hub" , " test" ) ] ,
621
" what the claim reports (#51)"
622
) ;
623
624
let now = SystemTime:: now( ) .duration_since ( SystemTime:: UNIX_EPOCH ) .unwrap ( ) .as_millis ( ) as u64 ;
625
let kept = pool.slot_times ( ) [ & flow_core:: api:: cache_slot( " andy/hub" , " test" ) ] ;
626
assert! ( now.abs_diff ( kept) < 60_000 , " kept {kept}, now {now}" ) ;
d60aac6c Andy McCall 23 days ago ↶
627
628
let feature = Ctx { bookmark: " feature" , .. main } ;
82cf6e91 Andy McCall 23 days ago ↶
629
let taken = pool.take ( & feature, & m, & key2, & work.path ( ) .join ( " d.ext4" ) ) .unwrap ( ) ;
630
assert! ( taken.how.contains ( " copied from another bookmark" ) , " {}" , taken.how) ;
631
assert_eq! ( taken.sources.as_ref ( ) , Some ( & built) , " a copy reads them and leaves them" ) ;
d60aac6c Andy McCall 23 days ago ↶
632
assert_eq! ( first ( & work.path ( ) .join ( " d.ext4" ) ) , b " MAIN" ) ;
82cf6e91 Andy McCall 23 days ago ↶
633
let taken = pool.take ( & main, & m, & key2, & work.path ( ) .join ( " e.ext4" ) ) .unwrap ( ) ;
634
assert! ( taken.how.contains ( " this bookmark's" ) & & taken.sources.is_some ( ) , " main's is still there, with its sources" ) ;
d60aac6c Andy McCall 23 days ago ↶
635
636
let other_job = Ctx { job: " build" , .. main } ;
82cf6e91 Andy McCall 23 days ago ↶
637
assert! ( pool.take ( & other_job, & m, & key2, & work.path ( ) .join ( " f.ext4" ) ) .is_none ( ) ) ;
d60aac6c Andy McCall 23 days ago ↶
638
let mut bigger = mount ( & [ " Cargo.lock" ] ) ;
639
bigger.size_mib = 2 ;
82cf6e91 Andy McCall 23 days ago ↶
640
assert! ( pool.take ( & feature, & bigger, & key2, & work.path ( ) .join ( " g.ext4" ) ) .is_none ( ) ) ;
d60aac6c Andy McCall 23 days ago ↶
641
}
642
643
# [ test ]
644
fn the_key_follows_the_image_the_job_and_the_key_files ( ) {
2594278f Andy McCall 17 days ago ↶
645
let ctx = Ctx { repo: " r" , bookmark: " main" , job: " test" , image: " img-1" , form: Form:: Image } ;
d60aac6c Andy McCall 23 days ago ↶
646
let m = mount ( & [ " Cargo.lock" , " rust-toolchain.toml" ] ) ;
647
let k = Pool:: key( & ctx, & m, | p | Some ( format! ( " {p} -v1" ) ) ) ;
648
assert_eq! ( k, Pool:: key( & ctx, & m, | p | Some ( format! ( " {p} -v1" ) ) ) ) ;
649
assert_ne! ( k, Pool:: key( & ctx, & m, | p | Some ( format! ( " {p} -v2" ) ) ) , " a key file changed" ) ;
650
assert_ne! ( k, Pool:: key( & Ctx { image: " img-2" , .. ctx } , & m, | p | Some ( format! ( " {p} -v1" ) ) ) , " another image" ) ;
651
assert_ne! ( k, Pool:: key( & ctx, & m, | _| None ) , " a key file missing" ) ;
652
}
653
654
# [ test ]
655
fn the_pool_drops_the_disks_used_longest_ago ( ) {
656
let home = tempfile:: tempdir( ) .unwrap ( ) ;
657
let work = tempfile:: tempdir( ) .unwrap ( ) ;
658
let pool = Pool { root: home.path ( ) .join ( " cache" ) , limit: 0 } ;
659
let m = mount ( & [ ] ) ;
2594278f Andy McCall 17 days ago ↶
660
let ctx = Ctx { repo: " r" , bookmark: " main" , job: " j" , image: " i" , form: Form:: Image } ;
d60aac6c Andy McCall 23 days ago ↶
661
let full = | p : & Path| {
662
let f = std:: fs:: File:: create( p) .unwrap ( ) ;
663
std:: os:: unix:: fs:: FileExt:: write_at( & f, & vec! [ 7 u8 ; 1 < < 20 ] , 0 ) .unwrap ( ) ;
664
} ;
665
full ( & work.path ( ) .join ( " a.ext4" ) ) ;
666
82cf6e91 Andy McCall 23 days ago ↶
667
pool.keep ( & ctx, & m, " k1" , & work.path ( ) .join ( " a.ext4" ) , & Sources:: new( ) ) .unwrap ( ) ;
668
assert! ( pool.take ( & ctx, & m, " k1" , & work.path ( ) .join ( " b.ext4" ) ) .is_none ( ) ) ;
d60aac6c Andy McCall 23 days ago ↶
669
let roomy = Pool { root: home.path ( ) .join ( " cache" ) , limit: 1 < < 30 } ;
670
full ( & work.path ( ) .join ( " c.ext4" ) ) ;
82cf6e91 Andy McCall 23 days ago ↶
671
roomy.keep ( & ctx, & m, " k2" , & work.path ( ) .join ( " c.ext4" ) , & Sources:: new( ) ) .unwrap ( ) ;
d60aac6c Andy McCall 23 days ago ↶
672
assert! ( roomy.take ( & ctx, & m, " k2" , & work.path ( ) .join ( " d.ext4" ) ) .is_some ( ) , " under the limit it stays" ) ;
2594278f Andy McCall 17 days ago ↶
673
}
674
675
676
677
fn tree ( path : & Path, marker : & [u8 ], kib : usize ) {
678
std:: fs:: create_dir_all( path.join ( " debug" ) ) .unwrap ( ) ;
679
std:: fs:: write( path.join ( " debug/marker" ) , marker) .unwrap ( ) ;
680
681
let bytes: Vec< u8 > = ( 0 .. kib < < 10 ) .map ( | i : usize | ( i.wrapping_mul ( 2_654_435_761 ) > > 13 ) as u8 ) .collect ( ) ;
682
std:: fs:: write( path.join ( " debug/blob" ) , bytes) .unwrap ( ) ;
683
}
684
685
fn dir_ctx < 'a > ( bookmark : & 'a str ) -> Ctx< 'a > {
686
Ctx { repo: " andy/flow" , bookmark, job: " test" , image: " img" , form: Form:: Dir }
687
}
688
689
690
691
# [ test ]
692
fn a_kept_directory_starts_the_next_syscall_job_and_others_copy_it ( ) {
693
let home = tempfile:: tempdir( ) .unwrap ( ) ;
694
let work = tempfile:: tempdir( ) .unwrap ( ) ;
695
let pool = Pool:: new( home.path ( ) , None ) ;
696
let m = mount ( & [ " Cargo.lock" ] ) ;
697
let main = dir_ctx ( " main" ) ;
698
let key = Pool:: key( & main, & m, | _| Some ( " lock-1" .into ( ) ) ) ;
699
let w = | n : & str | work.path ( ) .join ( n) ;
700
assert! ( pool.take ( & main, & m, & key, & w ( " a" ) ) .is_none ( ) , " nothing kept yet" ) ;
701
702
tree ( & w ( " a" ) , b " FIRST" , 4 ) ;
703
704
std:: fs:: write( w ( " a/debug/inner.ext4" ) , b " not a disk" ) .unwrap ( ) ;
705
let old = SystemTime:: UNIX_EPOCH + std:: time:: Duration:: from_secs( 1_000_000 ) ;
706
std:: fs:: File:: open( w ( " a/debug/marker" ) ) .unwrap ( ) .set_modified ( old) .unwrap ( ) ;
707
let built: Sources = [ ( " src/lib.rs" .to_string ( ) , ( " h1" .to_string ( ) , 1_000 ) ) ] .into ( ) ;
077a6524 Andy McCall 16 days ago ↶
708
pool.settle ( & main, & m, & key, & w ( " a" ) , & built, true , & keep_all ( ) ) .unwrap ( ) ;
2594278f Andy McCall 17 days ago ↶
709
assert! ( ! w ( " a" ) .exists ( ) , " moved, not copied" ) ;
710
assert_eq! (
711
pool.slot_times ( ) .into_keys ( ) .collect:: < Vec< _ > > ( ) ,
712
vec! [ flow_core:: api:: cache_slot( " andy/flow" , " test" ) ] ,
713
" the claim reports it like an image (#51)"
714
) ;
715
716
717
let taken = pool.take ( & main, & m, & key, & w ( " b" ) ) .unwrap ( ) ;
718
assert! ( taken.how.contains ( " this bookmark's, same key" ) , " {}" , taken.how) ;
719
assert_eq! ( taken.sources.as_ref ( ) , Some ( & built) , " its sources come with it (#52)" ) ;
720
assert_eq! ( std:: fs:: read( w ( " b/debug/marker" ) ) .unwrap ( ) , b " FIRST" ) ;
721
assert_eq! ( modified ( & w ( " b/debug/marker" ) ) , old, " its files keep their times" ) ;
722
assert_eq! ( std:: fs:: read( w ( " b/debug/inner.ext4" ) ) .unwrap ( ) , b " not a disk" ) ;
723
assert! ( pool.take ( & main, & m, & key, & w ( " c" ) ) .is_none ( ) , " taken once" ) ;
724
assert! ( pool.slot_times ( ) .is_empty ( ) ) ;
725
726
std:: fs:: write( w ( " b/debug/marker" ) , b " SECOND" ) .unwrap ( ) ;
077a6524 Andy McCall 16 days ago ↶
727
pool.settle ( & main, & m, & key, & w ( " b" ) , & built, true , & keep_all ( ) ) .unwrap ( ) ;
2594278f Andy McCall 17 days ago ↶
728
let key2 = Pool:: key( & main, & m, | _| Some ( " lock-2" .into ( ) ) ) ;
729
let taken = pool.take ( & main, & m, & key2, & w ( " d" ) ) .unwrap ( ) ;
730
assert! ( taken.how.contains ( " older key" ) , " {}" , taken.how) ;
731
assert_eq! ( std:: fs:: read( w ( " d/debug/marker" ) ) .unwrap ( ) , b " SECOND" ) ;
077a6524 Andy McCall 16 days ago ↶
732
pool.settle ( & main, & m, & key2, & w ( " d" ) , & built, true , & keep_all ( ) ) .unwrap ( ) ;
2594278f Andy McCall 17 days ago ↶
733
734
let feature = dir_ctx ( " feature" ) ;
735
let taken = pool.take ( & feature, & m, & key2, & w ( " e" ) ) .unwrap ( ) ;
736
assert! ( taken.how.contains ( " copied from another bookmark's (main-" ) , " {}" , taken.how) ;
737
assert_eq! ( taken.sources.as_ref ( ) , Some ( & built) ) ;
738
assert_eq! ( std:: fs:: read( w ( " e/debug/marker" ) ) .unwrap ( ) , b " SECOND" ) ;
739
assert! ( pool.take ( & main, & m, & key2, & w ( " f" ) ) .is_some ( ) , " main's is still there" ) ;
740
assert_eq! ( modified ( & w ( " e/debug/marker" ) ) , modified ( & w ( " f/debug/marker" ) ) , " a copy keeps the times" ) ;
077a6524 Andy McCall 16 days ago ↶
741
pool.settle ( & main, & m, & key2, & w ( " f" ) , & built, true , & keep_all ( ) ) .unwrap ( ) ;
2594278f Andy McCall 17 days ago ↶
742
743
744
let image = Ctx { form: Form:: Image, .. main } ;
745
assert! ( pool.take ( & image, & m, & key2, & w ( " g.ext4" ) ) .is_none ( ) ) ;
746
disk ( & w ( " h.ext4" ) , b " DISK" ) ;
747
pool.keep ( & image, & m, & key2, & w ( " h.ext4" ) , & built) .unwrap ( ) ;
748
let taken = pool.take ( & main, & m, & key2, & w ( " i" ) ) .unwrap ( ) ;
749
assert! ( w ( " i/debug/marker" ) .is_file ( ) , " {}" , taken.how) ;
750
assert! ( pool.take ( & main, & m, & key2, & w ( " j" ) ) .is_none ( ) , " the image is not a directory" ) ;
751
assert! ( pool.take ( & image, & m, & key2, & w ( " k.ext4" ) ) .is_some ( ) , " and is still there for a microVM" ) ;
752
}
753
754
# [ test ]
755
fn a_failing_jobs_directory_is_thrown_away ( ) {
756
let home = tempfile:: tempdir( ) .unwrap ( ) ;
757
let work = tempfile:: tempdir( ) .unwrap ( ) ;
758
let pool = Pool:: new( home.path ( ) , None ) ;
759
let m = mount ( & [ ] ) ;
760
let main = dir_ctx ( " main" ) ;
761
tree ( & work.path ( ) .join ( " a" ) , b " GOOD" , 1 ) ;
077a6524 Andy McCall 16 days ago ↶
762
pool.settle ( & main, & m, " k" , & work.path ( ) .join ( " a" ) , & Sources:: new( ) , true , & keep_all ( ) ) .unwrap ( ) ;
2594278f Andy McCall 17 days ago ↶
763
let taken = pool.take ( & main, & m, " k" , & work.path ( ) .join ( " b" ) ) ;
764
assert! ( taken.is_some ( ) ) ;
765
766
std:: fs:: write( work.path ( ) .join ( " b/debug/marker" ) , b " POISON" ) .unwrap ( ) ;
077a6524 Andy McCall 16 days ago ↶
767
pool.settle ( & main, & m, " k" , & work.path ( ) .join ( " b" ) , & Sources:: new( ) , false , & keep_all ( ) ) .unwrap ( ) ;
2594278f Andy McCall 17 days ago ↶
768
assert! ( ! work.path ( ) .join ( " b" ) .exists ( ) ) ;
769
assert! ( pool.take ( & main, & m, " k" , & work.path ( ) .join ( " c" ) ) .is_none ( ) , " nothing it wrote reaches a later job" ) ;
770
assert! ( pool.slot_times ( ) .is_empty ( ) ) ;
771
772
let image = Ctx { form: Form:: Image, .. main } ;
773
disk ( & work.path ( ) .join ( " d.ext4" ) , b " BAD!" ) ;
077a6524 Andy McCall 16 days ago ↶
774
pool.settle ( & image, & m, " k" , & work.path ( ) .join ( " d.ext4" ) , & Sources:: new( ) , false , & keep_all ( ) ) .unwrap ( ) ;
2594278f Andy McCall 17 days ago ↶
775
assert! ( ! work.path ( ) .join ( " d.ext4" ) .exists ( ) & & pool.slot_times ( ) .is_empty ( ) ) ;
776
}
777
778
# [ test ]
779
fn directories_are_evicted_by_their_size_oldest_first ( ) {
780
let home = tempfile:: tempdir( ) .unwrap ( ) ;
781
let work = tempfile:: tempdir( ) .unwrap ( ) ;
782
783
let pool = Pool { root: home.path ( ) .join ( " cache" ) , limit: 800 < < 10 } ;
784
let m = mount ( & [ ] ) ;
785
let ( main, feature) = ( dir_ctx ( " main" ) , dir_ctx ( " feature" ) ) ;
786
tree ( & work.path ( ) .join ( " a" ) , b " OLD" , 512 ) ;
787
pool.keep ( & main, & m, " k1" , & work.path ( ) .join ( " a" ) , & Sources:: new( ) ) .unwrap ( ) ;
788
let kept = pool.cache_dir ( & main, " target" ) .join ( segment ( " main" ) ) .join ( " k1.tree" ) ;
789
assert! ( kept.is_dir ( ) , " under the limit it stays" ) ;
790
assert! ( tree_bytes ( & kept) > = 512 < < 10 , " a directory counts what is in it" ) ;
791
792
std:: fs:: File:: open( & kept) .unwrap ( ) .set_modified ( SystemTime:: UNIX_EPOCH + std:: time:: Duration:: from_secs( 1 ) ) .unwrap ( ) ;
793
tree ( & work.path ( ) .join ( " b" ) , b " NEW" , 512 ) ;
794
pool.keep ( & feature, & m, " k1" , & work.path ( ) .join ( " b" ) , & Sources:: new( ) ) .unwrap ( ) ;
795
assert! ( ! kept.exists ( ) & & ! kept.with_extension ( " tree-sources" ) .exists ( ) , " the one used longest ago goes, sources and all" ) ;
796
let taken = pool.take ( & main, & m, " k1" , & work.path ( ) .join ( " c" ) ) .unwrap ( ) ;
797
assert! ( taken.how.contains ( " bookmark's (feature-" ) , " {}" , taken.how) ;
798
assert_eq! ( std:: fs:: read( work.path ( ) .join ( " c/debug/marker" ) ) .unwrap ( ) , b " NEW" ) ;
799
}
800
801
802
803
# [ test ]
804
fn nothing_kept_is_writable_by_others_or_set_id ( ) {
805
let home = tempfile:: tempdir( ) .unwrap ( ) ;
806
let work = tempfile:: tempdir( ) .unwrap ( ) ;
807
let pool = Pool:: new( home.path ( ) , None ) ;
808
let m = mount ( & [ ] ) ;
809
let main = dir_ctx ( " main" ) ;
810
let a = work.path ( ) .join ( " a" ) ;
811
tree ( & a, b " X" , 1 ) ;
812
let mode = | p : & Path, m : u32 | std:: fs:: set_permissions( p, std:: fs:: Permissions:: from_mode( m) ) .unwrap ( ) ;
813
std:: fs:: write( a.join ( " open" ) , b " " ) .unwrap ( ) ;
814
mode ( & a.join ( " open" ) , 0o777 ) ;
815
std:: fs:: write( a.join ( " suid" ) , b " " ) .unwrap ( ) ;
816
mode ( & a.join ( " suid" ) , 0o4755 ) ;
817
std:: fs:: write( a.join ( " debug/sealed" ) , b " secret" ) .unwrap ( ) ;
818
mode ( & a.join ( " debug/sealed" ) , 0o000 ) ;
819
std:: os:: unix:: fs:: symlink( " /etc/passwd" , a.join ( " link" ) ) .unwrap ( ) ;
820
let when = modified ( & a.join ( " debug/marker" ) ) ;
821
mode ( & a.join ( " debug" ) , 0o1777 ) ;
822
mode ( & a, 0o500 ) ;
823
pool.keep ( & main, & m, " k" , & a, & Sources:: new( ) ) .unwrap ( ) ;
824
let root = home.path ( ) .join ( " cache" ) ;
825
assert_eq! ( std:: fs:: metadata( & root) .unwrap ( ) .mode ( ) & 0o777 , 0o700 , " the pool is the runner's alone" ) ;
826
let mut seen = 0 ;
827
fn each ( p : & Path, f : & mut dyn FnMut( &Path , &std ::fs ::Metadata ) ) {
828
let md = std:: fs:: symlink_metadata( p) .unwrap ( ) ;
829
f ( p, & md) ;
830
if md.is_dir ( ) {
831
for e in std:: fs:: read_dir( p) .unwrap ( ) {
832
each ( & e.unwrap ( ) .path ( ) , f) ;
833
}
834
}
835
}
836
each ( & root, & mut | p, md| {
837
seen + = 1 ;
838
if ! md.file_type ( ) .is_symlink ( ) {
839
assert_eq! ( md.mode ( ) & 0o7022 , 0 , " {} is {:o}" , p.display ( ) , md.mode ( ) ) ;
840
}
841
} ) ;
842
assert! ( seen > = 10 , " {seen}" ) ;
843
844
let taken = pool.take ( & main, & m, " k" , & work.path ( ) .join ( " b" ) ) ;
845
assert! ( taken.is_some ( ) ) ;
846
let b = work.path ( ) .join ( " b" ) ;
847
assert_eq! ( std:: fs:: read( b.join ( " debug/sealed" ) ) .unwrap ( ) , b " secret" ) ;
848
assert_eq! ( std:: fs:: metadata( b.join ( " open" ) ) .unwrap ( ) .mode ( ) & 0o777 , 0o755 ) ;
849
assert_eq! ( std:: fs:: read_link( b.join ( " link" ) ) .unwrap ( ) , Path:: new( " /etc/passwd" ) , " a link is kept, not followed" ) ;
850
assert_eq! ( modified ( & b.join ( " debug/marker" ) ) , when) ;
077a6524 Andy McCall 16 days ago ↶
851
}
852
853
854
855
fn aged ( root : & Path, rel : & str , kib : usize , secs : u64 ) -> PathBuf {
856
let p = root.join ( rel) ;
857
std:: fs:: create_dir_all( p.parent ( ) .unwrap ( ) ) .unwrap ( ) ;
858
let bytes: Vec< u8 > = ( 0 .. kib < < 10 ) .map ( | i : usize | ( i.wrapping_mul ( 2_654_435_761 ) > > 13 ) as u8 ) .collect ( ) ;
859
std:: fs:: write( & p, bytes) .unwrap ( ) ;
860
let t = SystemTime:: UNIX_EPOCH + std:: time:: Duration:: from_secs( secs) ;
861
std:: fs:: File:: options( )
862
.write ( true )
863
.open ( & p)
864
.unwrap ( )
865
.set_times ( std:: fs:: FileTimes:: new( ) .set_accessed ( t) .set_modified ( t) )
866
.unwrap ( ) ;
867
p
868
}
869
870
const T: u64 = 4_000_000_000 ;
871
872
fn at_t ( ) -> SystemTime {
873
SystemTime:: UNIX_EPOCH + std:: time:: Duration:: from_secs( T)
874
}
875
876
877
878
879
# [ test ]
880
fn a_directory_is_pruned_of_what_the_job_did_not_use_before_it_is_kept ( ) {
881
let home = tempfile:: tempdir( ) .unwrap ( ) ;
882
let work = tempfile:: tempdir( ) .unwrap ( ) ;
883
let pool = Pool:: new( home.path ( ) , None ) ;
884
let main = dir_ctx ( " main" ) ;
885
let mut m = mount ( & [ ] ) ;
886
m.size_mib = 8 ;
887
let a = work.path ( ) .join ( " a" ) ;
888
889
aged ( & a, " debug/deps/libnew-aaaaaaaaaaaaaaaa.rlib" , 2 < < 10 , T + 10 ) ;
890
aged ( & a, " debug/deps/libold-bbbbbbbbbbbbbbbb.rlib" , 3 < < 10 , T - 1_000 ) ;
891
let prune = Prune { started: at_t ( ) , percent: 50 , guest: None } ;
892
let line = pool.settle ( & main, & m, " k" , & a, & Sources:: new( ) , true , & prune) .unwrap ( ) .unwrap ( ) ;
893
assert_eq! (
894
line,
895
" cache target: kept 2 MiB of 5 MiB (pruned 3 MiB in 1 file this run did not use; the cap is 50% of its 8 MiB disk)"
896
) ;
897
let b = work.path ( ) .join ( " b" ) ;
898
assert! ( pool.take ( & main, & m, " k" , & b) .unwrap ( ) .how.contains ( " same key" ) ) ;
899
assert! ( b.join ( " debug/deps/libnew-aaaaaaaaaaaaaaaa.rlib" ) .is_file ( ) & & ! b.join ( " debug/deps/libold-bbbbbbbbbbbbbbbb.rlib" ) .exists ( ) ) ;
900
901
forget_reads ( Form:: Dir, & b) .unwrap ( ) ;
902
let md = std:: fs:: metadata( b.join ( " debug/deps/libnew-aaaaaaaaaaaaaaaa.rlib" ) ) .unwrap ( ) ;
903
assert_eq! ( ( md.atime ( ) , md.mtime ( ) ) , ( 0 , ( T + 10 ) as i64 ) ) ;
904
905
906
aged ( & b, " debug/deps/libbig-cccccccccccccccc.rlib" , 3 < < 10 , T + 20 ) ;
907
let line = pool.settle ( & main, & m, " k" , & b, & Sources:: new( ) , true , & prune) .unwrap ( ) .unwrap ( ) ;
908
assert! (
909
line.starts_with ( " cache target: not kept: 5 MiB is over its cap of 4 MiB (50% of its 8 MiB disk); the next run starts empty" ) ,
910
" {line}"
911
) ;
912
assert! ( ! b.exists ( ) & & pool.take ( & main, & m, " k" , & work.path ( ) .join ( " c" ) ) .is_none ( ) ) ;
913
}
914
915
916
917
# [ test ]
918
fn an_image_is_kept_by_what_its_guest_pruned_or_else_by_its_size ( ) {
919
let home = tempfile:: tempdir( ) .unwrap ( ) ;
920
let work = tempfile:: tempdir( ) .unwrap ( ) ;
921
let pool = Pool:: new( home.path ( ) , None ) ;
922
let ctx = Ctx { repo: " r" , bookmark: " main" , job: " j" , image: " i" , form: Form:: Image } ;
923
let mut m = mount ( & [ ] ) ;
924
m.size_mib = 16 ;
925
let fits = Pruned { size: 16 < < 20 , percent: 60 , before: 12 < < 20 , after: 6 < < 20 , freed: 6 < < 20 , files: 40 , unpruned: None } ;
926
let over = Pruned { after: 11 < < 20 , .. fits.clone ( ) } ;
927
disk ( & work.path ( ) .join ( " a.ext4" ) , b " KEEP" ) ;
928
let line = pool
929
.settle ( & ctx, & m, " k" , & work.path ( ) .join ( " a.ext4" ) , & Sources:: new( ) , true , & Prune { guest: Some ( & fits) , .. keep_all ( ) } )
930
.unwrap ( )
931
.unwrap ( ) ;
932
assert_eq! (
933
line,
934
" cache target: kept 6 MiB of 12 MiB (pruned 6 MiB in 40 files this run did not use; the cap is 60% of its 16 MiB disk)"
935
) ;
936
assert! ( pool.take ( & ctx, & m, " k" , & work.path ( ) .join ( " b.ext4" ) ) .is_none ( ) , " a 1 MiB disk is not the mount's 16 MiB" ) ;
937
disk ( & work.path ( ) .join ( " c.ext4" ) , b " OVER" ) ;
938
let line = pool
939
.settle ( & ctx, & m, " k" , & work.path ( ) .join ( " c.ext4" ) , & Sources:: new( ) , true , & Prune { guest: Some ( & over) , .. keep_all ( ) } )
940
.unwrap ( )
941
.unwrap ( ) ;
942
assert! ( line.starts_with ( " cache target: not kept: 11 MiB after pruning 6 MiB in 40 files" ) , " {line}" ) ;
943
assert! ( ! work.path ( ) .join ( " c.ext4" ) .exists ( ) ) ;
944
945
let img = work.path ( ) .join ( " d.ext4" ) ;
946
crate :: workspace:: empty_ext4( & img, 16 , " scratch" ) .unwrap ( ) ;
947
let line = pool.settle ( & ctx, & m, " k" , & img, & Sources:: new( ) , true , & Prune { percent: 60 , .. keep_all ( ) } ) .unwrap ( ) .unwrap ( ) ;
948
assert! ( line.starts_with ( " cache target: kept " ) & & line.contains ( " under its cap of 60% of its 16 MiB disk" ) , " {line}" ) ;
949
assert! ( pool.take ( & ctx, & m, " k" , & work.path ( ) .join ( " e.ext4" ) ) .is_some ( ) ) ;
950
951
let line = pool
952
.settle ( & ctx, & m, " k" , & work.path ( ) .join ( " e.ext4" ) , & Sources:: new( ) , true , & Prune { percent: 1 , .. keep_all ( ) } )
953
.unwrap ( )
954
.unwrap ( ) ;
955
assert! ( line.contains ( " not kept" ) & & line.contains ( " (the guest did not prune it)" ) , " {line}" ) ;
956
}
957
958
# [ test ]
959
fn the_superblock_says_what_is_in_use ( ) {
960
let h = " Filesystem volume name: scratch\n Block count: 4096\n Free blocks: 1024\n Block size: 4096\n " ;
961
assert_eq! ( superblock_usage ( h) , Some ( ( 3072 * 4096 , 4096 * 4096 ) ) ) ;
962
assert_eq! ( superblock_usage ( " Block count: 1\n " ) , None ) ;
d60aac6c Andy McCall 23 days ago ↶
963
}
964
}