d60aac6c Andy McCall 22 days ago ↶
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
82cf6e91 Andy McCall 22 days ago ↶
20
2594278f Andy McCall 16 days ago ↶
21
22
23
24
25
26
27
28
29
30
31
32
33
34
077a6524 Andy McCall 15 days ago ↶
35
36
37
38
39
40
41
42
43
44
45
d60aac6c Andy McCall 22 days ago ↶
46
47
use anyhow:: { Context, Result } ;
48
use flow_core:: pipeline:: Mount;
077a6524 Andy McCall 15 days ago ↶
49
use flow_init:: prune:: Pruned;
d60aac6c Andy McCall 22 days ago ↶
50
use std:: path:: { Path, PathBuf} ;
51
use std:: time:: SystemTime;
52
53
54
pub const DEFAULT_GB : u64 = 64 ;
82cf6e91 Andy McCall 22 days ago ↶
55
56
57
58
pub type Sources = std:: collections:: BTreeMap< String, ( String, i64 ) > ;
59
60
61
pub struct Taken {
62
63
pub how : String,
64
65
pub sources : Option< Sources> ,
2594278f Andy McCall 16 days ago ↶
66
}
67
68
69
# [ derive ( Clone, Copy, Debug, PartialEq, Eq ) ]
70
pub enum Form {
71
72
Image,
73
74
Dir,
75
}
76
77
impl Form {
78
fn ext ( self ) -> & 'static str {
79
match self {
80
Form:: Image => " ext4" ,
81
Form:: Dir => " tree" ,
82
}
83
}
84
}
85
86
87
fn form_of ( p : & Path) -> Option< Form> {
88
match p.extension ( ) ? .to_str ( ) ? {
89
" ext4" => Some ( Form:: Image) ,
90
" tree" => Some ( Form:: Dir) ,
91
_ => None ,
92
}
82cf6e91 Andy McCall 22 days ago ↶
93
}
94
95
fn sources_of ( disk : & Path) -> PathBuf {
2594278f Andy McCall 16 days ago ↶
96
match form_of ( disk) {
97
Some ( Form:: Dir) => disk.with_extension ( " tree-sources" ) ,
98
_ => disk.with_extension ( " sources" ) ,
99
}
82cf6e91 Andy McCall 22 days ago ↶
100
}
101
102
fn read_sources ( disk : & Path) -> Option< Sources> {
103
serde_json:: from_slice( & std:: fs:: read( sources_of ( disk) ) .ok ( ) ? ) .ok ( )
077a6524 Andy McCall 15 days ago ↶
104
}
105
106
107
108
pub struct Prune < 'a > {
109
110
111
pub started : SystemTime,
112
113
pub percent : u8 ,
114
115
pub guest : Option< & 'a Pruned> ,
82cf6e91 Andy McCall 22 days ago ↶
116
}
d60aac6c Andy McCall 22 days ago ↶
117
118
119
pub struct Pool {
120
root : PathBuf,
121
limit : u64 ,
122
}
123
2594278f Andy McCall 16 days ago ↶
124
125
d60aac6c Andy McCall 22 days ago ↶
126
pub struct Ctx < 'a > {
127
pub repo : & 'a str ,
128
pub bookmark : & 'a str ,
129
pub job : & 'a str ,
130
pub image : & 'a str ,
2594278f Andy McCall 16 days ago ↶
131
pub form : Form,
d60aac6c Andy McCall 22 days ago ↶
132
}
133
82cf6e91 Andy McCall 22 days ago ↶
134
use flow_core:: api:: segment;
2594278f Andy McCall 16 days ago ↶
135
136
137
138
139
static CHANGING : std:: sync:: Mutex< ( ) > = std:: sync:: Mutex:: new( ( ) ) ;
d60aac6c Andy McCall 22 days ago ↶
140
2594278f Andy McCall 16 days ago ↶
141
fn changing ( ) -> std:: sync:: MutexGuard< 'static , ( ) > {
142
CHANGING .lock ( ) .unwrap_or_else ( | p | p.into_inner ( ) )
143
}
144
d60aac6c Andy McCall 22 days ago ↶
145
impl Pool {
146
pub fn new ( home_root : & Path, gb : Option< u64 > ) -> Pool {
147
Pool { root: home_root.join ( " cache" ) , limit: gb.unwrap_or ( DEFAULT_GB ) .saturating_mul ( 1 < < 30 ) }
148
}
149
150
fn cache_dir ( & self , ctx : & Ctx, name : & str ) -> PathBuf {
151
self .root.join ( segment ( ctx.repo) ) .join ( segment ( ctx.job) ) .join ( segment ( name) )
152
}
153
154
155
156
pub fn key ( ctx : & Ctx, m : & Mount, hash_of : impl Fn( &str ) -> Option< String> ) -> String {
157
let mut h = blake3:: Hasher:: new( ) ;
158
for part in [ ctx.image, ctx.job, m.cache.as_deref ( ) .unwrap_or ( " " ) ] {
159
h.update ( part.as_bytes ( ) ) ;
160
h.update ( b " \n " ) ;
161
}
162
for k in & m.key {
163
h.update ( k.as_bytes ( ) ) ;
164
h.update ( b " =" ) ;
165
h.update ( hash_of ( k) .unwrap_or_else ( | | " absent" .into ( ) ) .as_bytes ( ) ) ;
166
h.update ( b " \n " ) ;
167
}
168
h.finalize ( ) .to_hex ( ) [ .. 32 ] .to_string ( )
169
}
170
2594278f Andy McCall 16 days ago ↶
171
172
173
174
175
176
82cf6e91 Andy McCall 22 days ago ↶
177
pub fn take ( & self , ctx : & Ctx, m : & Mount, key : & str , dest : & Path) -> Option< Taken> {
d60aac6c Andy McCall 22 days ago ↶
178
let name = m.cache.as_deref ( ) ? ;
2594278f Andy McCall 16 days ago ↶
179
let _changing = changing ( ) ;
d60aac6c Andy McCall 22 days ago ↶
180
let dir = self .cache_dir ( ctx, name) ;
181
let size = m.size_mib as u64 * ( 1 < < 20 ) ;
2594278f Andy McCall 16 days ago ↶
182
let form = ctx.form;
183
let fits = | p : & Path| match form {
184
Form:: Image => std:: fs:: metadata( p) .is_ok_and ( | md | md.len ( ) = = size ) ,
185
Form:: Dir => std:: fs:: symlink_metadata( p) .is_ok_and ( | md | md.is_dir ( ) ) ,
186
} ;
187
let ext = form.ext ( ) ;
d60aac6c Andy McCall 22 days ago ↶
188
let mine = dir.join ( segment ( ctx.bookmark) ) ;
2594278f Andy McCall 16 days ago ↶
189
let exact = mine.join ( format! ( " {key} .{ext} " ) ) ;
82cf6e91 Andy McCall 22 days ago ↶
190
191
if fits ( & exact) {
192
let sources = read_sources ( & exact) ;
193
if std:: fs:: rename( & exact, dest) .is_ok ( ) {
194
let _ = std:: fs:: remove_file( sources_of ( & exact) ) ;
195
return Some ( Taken { how: format! ( " cache {name} : this bookmark's, same key" ) , sources } ) ;
196
}
d60aac6c Andy McCall 22 days ago ↶
197
}
2594278f Andy McCall 16 days ago ↶
198
if let Some ( p) = newest ( & mine, form, | p | fits ( p) ) {
82cf6e91 Andy McCall 22 days ago ↶
199
let sources = read_sources ( & p) ;
d60aac6c Andy McCall 22 days ago ↶
200
if std:: fs:: rename( & p, dest) .is_ok ( ) {
82cf6e91 Andy McCall 22 days ago ↶
201
let _ = std:: fs:: remove_file( sources_of ( & p) ) ;
202
return Some ( Taken { how: format! ( " cache {name} : this bookmark's, an older key" ) , sources } ) ;
d60aac6c Andy McCall 22 days ago ↶
203
}
204
}
205
let others: Vec< PathBuf> =
206
std:: fs:: read_dir( & dir) .into_iter ( ) .flatten ( ) .flatten ( ) .map ( | e | e.path ( ) ) .filter ( | p | p.is_dir ( ) & & * p ! = mine ) .collect ( ) ;
207
let pick = others
208
.iter ( )
2594278f Andy McCall 16 days ago ↶
209
.map ( | d | d.join ( format! ( " {key} .{ext} " ) ) )
d60aac6c Andy McCall 22 days ago ↶
210
.filter ( | p | fits ( p) )
211
.max_by_key ( | p | modified ( p) )
212
.map ( | p | ( p, " same key" ) )
2594278f Andy McCall 16 days ago ↶
213
.or_else ( | | {
214
others.iter ( ) .filter_map ( | d | newest ( d, form, | p | fits ( p) ) ) .max_by_key ( | p | modified ( p) ) .map ( | p | ( p, " an older key" ) )
215
} ) ;
d60aac6c Andy McCall 22 days ago ↶
216
if let Some ( ( p, which) ) = pick {
217
let from = p.parent ( ) .and_then ( | d | d.file_name ( ) ) .map ( | n | n.to_string_lossy ( ) .into_owned ( ) ) .unwrap_or_default ( ) ;
2594278f Andy McCall 16 days ago ↶
218
if copy ( form, & p, dest) .is_ok ( ) {
82cf6e91 Andy McCall 22 days ago ↶
219
return Some ( Taken {
220
how: format! ( " cache {name} : copied from another bookmark's ({from} ), {which} " ) ,
221
sources: read_sources ( & p) ,
222
} ) ;
d60aac6c Andy McCall 22 days ago ↶
223
}
224
}
225
None
226
}
227
228
229
82cf6e91 Andy McCall 22 days ago ↶
230
pub fn keep ( & self , ctx : & Ctx, m : & Mount, key : & str , disk : & Path, sources : & Sources) -> Result< ( ) > {
d60aac6c Andy McCall 22 days ago ↶
231
let Some ( name) = m.cache.as_deref ( ) else { return Ok ( ( ) ) } ;
2594278f Andy McCall 16 days ago ↶
232
let _changing = changing ( ) ;
233
234
235
if ctx.form = = Form:: Dir {
236
seal ( disk) .with_context ( | | format! ( " seal {} " , disk.display ( ) ) ) ? ;
237
}
d60aac6c Andy McCall 22 days ago ↶
238
let mine = self .cache_dir ( ctx, name) .join ( segment ( ctx.bookmark) ) ;
3554afef Andy McCall 9 days ago ↶
239
crate :: portable:: create_private_dir( & mine) ? ;
240
crate :: portable:: set_mode( & self .root, 0o700 ) ? ;
2594278f Andy McCall 16 days ago ↶
241
let dest = mine.join ( format! ( " {key} .{} " , ctx.form.ext ( ) ) ) ;
242
if ctx.form = = Form:: Dir {
243
244
crate :: workspace:: remove_tree( & dest) ? ;
245
}
d60aac6c Andy McCall 22 days ago ↶
246
if std:: fs:: rename( disk, & dest) .is_err ( ) {
2594278f Andy McCall 16 days ago ↶
247
copy ( ctx.form, disk, & dest) .with_context ( | | format! ( " keep {} " , disk.display ( ) ) ) ? ;
d60aac6c Andy McCall 22 days ago ↶
248
}
249
2594278f Andy McCall 16 days ago ↶
250
std:: fs:: File:: open( & dest) ? .set_modified ( SystemTime:: now( ) ) ? ;
251
let sidecar = sources_of ( & dest) ;
252
let tmp = sidecar.with_extension ( " sources-part" ) ;
253
let _ = std:: fs:: remove_file( & tmp) ;
254
{
255
use std:: io:: Write;
3554afef Andy McCall 9 days ago ↶
256
crate :: portable:: private( std:: fs:: OpenOptions:: new( ) .write ( true ) .create_new ( true ) )
257
.open ( & tmp) ?
258
.write_all ( & serde_json:: to_vec( sources) ? ) ? ;
2594278f Andy McCall 16 days ago ↶
259
}
260
std:: fs:: rename( & tmp, sidecar) ? ;
d60aac6c Andy McCall 22 days ago ↶
261
262
263
self .evict ( ) ;
264
Ok ( ( ) )
2594278f Andy McCall 16 days ago ↶
265
}
266
077a6524 Andy McCall 15 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 16 days ago ↶
282
if passed {
077a6524 Andy McCall 15 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 16 days ago ↶
292
}
077a6524 Andy McCall 15 days ago ↶
293
discard ( ctx.form, disk) ? ;
294
Ok ( None )
82cf6e91 Andy McCall 22 days ago ↶
295
}
296
297
298
1e6b9fb1 Andy McCall 22 days ago ↶
299
300
2594278f Andy McCall 16 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 22 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 16 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 22 days ago ↶
316
}
317
}
318
}
319
out
d60aac6c Andy McCall 22 days ago ↶
320
}
321
2594278f Andy McCall 16 days ago ↶
322
323
324
325
fn evict ( & self ) -> u64 {
d60aac6c Andy McCall 22 days ago ↶
326
let mut disks: Vec< ( SystemTime, u64 , PathBuf) > = Vec :: new( ) ;
2594278f Andy McCall 16 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 {
3554afef Andy McCall 9 days ago ↶
331
Form:: Image => crate :: portable:: disk_bytes( & md) ,
2594278f Andy McCall 16 days ago ↶
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 22 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 16 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 22 days ago ↶
349
let _ = std:: fs:: remove_file( sources_of ( & p) ) ;
d60aac6c Andy McCall 22 days ago ↶
350
total - = bytes;
351
freed + = bytes;
352
}
353
}
354
freed
077a6524 Andy McCall 15 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 22 days ago ↶
440
}
441
}
442
2594278f Andy McCall 16 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 22 days ago ↶
453
fn modified ( p : & Path) -> SystemTime {
2594278f Andy McCall 16 days ago ↶
454
std:: fs:: symlink_metadata( p) .and_then ( | m | m.modified ( ) ) .unwrap_or ( SystemTime:: UNIX_EPOCH )
d60aac6c Andy McCall 22 days ago ↶
455
}
456
2594278f Andy McCall 16 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 22 days ago ↶
459
}
460
2594278f Andy McCall 16 days ago ↶
461
462
463
fn kept ( dir : & Path, depth : usize , f : & mut dyn FnMut( &Path , Form ) ) {
d60aac6c Andy McCall 22 days ago ↶
464
for e in std:: fs:: read_dir( dir) .into_iter ( ) .flatten ( ) .flatten ( ) {
465
let p = e.path ( ) ;
2594278f Andy McCall 16 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 } ;
3554afef Andy McCall 9 days ago ↶
484
if crate :: portable:: shared_identity( & md) .is_some_and ( | id | ! seen.insert ( id) ) {
2594278f Andy McCall 16 days ago ↶
485
return 0 ;
486
}
3554afef Andy McCall 9 days ago ↶
487
let mut bytes = crate :: portable:: disk_bytes( & md) ;
2594278f Andy McCall 16 days ago ↶
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
}
3554afef Andy McCall 9 days ago ↶
508
let mode = crate :: portable:: mode( & md) ;
2594278f Andy McCall 16 days ago ↶
509
let want = ( mode & ! 0o7022 ) | if md.is_dir ( ) { 0o700 } else { 0o400 } ;
510
if want ! = mode {
3554afef Andy McCall 9 days ago ↶
511
crate :: portable:: set_mode( p, want) ? ;
2594278f Andy McCall 16 days ago ↶
512
}
513
if md.is_dir ( ) {
514
for e in std:: fs:: read_dir( p) ? {
515
seal ( & e? .path ( ) ) ? ;
d60aac6c Andy McCall 22 days ago ↶
516
}
2594278f Andy McCall 16 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 22 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 16 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
3554afef Andy McCall 9 days ago ↶
561
# [ cfg ( all ( test, unix ) ) ]
d60aac6c Andy McCall 22 days ago ↶
562
mod tests {
563
use super :: * ;
564
use flow_core:: pipeline:: MountSource;
3554afef Andy McCall 9 days ago ↶
565
use std:: os:: unix:: fs:: { MetadataExt, PermissionsExt} ;
d60aac6c Andy McCall 22 days ago ↶
566
077a6524 Andy McCall 15 days ago ↶
567
568
fn keep_all ( ) -> Prune< 'static > {
569
Prune { started: SystemTime:: now( ) , percent: 100 , guest: None }
570
}
571
d60aac6c Andy McCall 22 days ago ↶
572
fn mount ( key : & [& str ]) -> Mount {
573
Mount {
574
source: MountSource:: Disk,
575
target: " /src/target" .into ( ) ,
576
size_mib: 1 ,
577
cache: Some ( " target" .into ( ) ) ,
578
key: key.iter ( ) .map ( | s | s.to_string ( ) ) .collect ( ) ,
579
}
580
}
581
582
fn disk ( path : & Path, fill : & [u8 ]) {
583
let f = std:: fs:: File:: create( path) .unwrap ( ) ;
584
f.set_len ( 1 < < 20 ) .unwrap ( ) ;
585
std:: os:: unix:: fs:: FileExt:: write_at( & f, fill, 0 ) .unwrap ( ) ;
586
}
587
588
fn first ( path : & Path) -> Vec< u8 > {
589
std:: fs:: read( path) .unwrap ( ) [ .. 4 ] .to_vec ( )
590
}
591
592
# [ test ]
593
fn a_kept_disk_starts_the_next_job_of_its_bookmark_then_others_copy_it ( ) {
594
let home = tempfile:: tempdir( ) .unwrap ( ) ;
595
let work = tempfile:: tempdir( ) .unwrap ( ) ;
596
let pool = Pool:: new( home.path ( ) , None ) ;
597
let m = mount ( & [ " Cargo.lock" ] ) ;
2594278f Andy McCall 16 days ago ↶
598
let main = Ctx { repo: " andy/hub" , bookmark: " main" , job: " test" , image: " img" , form: Form:: Image } ;
d60aac6c Andy McCall 22 days ago ↶
599
let key = Pool:: key( & main, & m, | _| Some ( " lock-1" .into ( ) ) ) ;
600
82cf6e91 Andy McCall 22 days ago ↶
601
assert! ( pool.take ( & main, & m, & key, & work.path ( ) .join ( " a.ext4" ) ) .is_none ( ) ) ;
d60aac6c Andy McCall 22 days ago ↶
602
603
disk ( & work.path ( ) .join ( " a.ext4" ) , b " MAIN" ) ;
82cf6e91 Andy McCall 22 days ago ↶
604
let built: Sources = [ ( " src/lib.rs" .to_string ( ) , ( " h1" .to_string ( ) , 1_000 ) ) ] .into ( ) ;
605
pool.keep ( & main, & m, & key, & work.path ( ) .join ( " a.ext4" ) , & built) .unwrap ( ) ;
d60aac6c Andy McCall 22 days ago ↶
606
assert! ( ! work.path ( ) .join ( " a.ext4" ) .exists ( ) , " moved, not copied" ) ;
82cf6e91 Andy McCall 22 days ago ↶
607
let taken = pool.take ( & main, & m, & key, & work.path ( ) .join ( " b.ext4" ) ) .unwrap ( ) ;
608
assert! ( taken.how.contains ( " same key" ) , " {}" , taken.how) ;
609
assert_eq! ( taken.sources.as_ref ( ) , Some ( & built) , " its sources come with it (#52)" ) ;
d60aac6c Andy McCall 22 days ago ↶
610
assert_eq! ( first ( & work.path ( ) .join ( " b.ext4" ) ) , b " MAIN" ) ;
82cf6e91 Andy McCall 22 days ago ↶
611
pool.keep ( & main, & m, & key, & work.path ( ) .join ( " b.ext4" ) , & built) .unwrap ( ) ;
d60aac6c Andy McCall 22 days ago ↶
612
613
let key2 = Pool:: key( & main, & m, | _| Some ( " lock-2" .into ( ) ) ) ;
614
assert_ne! ( key, key2) ;
82cf6e91 Andy McCall 22 days ago ↶
615
let taken = pool.take ( & main, & m, & key2, & work.path ( ) .join ( " c.ext4" ) ) .unwrap ( ) ;
616
assert! ( taken.how.contains ( " older key" ) , " {}" , taken.how) ;
617
assert_eq! ( taken.sources.as_ref ( ) , Some ( & built) ) ;
618
pool.keep ( & main, & m, & key2, & work.path ( ) .join ( " c.ext4" ) , & built) .unwrap ( ) ;
1e6b9fb1 Andy McCall 22 days ago ↶
619
assert_eq! (
620
pool.slot_times ( ) .into_keys ( ) .collect:: < Vec< _ > > ( ) ,
621
vec! [ flow_core:: api:: cache_slot( " andy/hub" , " test" ) ] ,
622
" what the claim reports (#51)"
623
) ;
624
625
let now = SystemTime:: now( ) .duration_since ( SystemTime:: UNIX_EPOCH ) .unwrap ( ) .as_millis ( ) as u64 ;
626
let kept = pool.slot_times ( ) [ & flow_core:: api:: cache_slot( " andy/hub" , " test" ) ] ;
627
assert! ( now.abs_diff ( kept) < 60_000 , " kept {kept}, now {now}" ) ;
d60aac6c Andy McCall 22 days ago ↶
628
629
let feature = Ctx { bookmark: " feature" , .. main } ;
82cf6e91 Andy McCall 22 days ago ↶
630
let taken = pool.take ( & feature, & m, & key2, & work.path ( ) .join ( " d.ext4" ) ) .unwrap ( ) ;
631
assert! ( taken.how.contains ( " copied from another bookmark" ) , " {}" , taken.how) ;
632
assert_eq! ( taken.sources.as_ref ( ) , Some ( & built) , " a copy reads them and leaves them" ) ;
d60aac6c Andy McCall 22 days ago ↶
633
assert_eq! ( first ( & work.path ( ) .join ( " d.ext4" ) ) , b " MAIN" ) ;
82cf6e91 Andy McCall 22 days ago ↶
634
let taken = pool.take ( & main, & m, & key2, & work.path ( ) .join ( " e.ext4" ) ) .unwrap ( ) ;
635
assert! ( taken.how.contains ( " this bookmark's" ) & & taken.sources.is_some ( ) , " main's is still there, with its sources" ) ;
d60aac6c Andy McCall 22 days ago ↶
636
637
let other_job = Ctx { job: " build" , .. main } ;
82cf6e91 Andy McCall 22 days ago ↶
638
assert! ( pool.take ( & other_job, & m, & key2, & work.path ( ) .join ( " f.ext4" ) ) .is_none ( ) ) ;
d60aac6c Andy McCall 22 days ago ↶
639
let mut bigger = mount ( & [ " Cargo.lock" ] ) ;
640
bigger.size_mib = 2 ;
82cf6e91 Andy McCall 22 days ago ↶
641
assert! ( pool.take ( & feature, & bigger, & key2, & work.path ( ) .join ( " g.ext4" ) ) .is_none ( ) ) ;
d60aac6c Andy McCall 22 days ago ↶
642
}
643
644
# [ test ]
645
fn the_key_follows_the_image_the_job_and_the_key_files ( ) {
2594278f Andy McCall 16 days ago ↶
646
let ctx = Ctx { repo: " r" , bookmark: " main" , job: " test" , image: " img-1" , form: Form:: Image } ;
d60aac6c Andy McCall 22 days ago ↶
647
let m = mount ( & [ " Cargo.lock" , " rust-toolchain.toml" ] ) ;
648
let k = Pool:: key( & ctx, & m, | p | Some ( format! ( " {p} -v1" ) ) ) ;
649
assert_eq! ( k, Pool:: key( & ctx, & m, | p | Some ( format! ( " {p} -v1" ) ) ) ) ;
650
assert_ne! ( k, Pool:: key( & ctx, & m, | p | Some ( format! ( " {p} -v2" ) ) ) , " a key file changed" ) ;
651
assert_ne! ( k, Pool:: key( & Ctx { image: " img-2" , .. ctx } , & m, | p | Some ( format! ( " {p} -v1" ) ) ) , " another image" ) ;
652
assert_ne! ( k, Pool:: key( & ctx, & m, | _| None ) , " a key file missing" ) ;
653
}
654
655
# [ test ]
656
fn the_pool_drops_the_disks_used_longest_ago ( ) {
657
let home = tempfile:: tempdir( ) .unwrap ( ) ;
658
let work = tempfile:: tempdir( ) .unwrap ( ) ;
659
let pool = Pool { root: home.path ( ) .join ( " cache" ) , limit: 0 } ;
660
let m = mount ( & [ ] ) ;
2594278f Andy McCall 16 days ago ↶
661
let ctx = Ctx { repo: " r" , bookmark: " main" , job: " j" , image: " i" , form: Form:: Image } ;
d60aac6c Andy McCall 22 days ago ↶
662
let full = | p : & Path| {
663
let f = std:: fs:: File:: create( p) .unwrap ( ) ;
664
std:: os:: unix:: fs:: FileExt:: write_at( & f, & vec! [ 7 u8 ; 1 < < 20 ] , 0 ) .unwrap ( ) ;
665
} ;
666
full ( & work.path ( ) .join ( " a.ext4" ) ) ;
667
82cf6e91 Andy McCall 22 days ago ↶
668
pool.keep ( & ctx, & m, " k1" , & work.path ( ) .join ( " a.ext4" ) , & Sources:: new( ) ) .unwrap ( ) ;
669
assert! ( pool.take ( & ctx, & m, " k1" , & work.path ( ) .join ( " b.ext4" ) ) .is_none ( ) ) ;
d60aac6c Andy McCall 22 days ago ↶
670
let roomy = Pool { root: home.path ( ) .join ( " cache" ) , limit: 1 < < 30 } ;
671
full ( & work.path ( ) .join ( " c.ext4" ) ) ;
82cf6e91 Andy McCall 22 days ago ↶
672
roomy.keep ( & ctx, & m, " k2" , & work.path ( ) .join ( " c.ext4" ) , & Sources:: new( ) ) .unwrap ( ) ;
d60aac6c Andy McCall 22 days ago ↶
673
assert! ( roomy.take ( & ctx, & m, " k2" , & work.path ( ) .join ( " d.ext4" ) ) .is_some ( ) , " under the limit it stays" ) ;
2594278f Andy McCall 16 days ago ↶
674
}
675
676
677
678
fn tree ( path : & Path, marker : & [u8 ], kib : usize ) {
679
std:: fs:: create_dir_all( path.join ( " debug" ) ) .unwrap ( ) ;
680
std:: fs:: write( path.join ( " debug/marker" ) , marker) .unwrap ( ) ;
681
682
let bytes: Vec< u8 > = ( 0 .. kib < < 10 ) .map ( | i : usize | ( i.wrapping_mul ( 2_654_435_761 ) > > 13 ) as u8 ) .collect ( ) ;
683
std:: fs:: write( path.join ( " debug/blob" ) , bytes) .unwrap ( ) ;
684
}
685
686
fn dir_ctx < 'a > ( bookmark : & 'a str ) -> Ctx< 'a > {
687
Ctx { repo: " andy/flow" , bookmark, job: " test" , image: " img" , form: Form:: Dir }
688
}
689
690
691
692
# [ test ]
693
fn a_kept_directory_starts_the_next_syscall_job_and_others_copy_it ( ) {
694
let home = tempfile:: tempdir( ) .unwrap ( ) ;
695
let work = tempfile:: tempdir( ) .unwrap ( ) ;
696
let pool = Pool:: new( home.path ( ) , None ) ;
697
let m = mount ( & [ " Cargo.lock" ] ) ;
698
let main = dir_ctx ( " main" ) ;
699
let key = Pool:: key( & main, & m, | _| Some ( " lock-1" .into ( ) ) ) ;
700
let w = | n : & str | work.path ( ) .join ( n) ;
701
assert! ( pool.take ( & main, & m, & key, & w ( " a" ) ) .is_none ( ) , " nothing kept yet" ) ;
702
703
tree ( & w ( " a" ) , b " FIRST" , 4 ) ;
704
705
std:: fs:: write( w ( " a/debug/inner.ext4" ) , b " not a disk" ) .unwrap ( ) ;
706
let old = SystemTime:: UNIX_EPOCH + std:: time:: Duration:: from_secs( 1_000_000 ) ;
707
std:: fs:: File:: open( w ( " a/debug/marker" ) ) .unwrap ( ) .set_modified ( old) .unwrap ( ) ;
708
let built: Sources = [ ( " src/lib.rs" .to_string ( ) , ( " h1" .to_string ( ) , 1_000 ) ) ] .into ( ) ;
077a6524 Andy McCall 15 days ago ↶
709
pool.settle ( & main, & m, & key, & w ( " a" ) , & built, true , & keep_all ( ) ) .unwrap ( ) ;
2594278f Andy McCall 16 days ago ↶
710
assert! ( ! w ( " a" ) .exists ( ) , " moved, not copied" ) ;
711
assert_eq! (
712
pool.slot_times ( ) .into_keys ( ) .collect:: < Vec< _ > > ( ) ,
713
vec! [ flow_core:: api:: cache_slot( " andy/flow" , " test" ) ] ,
714
" the claim reports it like an image (#51)"
715
) ;
716
717
718
let taken = pool.take ( & main, & m, & key, & w ( " b" ) ) .unwrap ( ) ;
719
assert! ( taken.how.contains ( " this bookmark's, same key" ) , " {}" , taken.how) ;
720
assert_eq! ( taken.sources.as_ref ( ) , Some ( & built) , " its sources come with it (#52)" ) ;
721
assert_eq! ( std:: fs:: read( w ( " b/debug/marker" ) ) .unwrap ( ) , b " FIRST" ) ;
722
assert_eq! ( modified ( & w ( " b/debug/marker" ) ) , old, " its files keep their times" ) ;
723
assert_eq! ( std:: fs:: read( w ( " b/debug/inner.ext4" ) ) .unwrap ( ) , b " not a disk" ) ;
724
assert! ( pool.take ( & main, & m, & key, & w ( " c" ) ) .is_none ( ) , " taken once" ) ;
725
assert! ( pool.slot_times ( ) .is_empty ( ) ) ;
726
727
std:: fs:: write( w ( " b/debug/marker" ) , b " SECOND" ) .unwrap ( ) ;
077a6524 Andy McCall 15 days ago ↶
728
pool.settle ( & main, & m, & key, & w ( " b" ) , & built, true , & keep_all ( ) ) .unwrap ( ) ;
2594278f Andy McCall 16 days ago ↶
729
let key2 = Pool:: key( & main, & m, | _| Some ( " lock-2" .into ( ) ) ) ;
730
let taken = pool.take ( & main, & m, & key2, & w ( " d" ) ) .unwrap ( ) ;
731
assert! ( taken.how.contains ( " older key" ) , " {}" , taken.how) ;
732
assert_eq! ( std:: fs:: read( w ( " d/debug/marker" ) ) .unwrap ( ) , b " SECOND" ) ;
077a6524 Andy McCall 15 days ago ↶
733
pool.settle ( & main, & m, & key2, & w ( " d" ) , & built, true , & keep_all ( ) ) .unwrap ( ) ;
2594278f Andy McCall 16 days ago ↶
734
735
let feature = dir_ctx ( " feature" ) ;
736
let taken = pool.take ( & feature, & m, & key2, & w ( " e" ) ) .unwrap ( ) ;
737
assert! ( taken.how.contains ( " copied from another bookmark's (main-" ) , " {}" , taken.how) ;
738
assert_eq! ( taken.sources.as_ref ( ) , Some ( & built) ) ;
739
assert_eq! ( std:: fs:: read( w ( " e/debug/marker" ) ) .unwrap ( ) , b " SECOND" ) ;
740
assert! ( pool.take ( & main, & m, & key2, & w ( " f" ) ) .is_some ( ) , " main's is still there" ) ;
741
assert_eq! ( modified ( & w ( " e/debug/marker" ) ) , modified ( & w ( " f/debug/marker" ) ) , " a copy keeps the times" ) ;
077a6524 Andy McCall 15 days ago ↶
742
pool.settle ( & main, & m, & key2, & w ( " f" ) , & built, true , & keep_all ( ) ) .unwrap ( ) ;
2594278f Andy McCall 16 days ago ↶
743
744
745
let image = Ctx { form: Form:: Image, .. main } ;
746
assert! ( pool.take ( & image, & m, & key2, & w ( " g.ext4" ) ) .is_none ( ) ) ;
747
disk ( & w ( " h.ext4" ) , b " DISK" ) ;
748
pool.keep ( & image, & m, & key2, & w ( " h.ext4" ) , & built) .unwrap ( ) ;
749
let taken = pool.take ( & main, & m, & key2, & w ( " i" ) ) .unwrap ( ) ;
750
assert! ( w ( " i/debug/marker" ) .is_file ( ) , " {}" , taken.how) ;
751
assert! ( pool.take ( & main, & m, & key2, & w ( " j" ) ) .is_none ( ) , " the image is not a directory" ) ;
752
assert! ( pool.take ( & image, & m, & key2, & w ( " k.ext4" ) ) .is_some ( ) , " and is still there for a microVM" ) ;
753
}
754
755
# [ test ]
756
fn a_failing_jobs_directory_is_thrown_away ( ) {
757
let home = tempfile:: tempdir( ) .unwrap ( ) ;
758
let work = tempfile:: tempdir( ) .unwrap ( ) ;
759
let pool = Pool:: new( home.path ( ) , None ) ;
760
let m = mount ( & [ ] ) ;
761
let main = dir_ctx ( " main" ) ;
762
tree ( & work.path ( ) .join ( " a" ) , b " GOOD" , 1 ) ;
077a6524 Andy McCall 15 days ago ↶
763
pool.settle ( & main, & m, " k" , & work.path ( ) .join ( " a" ) , & Sources:: new( ) , true , & keep_all ( ) ) .unwrap ( ) ;
2594278f Andy McCall 16 days ago ↶
764
let taken = pool.take ( & main, & m, " k" , & work.path ( ) .join ( " b" ) ) ;
765
assert! ( taken.is_some ( ) ) ;
766
767
std:: fs:: write( work.path ( ) .join ( " b/debug/marker" ) , b " POISON" ) .unwrap ( ) ;
077a6524 Andy McCall 15 days ago ↶
768
pool.settle ( & main, & m, " k" , & work.path ( ) .join ( " b" ) , & Sources:: new( ) , false , & keep_all ( ) ) .unwrap ( ) ;
2594278f Andy McCall 16 days ago ↶
769
assert! ( ! work.path ( ) .join ( " b" ) .exists ( ) ) ;
770
assert! ( pool.take ( & main, & m, " k" , & work.path ( ) .join ( " c" ) ) .is_none ( ) , " nothing it wrote reaches a later job" ) ;
771
assert! ( pool.slot_times ( ) .is_empty ( ) ) ;
772
773
let image = Ctx { form: Form:: Image, .. main } ;
774
disk ( & work.path ( ) .join ( " d.ext4" ) , b " BAD!" ) ;
077a6524 Andy McCall 15 days ago ↶
775
pool.settle ( & image, & m, " k" , & work.path ( ) .join ( " d.ext4" ) , & Sources:: new( ) , false , & keep_all ( ) ) .unwrap ( ) ;
2594278f Andy McCall 16 days ago ↶
776
assert! ( ! work.path ( ) .join ( " d.ext4" ) .exists ( ) & & pool.slot_times ( ) .is_empty ( ) ) ;
777
}
778
779
# [ test ]
780
fn directories_are_evicted_by_their_size_oldest_first ( ) {
781
let home = tempfile:: tempdir( ) .unwrap ( ) ;
782
let work = tempfile:: tempdir( ) .unwrap ( ) ;
783
784
let pool = Pool { root: home.path ( ) .join ( " cache" ) , limit: 800 < < 10 } ;
785
let m = mount ( & [ ] ) ;
786
let ( main, feature) = ( dir_ctx ( " main" ) , dir_ctx ( " feature" ) ) ;
787
tree ( & work.path ( ) .join ( " a" ) , b " OLD" , 512 ) ;
788
pool.keep ( & main, & m, " k1" , & work.path ( ) .join ( " a" ) , & Sources:: new( ) ) .unwrap ( ) ;
789
let kept = pool.cache_dir ( & main, " target" ) .join ( segment ( " main" ) ) .join ( " k1.tree" ) ;
790
assert! ( kept.is_dir ( ) , " under the limit it stays" ) ;
791
assert! ( tree_bytes ( & kept) > = 512 < < 10 , " a directory counts what is in it" ) ;
792
793
std:: fs:: File:: open( & kept) .unwrap ( ) .set_modified ( SystemTime:: UNIX_EPOCH + std:: time:: Duration:: from_secs( 1 ) ) .unwrap ( ) ;
794
tree ( & work.path ( ) .join ( " b" ) , b " NEW" , 512 ) ;
795
pool.keep ( & feature, & m, " k1" , & work.path ( ) .join ( " b" ) , & Sources:: new( ) ) .unwrap ( ) ;
796
assert! ( ! kept.exists ( ) & & ! kept.with_extension ( " tree-sources" ) .exists ( ) , " the one used longest ago goes, sources and all" ) ;
797
let taken = pool.take ( & main, & m, " k1" , & work.path ( ) .join ( " c" ) ) .unwrap ( ) ;
798
assert! ( taken.how.contains ( " bookmark's (feature-" ) , " {}" , taken.how) ;
799
assert_eq! ( std:: fs:: read( work.path ( ) .join ( " c/debug/marker" ) ) .unwrap ( ) , b " NEW" ) ;
800
}
801
802
803
804
# [ test ]
805
fn nothing_kept_is_writable_by_others_or_set_id ( ) {
806
let home = tempfile:: tempdir( ) .unwrap ( ) ;
807
let work = tempfile:: tempdir( ) .unwrap ( ) ;
808
let pool = Pool:: new( home.path ( ) , None ) ;
809
let m = mount ( & [ ] ) ;
810
let main = dir_ctx ( " main" ) ;
811
let a = work.path ( ) .join ( " a" ) ;
812
tree ( & a, b " X" , 1 ) ;
813
let mode = | p : & Path, m : u32 | std:: fs:: set_permissions( p, std:: fs:: Permissions:: from_mode( m) ) .unwrap ( ) ;
814
std:: fs:: write( a.join ( " open" ) , b " " ) .unwrap ( ) ;
815
mode ( & a.join ( " open" ) , 0o777 ) ;
816
std:: fs:: write( a.join ( " suid" ) , b " " ) .unwrap ( ) ;
817
mode ( & a.join ( " suid" ) , 0o4755 ) ;
818
std:: fs:: write( a.join ( " debug/sealed" ) , b " secret" ) .unwrap ( ) ;
819
mode ( & a.join ( " debug/sealed" ) , 0o000 ) ;
820
std:: os:: unix:: fs:: symlink( " /etc/passwd" , a.join ( " link" ) ) .unwrap ( ) ;
821
let when = modified ( & a.join ( " debug/marker" ) ) ;
822
mode ( & a.join ( " debug" ) , 0o1777 ) ;
823
mode ( & a, 0o500 ) ;
824
pool.keep ( & main, & m, " k" , & a, & Sources:: new( ) ) .unwrap ( ) ;
825
let root = home.path ( ) .join ( " cache" ) ;
826
assert_eq! ( std:: fs:: metadata( & root) .unwrap ( ) .mode ( ) & 0o777 , 0o700 , " the pool is the runner's alone" ) ;
827
let mut seen = 0 ;
828
fn each ( p : & Path, f : & mut dyn FnMut( &Path , &std ::fs ::Metadata ) ) {
829
let md = std:: fs:: symlink_metadata( p) .unwrap ( ) ;
830
f ( p, & md) ;
831
if md.is_dir ( ) {
832
for e in std:: fs:: read_dir( p) .unwrap ( ) {
833
each ( & e.unwrap ( ) .path ( ) , f) ;
834
}
835
}
836
}
837
each ( & root, & mut | p, md| {
838
seen + = 1 ;
839
if ! md.file_type ( ) .is_symlink ( ) {
840
assert_eq! ( md.mode ( ) & 0o7022 , 0 , " {} is {:o}" , p.display ( ) , md.mode ( ) ) ;
841
}
842
} ) ;
843
assert! ( seen > = 10 , " {seen}" ) ;
844
845
let taken = pool.take ( & main, & m, " k" , & work.path ( ) .join ( " b" ) ) ;
846
assert! ( taken.is_some ( ) ) ;
847
let b = work.path ( ) .join ( " b" ) ;
848
assert_eq! ( std:: fs:: read( b.join ( " debug/sealed" ) ) .unwrap ( ) , b " secret" ) ;
849
assert_eq! ( std:: fs:: metadata( b.join ( " open" ) ) .unwrap ( ) .mode ( ) & 0o777 , 0o755 ) ;
850
assert_eq! ( std:: fs:: read_link( b.join ( " link" ) ) .unwrap ( ) , Path:: new( " /etc/passwd" ) , " a link is kept, not followed" ) ;
851
assert_eq! ( modified ( & b.join ( " debug/marker" ) ) , when) ;
077a6524 Andy McCall 15 days ago ↶
852
}
853
854
855
856
fn aged ( root : & Path, rel : & str , kib : usize , secs : u64 ) -> PathBuf {
857
let p = root.join ( rel) ;
858
std:: fs:: create_dir_all( p.parent ( ) .unwrap ( ) ) .unwrap ( ) ;
859
let bytes: Vec< u8 > = ( 0 .. kib < < 10 ) .map ( | i : usize | ( i.wrapping_mul ( 2_654_435_761 ) > > 13 ) as u8 ) .collect ( ) ;
860
std:: fs:: write( & p, bytes) .unwrap ( ) ;
861
let t = SystemTime:: UNIX_EPOCH + std:: time:: Duration:: from_secs( secs) ;
862
std:: fs:: File:: options( )
863
.write ( true )
864
.open ( & p)
865
.unwrap ( )
866
.set_times ( std:: fs:: FileTimes:: new( ) .set_accessed ( t) .set_modified ( t) )
867
.unwrap ( ) ;
868
p
869
}
870
871
const T: u64 = 4_000_000_000 ;
872
873
fn at_t ( ) -> SystemTime {
874
SystemTime:: UNIX_EPOCH + std:: time:: Duration:: from_secs( T)
875
}
876
877
878
879
880
# [ test ]
881
fn a_directory_is_pruned_of_what_the_job_did_not_use_before_it_is_kept ( ) {
882
let home = tempfile:: tempdir( ) .unwrap ( ) ;
883
let work = tempfile:: tempdir( ) .unwrap ( ) ;
884
let pool = Pool:: new( home.path ( ) , None ) ;
885
let main = dir_ctx ( " main" ) ;
886
let mut m = mount ( & [ ] ) ;
887
m.size_mib = 8 ;
888
let a = work.path ( ) .join ( " a" ) ;
889
890
aged ( & a, " debug/deps/libnew-aaaaaaaaaaaaaaaa.rlib" , 2 < < 10 , T + 10 ) ;
891
aged ( & a, " debug/deps/libold-bbbbbbbbbbbbbbbb.rlib" , 3 < < 10 , T - 1_000 ) ;
892
let prune = Prune { started: at_t ( ) , percent: 50 , guest: None } ;
893
let line = pool.settle ( & main, & m, " k" , & a, & Sources:: new( ) , true , & prune) .unwrap ( ) .unwrap ( ) ;
894
assert_eq! (
895
line,
896
" 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)"
897
) ;
898
let b = work.path ( ) .join ( " b" ) ;
899
assert! ( pool.take ( & main, & m, " k" , & b) .unwrap ( ) .how.contains ( " same key" ) ) ;
900
assert! ( b.join ( " debug/deps/libnew-aaaaaaaaaaaaaaaa.rlib" ) .is_file ( ) & & ! b.join ( " debug/deps/libold-bbbbbbbbbbbbbbbb.rlib" ) .exists ( ) ) ;
901
902
forget_reads ( Form:: Dir, & b) .unwrap ( ) ;
903
let md = std:: fs:: metadata( b.join ( " debug/deps/libnew-aaaaaaaaaaaaaaaa.rlib" ) ) .unwrap ( ) ;
904
assert_eq! ( ( md.atime ( ) , md.mtime ( ) ) , ( 0 , ( T + 10 ) as i64 ) ) ;
905
906
907
aged ( & b, " debug/deps/libbig-cccccccccccccccc.rlib" , 3 < < 10 , T + 20 ) ;
908
let line = pool.settle ( & main, & m, " k" , & b, & Sources:: new( ) , true , & prune) .unwrap ( ) .unwrap ( ) ;
909
assert! (
910
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" ) ,
911
" {line}"
912
) ;
913
assert! ( ! b.exists ( ) & & pool.take ( & main, & m, " k" , & work.path ( ) .join ( " c" ) ) .is_none ( ) ) ;
914
}
915
916
917
918
# [ test ]
919
fn an_image_is_kept_by_what_its_guest_pruned_or_else_by_its_size ( ) {
920
let home = tempfile:: tempdir( ) .unwrap ( ) ;
921
let work = tempfile:: tempdir( ) .unwrap ( ) ;
922
let pool = Pool:: new( home.path ( ) , None ) ;
923
let ctx = Ctx { repo: " r" , bookmark: " main" , job: " j" , image: " i" , form: Form:: Image } ;
924
let mut m = mount ( & [ ] ) ;
925
m.size_mib = 16 ;
926
let fits = Pruned { size: 16 < < 20 , percent: 60 , before: 12 < < 20 , after: 6 < < 20 , freed: 6 < < 20 , files: 40 , unpruned: None } ;
927
let over = Pruned { after: 11 < < 20 , .. fits.clone ( ) } ;
928
disk ( & work.path ( ) .join ( " a.ext4" ) , b " KEEP" ) ;
929
let line = pool
930
.settle ( & ctx, & m, " k" , & work.path ( ) .join ( " a.ext4" ) , & Sources:: new( ) , true , & Prune { guest: Some ( & fits) , .. keep_all ( ) } )
931
.unwrap ( )
932
.unwrap ( ) ;
933
assert_eq! (
934
line,
935
" 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)"
936
) ;
937
assert! ( pool.take ( & ctx, & m, " k" , & work.path ( ) .join ( " b.ext4" ) ) .is_none ( ) , " a 1 MiB disk is not the mount's 16 MiB" ) ;
938
disk ( & work.path ( ) .join ( " c.ext4" ) , b " OVER" ) ;
939
let line = pool
940
.settle ( & ctx, & m, " k" , & work.path ( ) .join ( " c.ext4" ) , & Sources:: new( ) , true , & Prune { guest: Some ( & over) , .. keep_all ( ) } )
941
.unwrap ( )
942
.unwrap ( ) ;
943
assert! ( line.starts_with ( " cache target: not kept: 11 MiB after pruning 6 MiB in 40 files" ) , " {line}" ) ;
944
assert! ( ! work.path ( ) .join ( " c.ext4" ) .exists ( ) ) ;
945
946
let img = work.path ( ) .join ( " d.ext4" ) ;
947
crate :: workspace:: empty_ext4( & img, 16 , " scratch" ) .unwrap ( ) ;
948
let line = pool.settle ( & ctx, & m, " k" , & img, & Sources:: new( ) , true , & Prune { percent: 60 , .. keep_all ( ) } ) .unwrap ( ) .unwrap ( ) ;
949
assert! ( line.starts_with ( " cache target: kept " ) & & line.contains ( " under its cap of 60% of its 16 MiB disk" ) , " {line}" ) ;
950
assert! ( pool.take ( & ctx, & m, " k" , & work.path ( ) .join ( " e.ext4" ) ) .is_some ( ) ) ;
951
952
let line = pool
953
.settle ( & ctx, & m, " k" , & work.path ( ) .join ( " e.ext4" ) , & Sources:: new( ) , true , & Prune { percent: 1 , .. keep_all ( ) } )
954
.unwrap ( )
955
.unwrap ( ) ;
956
assert! ( line.contains ( " not kept" ) & & line.contains ( " (the guest did not prune it)" ) , " {line}" ) ;
957
}
958
959
# [ test ]
960
fn the_superblock_says_what_is_in_use ( ) {
961
let h = " Filesystem volume name: scratch\n Block count: 4096\n Free blocks: 1024\n Block size: 4096\n " ;
962
assert_eq! ( superblock_usage ( h) , Some ( ( 3072 * 4096 , 4096 * 4096 ) ) ) ;
963
assert_eq! ( superblock_usage ( " Block count: 1\n " ) , None ) ;
d60aac6c Andy McCall 22 days ago ↶
964
}
965
}