Skip to main content

maelstrom_util/
cache.rs

1//! Manage downloading, extracting, and storing of artifacts specified by jobs.
2
3pub mod fs;
4
5use crate::{
6    config::common::CacheSize,
7    heap::{Heap, HeapDeps, HeapIndex},
8    root::{Root, RootBuf},
9};
10use anyhow::{bail, Error, Result};
11use bytesize::ByteSize;
12use derive_more::{with_trait::Debug, Deref, DerefMut};
13use fs::{FileType, Fs, Metadata};
14use maelstrom_base::{JobId, Sha256Digest};
15use slog::{debug, info, warn, Logger};
16use std::{
17    cmp::Ordering,
18    collections::{hash_map::Entry as HashEntry, HashMap, HashSet},
19    ffi::{OsStr, OsString},
20    hash::Hash,
21    iter::IntoIterator,
22    mem,
23    num::NonZeroU32,
24    path::{Path, PathBuf},
25    result,
26    str::FromStr as _,
27    string::ToString,
28};
29
30const CACHEDIR_TAG: &str = "CACHEDIR.TAG";
31const CACHEDIR_TAG_CONTENTS: [u8; 43] = *b"Signature: 8a477f597d28d172789f06886806bc55";
32const CACHEDIR_TAG_CONTENTS_LEN: usize = CACHEDIR_TAG_CONTENTS.len();
33const CACHEDIR_TAG_CONTENTS_LEN_U64: u64 = CACHEDIR_TAG_CONTENTS_LEN as u64;
34const LOCK_FILE: &str = "lock";
35const REMOVING: &str = "removing";
36const SHA256: &str = "sha256";
37
38/// Type returned from [`Cache::get_artifact`].
39#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
40pub enum GetArtifact {
41    /// The artifact is in the cache. The caller has been given a reference that must later be
42    /// released by calling [`Cache::decrement_ref_count`]. The artifact can be found at the path
43    /// returned by [`Cache::cache_path`] as long as the caller has an outstanding ref count.
44    Success,
45
46    /// The artifact is not in the cache and is currently being retrieved. There is nothing for the
47    /// caller to do other than wait. The caller's [`JobId`] will be returned at some point from a
48    /// call to [`Cache::got_artifact_success`] or [`Cache::got_artifact_failure`].
49    Wait,
50
51    /// The artifact is not in the cache but is not currently being retrieved. It's caller's
52    /// responsibility to start the retrieval process. The artifact should be put in the path
53    /// provided by [`Cache::cache_path`], and then either [`Cache::got_artifact_success`] or
54    /// [`Cache::got_artifact_failure`] should be called.
55    Get,
56}
57
58/// Type passed to [`Cache::got_artifact_success`].
59#[derive(Clone, Debug)]
60#[debug(bound(FsT::TempFile: Debug, FsT::TempDir: Debug))]
61pub enum GotArtifact<FsT: Fs> {
62    Symlink { target: PathBuf },
63    File { source: FsT::TempFile },
64    Directory { source: FsT::TempDir, size: u64 },
65}
66
67impl<FsT: Fs> GotArtifact<FsT> {
68    pub fn symlink(target: PathBuf) -> Self {
69        Self::Symlink { target }
70    }
71
72    pub fn file(source: FsT::TempFile) -> Self {
73        Self::File { source }
74    }
75
76    pub fn directory(source: FsT::TempDir, size: u64) -> Self {
77        Self::Directory { source, size }
78    }
79}
80
81impl<FsT: Fs> Eq for GotArtifact<FsT>
82where
83    FsT::TempFile: Eq,
84    FsT::TempDir: Eq,
85{
86}
87
88impl<FsT: Fs> Ord for GotArtifact<FsT>
89where
90    FsT::TempFile: Ord,
91    FsT::TempDir: Ord,
92{
93    fn cmp(&self, other: &Self) -> Ordering {
94        match (self, other) {
95            (
96                Self::Symlink {
97                    target: self_target,
98                },
99                Self::Symlink {
100                    target: other_target,
101                },
102            ) => self_target.cmp(other_target),
103            (
104                Self::File {
105                    source: self_source,
106                },
107                Self::File {
108                    source: other_source,
109                },
110            ) => self_source.cmp(other_source),
111            (
112                Self::Directory {
113                    source: self_source,
114                    size: self_size,
115                },
116                Self::Directory {
117                    source: other_source,
118                    size: other_size,
119                },
120            ) => self_source
121                .cmp(other_source)
122                .then(self_size.cmp(other_size)),
123            (Self::Symlink { .. }, _) => Ordering::Less,
124            (Self::File { .. }, Self::Directory { .. }) => Ordering::Less,
125            (Self::File { .. }, Self::Symlink { .. }) => Ordering::Greater,
126            (Self::Directory { .. }, _) => Ordering::Greater,
127        }
128    }
129}
130
131impl<FsT: Fs> PartialEq for GotArtifact<FsT>
132where
133    FsT::TempFile: PartialEq,
134    FsT::TempDir: PartialEq,
135{
136    fn eq(&self, other: &Self) -> bool {
137        match (self, other) {
138            (
139                Self::Symlink {
140                    target: self_target,
141                },
142                Self::Symlink {
143                    target: other_target,
144                },
145            ) => self_target.eq(other_target),
146            (
147                Self::File {
148                    source: self_source,
149                },
150                Self::File {
151                    source: other_source,
152                },
153            ) => self_source.eq(other_source),
154            (
155                Self::Directory {
156                    source: self_source,
157                    size: self_size,
158                },
159                Self::Directory {
160                    source: other_source,
161                    size: other_size,
162                },
163            ) => self_source.eq(other_source) && self_size.eq(other_size),
164            _ => false,
165        }
166    }
167}
168
169impl<FsT: Fs> PartialOrd for GotArtifact<FsT>
170where
171    GotArtifact<FsT>: Ord,
172{
173    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
174        Some(self.cmp(other))
175    }
176}
177
178pub trait Key: Clone + Debug + Eq + Hash {
179    fn kinds() -> impl Iterator<Item = &'static str>;
180    fn from_kind_and_digest(kind: &'static str, digest: Sha256Digest) -> Self;
181    fn kind(&self) -> &'static str;
182    fn digest(&self) -> &Sha256Digest;
183}
184
185pub trait GetStrategy {
186    type Getter: Eq + Hash;
187    fn getter_from_job_id(jid: JobId) -> Self::Getter;
188}
189
190/// An entry for a specific [`Key`] in the [`Cache`]'s hash table. There is one of these for every
191/// entry in the per-kind subdirectories of the `sha256` subdirectory of the [`Cache`]'s root
192/// directory.
193enum Entry<GetStrategyT: GetStrategy> {
194    /// The artifact is being gotten by one of the clients.
195    Getting {
196        jobs: Vec<JobId>,
197        getters: HashSet<GetStrategyT::Getter>,
198    },
199
200    /// The artifact has been successfully gotten, and is currently being used by at least one job.
201    /// We reference count this state since there may be multiple jobs using the same artifact.
202    InUse {
203        file_type: FileType,
204        bytes_used: u64,
205        ref_count: NonZeroU32,
206    },
207
208    /// The artifact has been successfully gotten, but no jobs are currently using it. The
209    /// `priority` is provided by [`Cache`] and is used by the [`Heap`] to determine which entry
210    /// should be removed first when freeing up space.
211    InHeap {
212        file_type: FileType,
213        bytes_used: u64,
214        priority: u64,
215        heap_index: HeapIndex,
216    },
217}
218
219/// An implementation of the "newtype" pattern so that we can implement [`HeapDeps`] on a
220/// [`HashMap`].
221#[derive(Deref, DerefMut)]
222struct Map<KeyT: Key, GetStrategyT: GetStrategy>(HashMap<KeyT, Entry<GetStrategyT>>);
223
224impl<KeyT: Key, GetStrategyT: GetStrategy> Default for Map<KeyT, GetStrategyT> {
225    fn default() -> Self {
226        Self(HashMap::default())
227    }
228}
229
230impl<KeyT: Key, GetStrategyT: GetStrategy> HeapDeps for Map<KeyT, GetStrategyT> {
231    type Element = KeyT;
232
233    fn is_element_less_than(&self, lhs: &Self::Element, rhs: &Self::Element) -> bool {
234        let lhs_priority = match self.get(lhs) {
235            Some(Entry::InHeap { priority, .. }) => *priority,
236            _ => panic!("Element should be in heap"),
237        };
238        let rhs_priority = match self.get(rhs) {
239            Some(Entry::InHeap { priority, .. }) => *priority,
240            _ => panic!("Element should be in heap"),
241        };
242        lhs_priority.cmp(&rhs_priority) == Ordering::Less
243    }
244
245    fn update_index(&mut self, elem: &Self::Element, idx: HeapIndex) {
246        match self.get_mut(elem) {
247            Some(Entry::InHeap { heap_index, .. }) => *heap_index = idx,
248            _ => panic!("Element should be in heap"),
249        };
250    }
251}
252
253#[derive(Clone)]
254pub struct TempFileFactory<FsT> {
255    fs: FsT,
256    tmp: RootBuf<TmpDir>,
257}
258
259impl<FsT: Fs> TempFileFactory<FsT> {
260    pub fn temp_dir(&self) -> Result<FsT::TempDir> {
261        Ok(self.fs.temp_dir(&self.tmp)?)
262    }
263
264    pub fn temp_file(&self) -> Result<FsT::TempFile> {
265        Ok(self.fs.temp_file(&self.tmp)?)
266    }
267}
268
269pub struct CacheDir;
270
271pub struct EntryPath;
272
273struct CachedirTagPath;
274struct LockFilePath;
275struct RemovingRoot;
276struct RemovingPath;
277struct TmpDir;
278struct Sha256Dir;
279struct KindDir;
280struct SizeFile;
281
282/// Manage a directory of downloaded, extracted artifacts. Coordinate fetching of these artifacts,
283/// and removing them when they are no longer in use and the amount of space used by the directory
284/// has grown too large.
285pub struct Cache<FsT: Fs, KeyT: Key, GetStrategyT: GetStrategy> {
286    fs: FsT,
287    root: RootBuf<CacheDir>,
288    removing: RootBuf<RemovingRoot>,
289    sha256: RootBuf<Sha256Dir>,
290    entries: Map<KeyT, GetStrategyT>,
291    heap: Heap<Map<KeyT, GetStrategyT>>,
292    next_priority: u64,
293    bytes_used: u64,
294    bytes_used_target: u64,
295    getting: usize,
296    log: Logger,
297    _lock_file: FsT::FileLock,
298}
299
300impl<FsT: Fs, KeyT: Key, GetStrategyT: GetStrategy> Cache<FsT, KeyT, GetStrategyT> {
301    /// Create a new [Cache] rooted at `root`. The directory `root` and all necessary ancestors
302    /// will be created, along with `{root}/removing` and `{root}/{kind}/sha256`. Any pre-existing
303    /// entries in `{root}/removing` and `{root}/{kind}/sha256` will be removed. That implies that
304    /// the [Cache] doesn't currently keep data stored across invocations.
305    ///
306    /// `bytes_used_target` is the goal on-disk size for the cache. The cache will periodically grow
307    /// larger than this size, but then shrink back down to this size. Ideally, the cache would use
308    /// this as a hard upper bound, but that's not how it currently works.
309    pub fn new(
310        fs: FsT,
311        root: RootBuf<CacheDir>,
312        size: CacheSize,
313        log: Logger,
314        log_initial_message_at_info: bool,
315    ) -> Result<(Self, TempFileFactory<FsT>)> {
316        let cachedir_tag = root.join::<CachedirTagPath>(CACHEDIR_TAG);
317        let lock_file_path = root.join::<LockFilePath>(LOCK_FILE);
318        let removing = root.join::<RemovingRoot>(REMOVING);
319        let sha256 = root.join::<Sha256Dir>(SHA256);
320        let tmp = root.join::<TmpDir>("tmp");
321
322        // First, make sure the root directory exists.
323        fs.mkdir_recursively(&root)?;
324
325        // Next, get an exclusive lock on the lock file.
326        let Some(lock_file) = fs.possibly_create_file_and_try_exclusive_lock(&lock_file_path)?
327        else {
328            bail!("lock file {lock_file_path:?} is held by a different process");
329        };
330
331        // Next, see if the `CACHEDIR.TAG` file exists and is correctly formed. We use this to
332        // decide if this is a "new style" cache directory that can be re-used across invocations.
333        // In the future, if we change the layout of the cache directory, we may need a proper
334        // version file, but for now, we can just use `CACHEDIR.TAG`.
335        let preserve_directory_contents = match fs.metadata(&cachedir_tag)? {
336            Some(Metadata {
337                type_: FileType::File,
338                size: CACHEDIR_TAG_CONTENTS_LEN_U64,
339            }) => {
340                let mut contents = [0u8; CACHEDIR_TAG_CONTENTS_LEN];
341                fs.read_file(&cachedir_tag, &mut contents[..])?;
342                contents == CACHEDIR_TAG_CONTENTS
343            }
344            _ => false,
345        };
346
347        let (cache, temp_file_factory) = if preserve_directory_contents {
348            Self::new_preserve_directory_contents(
349                fs, lock_file, root, removing, tmp, sha256, size, log,
350            )
351        } else {
352            Self::new_clear_directory_contents(
353                fs,
354                lock_file,
355                root,
356                cachedir_tag,
357                removing,
358                tmp,
359                sha256,
360                size,
361                log,
362            )
363        }?;
364
365        if log_initial_message_at_info {
366            info!(cache.log, "cache initialized";
367                "entries" => %cache.entries.len(),
368                "bytes_used" => %ByteSize::b(cache.bytes_used),
369                "byte_used_target" => %ByteSize::b(cache.bytes_used_target)
370            );
371        } else {
372            debug!(cache.log, "cache initialized";
373                "entries" => %cache.entries.len(),
374                "bytes_used" => %ByteSize::b(cache.bytes_used),
375                "byte_used_target" => %ByteSize::b(cache.bytes_used_target)
376            );
377        }
378
379        Ok((cache, temp_file_factory))
380    }
381
382    #[allow(clippy::too_many_arguments)]
383    fn new_clear_directory_contents(
384        fs: FsT,
385        lock_file: FsT::FileLock,
386        root: RootBuf<CacheDir>,
387        cachedir_tag: RootBuf<CachedirTagPath>,
388        removing: RootBuf<RemovingRoot>,
389        tmp: RootBuf<TmpDir>,
390        sha256: RootBuf<Sha256Dir>,
391        size: CacheSize,
392        log: Logger,
393    ) -> Result<(Self, TempFileFactory<FsT>)> {
394        // First, make sure the `removing` directory exists and is empty.
395        ensure_removing_directory(&fs, &root, &removing)?;
396
397        // Next, remove everything else in the cache directory.
398        remove_all_from_directory_except(&fs, &removing, &root, [LOCK_FILE, REMOVING])?;
399
400        // Finally, create all of the files all directories that should be there.
401        fs.create_file(&cachedir_tag, &CACHEDIR_TAG_CONTENTS)?;
402        fs.mkdir(&tmp)?;
403        fs.mkdir(&sha256)?;
404        for kind in KeyT::kinds() {
405            fs.mkdir(&kind_dir(&sha256, kind))?;
406        }
407
408        let cache = Cache {
409            fs: fs.clone(),
410            root,
411            removing,
412            sha256,
413            entries: Map::default(),
414            heap: Heap::default(),
415            getting: 0,
416            next_priority: 0,
417            bytes_used: 0,
418            bytes_used_target: size.into(),
419            log,
420            _lock_file: lock_file,
421        };
422        let temp_file_factory = TempFileFactory { fs, tmp };
423        Ok((cache, temp_file_factory))
424    }
425
426    #[allow(clippy::too_many_arguments)]
427    fn new_preserve_directory_contents(
428        fs: FsT,
429        lock_file: FsT::FileLock,
430        root: RootBuf<CacheDir>,
431        removing: RootBuf<RemovingRoot>,
432        tmp: RootBuf<TmpDir>,
433        sha256: RootBuf<Sha256Dir>,
434        size: CacheSize,
435        log: Logger,
436    ) -> Result<(Self, TempFileFactory<FsT>)> {
437        // First, make sure the `removing` directory exists and is empty.
438        ensure_removing_directory(&fs, &root, &removing)?;
439
440        // Next, remove any garbage in the cache directory.
441        remove_all_from_directory_except(
442            &fs,
443            &removing,
444            &root,
445            [LOCK_FILE, REMOVING, SHA256, CACHEDIR_TAG],
446        )?;
447
448        // Next, create the sha256 and tmp directories.
449        fs.mkdir(&tmp)?;
450        fs.mkdir_recursively(&sha256)?;
451
452        // Next, remove any old or garbage sha256 subdirectories and create any missing ones.
453        remove_all_from_directory_except(&fs, &removing, &sha256, KeyT::kinds())?;
454        for kind in KeyT::kinds() {
455            fs.mkdir_recursively(&kind_dir(&sha256, kind))?;
456        }
457
458        let mut entries = Map::<KeyT, GetStrategyT>::default();
459        let mut heap = Heap::<Map<KeyT, GetStrategyT>>::default();
460        let mut next_priority = 0u64;
461        let mut bytes_used = 0u64;
462
463        // Finally, Go through the sha256 subdirs and decide what to do with the contents.
464        for kind in KeyT::kinds() {
465            let kind_dir: RootBuf<KindDir> = sha256.join(kind);
466            let mut directory_sizes = HashMap::<Sha256Digest, (bool, Option<u64>)>::new();
467            for entry in fs.read_dir(&kind_dir)? {
468                let (name, metadata) = entry?;
469                match cache_file_name_type(&fs, &kind_dir, &name, metadata) {
470                    CacheFileNameType::File(digest, size) => {
471                        let key = KeyT::from_kind_and_digest(kind, digest);
472                        entries.insert(
473                            key.clone(),
474                            Entry::InHeap {
475                                file_type: metadata.type_,
476                                bytes_used: metadata.size,
477                                priority: next_priority,
478                                heap_index: Default::default(),
479                            },
480                        );
481                        heap.push(&mut entries, key);
482                        next_priority = next_priority.checked_add(1).unwrap();
483                        bytes_used = bytes_used.checked_add(size).unwrap();
484                    }
485                    CacheFileNameType::Directory(digest) => {
486                        directory_sizes.entry(digest).or_default().0 = true;
487                    }
488                    CacheFileNameType::DirectorySize(digest, size) => {
489                        directory_sizes.entry(digest).or_default().1 = Some(size);
490                    }
491                    CacheFileNameType::Other => {
492                        let path: RootBuf<EntryPath> = kind_dir.join(name);
493                        if metadata.is_directory() {
494                            rmdir_in_background(&fs, &removing, &path)?;
495                        } else {
496                            fs.remove(&path)?;
497                        }
498                    }
499                }
500            }
501
502            for (digest, entry) in directory_sizes {
503                match entry {
504                    (true, Some(size)) => {
505                        let key = KeyT::from_kind_and_digest(kind, digest);
506                        entries.insert(
507                            key.clone(),
508                            Entry::InHeap {
509                                file_type: FileType::Directory,
510                                bytes_used: size,
511                                priority: next_priority,
512                                heap_index: Default::default(),
513                            },
514                        );
515                        heap.push(&mut entries, key);
516                        next_priority = next_priority.checked_add(1).unwrap();
517                        bytes_used = bytes_used.checked_add(size).unwrap();
518                    }
519                    (false, Some(_)) => {
520                        fs.remove(&size_file_name(&cache_file_name(&kind_dir, &digest)))?;
521                    }
522                    (true, None) => {
523                        rmdir_in_background(&fs, &removing, &cache_file_name(&kind_dir, &digest))?;
524                    }
525                    _ => {
526                        unreachable!();
527                    }
528                }
529            }
530        }
531
532        let mut cache = Cache {
533            fs: fs.clone(),
534            root,
535            removing,
536            sha256,
537            entries,
538            heap,
539            getting: 0,
540            next_priority,
541            bytes_used,
542            bytes_used_target: size.into(),
543            log,
544            _lock_file: lock_file,
545        };
546
547        cache.possibly_remove_some_can_error()?;
548        let temp_file_factory = TempFileFactory { fs, tmp };
549        Ok((cache, temp_file_factory))
550    }
551
552    /// Attempt to fetch `artifact` from the cache. See [`GetArtifact`] for the meaning of the
553    /// return values.
554    pub fn get_artifact(&mut self, key: KeyT, jid: JobId) -> GetArtifact {
555        match self.entries.entry(key) {
556            HashEntry::Vacant(entry) => {
557                entry.insert(Entry::Getting {
558                    jobs: vec![jid],
559                    getters: HashSet::from([GetStrategyT::getter_from_job_id(jid)]),
560                });
561                self.getting = self.getting.checked_add(1).unwrap();
562                GetArtifact::Get
563            }
564            HashEntry::Occupied(entry) => {
565                let entry = entry.into_mut();
566                match entry {
567                    Entry::Getting { jobs, getters } => {
568                        jobs.push(jid);
569                        let getter = GetStrategyT::getter_from_job_id(jid);
570                        if getters.insert(getter) {
571                            GetArtifact::Get
572                        } else {
573                            GetArtifact::Wait
574                        }
575                    }
576                    Entry::InUse { ref_count, .. } => {
577                        *ref_count = ref_count.checked_add(1).unwrap();
578                        GetArtifact::Success
579                    }
580                    Entry::InHeap {
581                        file_type,
582                        bytes_used,
583                        heap_index,
584                        ..
585                    } => {
586                        let heap_index = *heap_index;
587                        *entry = Entry::InUse {
588                            file_type: *file_type,
589                            ref_count: NonZeroU32::new(1).unwrap(),
590                            bytes_used: *bytes_used,
591                        };
592                        self.heap.remove(&mut self.entries, heap_index);
593                        GetArtifact::Success
594                    }
595                }
596            }
597        }
598    }
599
600    /// Notify the cache that an artifact fetch has failed. The returned vector lists the jobs that
601    /// are affected and that need to be canceled.
602    pub fn got_artifact_failure(&mut self, key: &KeyT) -> Vec<JobId> {
603        if let Some(Entry::Getting { jobs, .. }) = self.entries.remove(key) {
604            self.getting = self.getting.checked_sub(1).unwrap();
605            jobs
606        } else {
607            vec![]
608        }
609    }
610
611    /// Notify the cache that an artifact fetch has successfully completed. The returned vector
612    /// lists the jobs that are affected, and the path they can use to access the artifact.
613    pub fn got_artifact_success(
614        &mut self,
615        key: &KeyT,
616        artifact: GotArtifact<FsT>,
617    ) -> result::Result<Vec<JobId>, (Error, Vec<JobId>)> {
618        fn update_cache_directory<FsT: Fs>(
619            fs: &FsT,
620            path: &Root<EntryPath>,
621            artifact: GotArtifact<FsT>,
622        ) -> Result<(FileType, u64)> {
623            match artifact {
624                GotArtifact::Directory { source, size } => {
625                    write_size_file(fs, path, size)?;
626                    fs.persist_temp_dir(source, path)?;
627                    Ok((FileType::Directory, size))
628                }
629                GotArtifact::File { source } => {
630                    fs.persist_temp_file(source, path)?;
631                    let metadata = fs.metadata(path)?.unwrap();
632                    assert!(matches!(metadata.type_, FileType::File));
633                    Ok((FileType::File, metadata.size))
634                }
635                GotArtifact::Symlink { target } => {
636                    fs.symlink(&target, path)?;
637                    let metadata = fs.metadata(path)?.unwrap();
638                    assert!(matches!(metadata.type_, FileType::Symlink));
639                    Ok((FileType::Symlink, metadata.size))
640                }
641            }
642        }
643
644        fn dispose_of_artifact<FsT: Fs>(
645            fs: &FsT,
646            removing: &Root<RemovingRoot>,
647            artifact: GotArtifact<FsT>,
648        ) -> result::Result<(), FsT::Error> {
649            match artifact {
650                GotArtifact::Directory { source, .. } => {
651                    let target = get_unused_removing_path(fs, removing)?;
652                    fs.persist_temp_dir(source, &target)?;
653                    fs.rmdir_recursively_on_thread(target.into_path_buf())?;
654                }
655                GotArtifact::File { source } => {
656                    let target = get_unused_removing_path(fs, removing)?;
657                    fs.persist_temp_file(source, &target)?;
658                    fs.remove(&target)?;
659                }
660                GotArtifact::Symlink { .. } => {}
661            }
662            Ok(())
663        }
664
665        fn dispose_of_artifact_or_warn<FsT: Fs>(
666            fs: &FsT,
667            log: &Logger,
668            removing: &Root<RemovingRoot>,
669            artifact: GotArtifact<FsT>,
670        ) {
671            if let Err(err) = dispose_of_artifact(fs, removing, artifact) {
672                warn!(log, "error encountered disposing of unnecessary artifact"; "error" => %err);
673            }
674        }
675
676        let cache_path = self.cache_path(key);
677
678        let Some(entry) = self.entries.get_mut(key) else {
679            debug!(self.log, "cache disposing of added artifact because it is no longer required";
680                "key" => ?key,
681            );
682            dispose_of_artifact_or_warn(&self.fs, &self.log, &self.removing, artifact);
683            return Ok(vec![]);
684        };
685
686        let Entry::Getting { jobs, .. } = entry else {
687            debug!(self.log, "cache disposing of added artifact because it is no longer required";
688                "key" => ?key,
689            );
690            dispose_of_artifact_or_warn(&self.fs, &self.log, &self.removing, artifact);
691            return Ok(vec![]);
692        };
693
694        let (file_type, bytes_used) = match update_cache_directory(&self.fs, &cache_path, artifact)
695        {
696            Err(err) => {
697                return Err((err, self.got_artifact_failure(key)));
698            }
699            Ok((file_type, bytes_used)) => (file_type, bytes_used),
700        };
701
702        // Reference count must be > 0 since we don't allow cancellation of gets.
703        let ref_count = NonZeroU32::new(jobs.len().try_into().unwrap()).unwrap();
704        let jobs = mem::take(jobs);
705        *entry = Entry::InUse {
706            file_type,
707            bytes_used,
708            ref_count,
709        };
710        self.getting = self.getting.checked_sub(1).unwrap();
711        self.bytes_used = self.bytes_used.checked_add(bytes_used).unwrap();
712        debug!(self.log, "cache added artifact";
713            "key" => ?key,
714            "artifact_bytes_used" => %ByteSize::b(bytes_used),
715            "entries" => %(self.entries.len() - self.getting),
716            "file_type" => %file_type,
717            "bytes_used" => %ByteSize::b(self.bytes_used),
718            "byte_used_target" => %ByteSize::b(self.bytes_used_target)
719        );
720        self.possibly_remove_some_ignore_error();
721        Ok(jobs)
722    }
723
724    /// Assuming that a reference count is already is already held for the artifact, increment it
725    /// by one and return the size of the artifact. If no reference count is already held, return
726    /// None.
727    #[must_use]
728    pub fn try_increment_ref_count(&mut self, key: &KeyT) -> Option<u64> {
729        if let Some(Entry::InUse {
730            ref_count,
731            bytes_used,
732            ..
733        }) = self.entries.get_mut(key)
734        {
735            *ref_count = ref_count.checked_add(1).unwrap();
736            Some(*bytes_used)
737        } else {
738            None
739        }
740    }
741
742    /// Assuming that a reference count is already is already held for the artifact, return the
743    /// size of the artifact, otherwise return None.
744    #[must_use]
745    pub fn try_get_size(&mut self, key: &KeyT) -> Option<u64> {
746        if let Some(Entry::InUse { bytes_used, .. }) = self.entries.get_mut(key) {
747            Some(*bytes_used)
748        } else {
749            None
750        }
751    }
752
753    /// Notify the cache that a reference to an artifact is no longer needed.
754    pub fn decrement_ref_count(&mut self, key: &KeyT) {
755        let entry = self
756            .entries
757            .get_mut(key)
758            .expect("got decrement_ref_count for key with no entry");
759        let Entry::InUse {
760            file_type,
761            bytes_used,
762            ref_count,
763        } = entry
764        else {
765            panic!("got decrement_ref_count with for key with zero reference count");
766        };
767        match NonZeroU32::new(ref_count.get() - 1) {
768            Some(new_ref_count) => *ref_count = new_ref_count,
769            None => {
770                *entry = Entry::InHeap {
771                    file_type: *file_type,
772                    bytes_used: *bytes_used,
773                    priority: self.next_priority,
774                    heap_index: HeapIndex::default(),
775                };
776                self.heap.push(&mut self.entries, key.clone());
777                self.next_priority = self.next_priority.checked_add(1).unwrap();
778                self.possibly_remove_some_ignore_error();
779            }
780        }
781    }
782
783    pub fn getter_disconnected(&mut self, getter: GetStrategyT::Getter) {
784        self.entries.retain(|_, e| {
785            let Entry::Getting { jobs, getters } = e else {
786                return true;
787            };
788            jobs.retain(|jid| GetStrategyT::getter_from_job_id(*jid) != getter);
789            getters.remove(&getter);
790            let keep = !jobs.is_empty();
791            if !keep {
792                self.getting = self.getting.checked_sub(1).unwrap();
793            }
794            keep
795        });
796    }
797
798    pub fn root(&self) -> &Root<CacheDir> {
799        &self.root
800    }
801
802    /// Return the directory path for the artifact referenced by `digest`.
803    pub fn cache_path(&self, key: &KeyT) -> RootBuf<EntryPath> {
804        cache_file_name(self.sha256.join(key.kind()).as_root(), key.digest())
805    }
806
807    /// Check to see if the cache is over its goal size, and if so, try to remove the least
808    /// recently used artifacts.
809    fn possibly_remove_some_ignore_error(&mut self) {
810        if let Err(err) = self.possibly_remove_some_can_error() {
811            warn!(self.log, "error removing unused entries from cache to reclaim space"; "error" => %err);
812        }
813    }
814
815    fn possibly_remove_some_can_error(&mut self) -> Result<()> {
816        while self.bytes_used > self.bytes_used_target {
817            let Some(key) = self.heap.pop(&mut self.entries) else {
818                break;
819            };
820            let Some(Entry::InHeap {
821                file_type,
822                bytes_used,
823                ..
824            }) = self.entries.remove(&key)
825            else {
826                panic!("Entry popped off of heap was in unexpected state");
827            };
828            let cache_path = self.cache_path(&key);
829            if file_type == FileType::Directory {
830                rmdir_in_background(&self.fs, &self.removing, &cache_path)?;
831                self.fs.remove(&size_file_name(&cache_path))?;
832            } else {
833                self.fs.remove(&cache_path)?;
834            }
835            self.bytes_used = self.bytes_used.checked_sub(bytes_used).unwrap();
836            debug!(self.log, "cache removed unused artifact";
837                "key" => ?key,
838                "artifact_bytes_used" => %ByteSize::b(bytes_used),
839                "entries" => %(self.entries.len() - self.getting),
840                "bytes_used" => %ByteSize::b(self.bytes_used),
841                "byte_used_target" => %ByteSize::b(self.bytes_used_target)
842            );
843        }
844        Ok(())
845    }
846}
847
848enum CacheFileNameType {
849    Other,
850    File(Sha256Digest, u64),
851    Directory(Sha256Digest),
852    DirectorySize(Sha256Digest, u64),
853}
854
855fn remove_all_from_directory_except<S>(
856    fs: &impl Fs,
857    removing: &Root<RemovingRoot>,
858    dir: &Path,
859    except: impl IntoIterator<Item = S>,
860) -> Result<()>
861where
862    S: ToString,
863{
864    let except = except
865        .into_iter()
866        .map(|e| OsString::from(e.to_string()))
867        .collect::<HashSet<_>>();
868    for entry in fs.read_dir(dir)? {
869        let (entry_name, metadata) = entry?;
870        if !except.contains(&entry_name) {
871            let path = dir.join(entry_name);
872            if metadata.is_directory() {
873                rmdir_in_background(fs, removing, &path)?;
874            } else {
875                fs.remove(&path)?;
876            }
877        }
878    }
879    Ok(())
880}
881
882/// Return an unused path in the provided directory.
883fn get_unused_removing_path<FsT: Fs>(
884    fs: &FsT,
885    removing: &Root<RemovingRoot>,
886) -> result::Result<RootBuf<RemovingPath>, FsT::Error> {
887    loop {
888        let target = removing.join(format!("{:016x}", fs.rand_u64()));
889        if fs.metadata(&target)?.is_none() {
890            break Ok(target);
891        }
892    }
893}
894
895/// Remove all files and directories rooted in `source` in a separate thread.
896fn rmdir_in_background(fs: &impl Fs, removing: &Root<RemovingRoot>, source: &Path) -> Result<()> {
897    let target = get_unused_removing_path(fs, removing)?;
898    fs.rename(source, &target)?;
899    fs.rmdir_recursively_on_thread(target.into_path_buf())?;
900    Ok(())
901}
902
903fn cache_file_name(dir: &Root<KindDir>, digest: &Sha256Digest) -> RootBuf<EntryPath> {
904    dir.join(digest.to_string())
905}
906
907fn size_file_name(base: &Root<EntryPath>) -> RootBuf<SizeFile> {
908    let mut result = base.to_owned().into_path_buf();
909    result.set_extension("size");
910    RootBuf::new(result)
911}
912
913fn read_size_file(
914    fs: &impl Fs,
915    kind_dir: &Root<KindDir>,
916    digest: &Sha256Digest,
917    metadata: Metadata,
918) -> Option<u64> {
919    if metadata.size != 8 {
920        return None;
921    }
922    let mut contents = [0u8; 8];
923    let bytes_read = fs
924        .read_file(
925            &size_file_name(&cache_file_name(kind_dir, digest)),
926            &mut contents,
927        )
928        .ok()?;
929    (bytes_read == 8).then_some(u64::from_be_bytes(contents))
930}
931
932fn cache_file_name_type(
933    fs: &impl Fs,
934    dir: &Root<KindDir>,
935    name: &OsStr,
936    metadata: Metadata,
937) -> CacheFileNameType {
938    if let Some(name) = name.to_str() {
939        if let Ok(digest) = Sha256Digest::from_str(name) {
940            return match metadata.type_ {
941                FileType::File => CacheFileNameType::File(digest, metadata.size),
942                FileType::Directory => CacheFileNameType::Directory(digest),
943                _ => CacheFileNameType::Other,
944            };
945        }
946        if metadata.type_ == FileType::File {
947            if let Some(name) = name.strip_suffix(".size") {
948                if let Ok(digest) = Sha256Digest::from_str(name) {
949                    if let Some(size) = read_size_file(fs, dir, &digest, metadata) {
950                        return CacheFileNameType::DirectorySize(digest, size);
951                    }
952                }
953            }
954        }
955    }
956
957    CacheFileNameType::Other
958}
959
960fn write_size_file(fs: &impl Fs, base: &Root<EntryPath>, size: u64) -> Result<()> {
961    Ok(fs.create_file(&size_file_name(base), &size.to_be_bytes())?)
962}
963
964fn ensure_removing_directory(
965    fs: &impl Fs,
966    root: &Root<CacheDir>,
967    removing: &Root<RemovingRoot>,
968) -> Result<()> {
969    match fs.metadata(removing)? {
970        Some(metadata) if metadata.is_directory() => {
971            let removing_tmp = loop {
972                let target: RootBuf<()> = root.join(format!("removing.{:016x}", fs.rand_u64()));
973                if fs.metadata(&target)?.is_none() {
974                    break target;
975                }
976            };
977            fs.rename(removing, &removing_tmp)?;
978        }
979        Some(_) => {
980            fs.remove(removing)?;
981        }
982        None => {}
983    }
984    fs.mkdir(removing)?;
985    Ok(())
986}
987
988fn kind_dir(sha256: &Root<Sha256Dir>, kind: &'static str) -> RootBuf<KindDir> {
989    sha256.join(kind)
990}
991
992/*  _            _
993 * | |_ ___  ___| |_ ___
994 * | __/ _ \/ __| __/ __|
995 * | ||  __/\__ \ |_\__ \
996 *  \__\___||___/\__|___/
997 *  FIGLET: tests
998 */
999
1000#[cfg(test)]
1001mod tests {
1002    use super::{
1003        fs::{
1004            test::{self, Entry},
1005            Metadata, TempDir as _, TempFile as _,
1006        },
1007        Debug, *,
1008    };
1009    use crate::fs;
1010    use maelstrom_base::digest;
1011    use maelstrom_test::*;
1012    use slog::{o, Discard};
1013    use strum::{EnumString, IntoStaticStr, VariantNames};
1014
1015    #[derive(Clone, Copy, Debug, EnumString, Eq, Hash, IntoStaticStr, PartialEq, VariantNames)]
1016    #[strum(serialize_all = "snake_case")]
1017    enum TestKeyKind {
1018        Apple,
1019        Orange,
1020    }
1021
1022    #[derive(Clone, Debug, Eq, Hash, PartialEq)]
1023    struct TestKey {
1024        kind: TestKeyKind,
1025        digest: Sha256Digest,
1026    }
1027
1028    impl Key for TestKey {
1029        fn kinds() -> impl Iterator<Item = &'static str> {
1030            TestKeyKind::VARIANTS.iter().copied()
1031        }
1032
1033        fn from_kind_and_digest(kind: &'static str, digest: Sha256Digest) -> Self {
1034            Self {
1035                kind: kind.try_into().unwrap(),
1036                digest,
1037            }
1038        }
1039
1040        fn kind(&self) -> &'static str {
1041            self.kind.into()
1042        }
1043
1044        fn digest(&self) -> &Sha256Digest {
1045            &self.digest
1046        }
1047    }
1048
1049    macro_rules! apple {
1050        ($digest:expr) => {
1051            TestKey {
1052                kind: TestKeyKind::Apple,
1053                digest: digest!($digest),
1054            }
1055        };
1056    }
1057
1058    macro_rules! orange {
1059        ($digest:expr) => {
1060            TestKey {
1061                kind: TestKeyKind::Orange,
1062                digest: digest!($digest),
1063            }
1064        };
1065    }
1066
1067    #[derive(Eq, Hash, PartialEq)]
1068    enum TestGetter {
1069        Small,
1070        Large,
1071    }
1072
1073    struct TestGetStrategy;
1074
1075    impl GetStrategy for TestGetStrategy {
1076        type Getter = TestGetter;
1077
1078        fn getter_from_job_id(job_id: JobId) -> Self::Getter {
1079            if job_id.cid.as_u32() < 100 {
1080                TestGetter::Small
1081            } else {
1082                TestGetter::Large
1083            }
1084        }
1085    }
1086
1087    struct Fixture {
1088        fs: test::Fs,
1089        cache: Cache<test::Fs, TestKey, TestGetStrategy>,
1090        temp_file_factory: TempFileFactory<test::Fs>,
1091    }
1092
1093    impl Fixture {
1094        fn new(bytes_used_target: u64, root: Entry) -> Self {
1095            let fs = test::Fs::new(root);
1096            let (cache, temp_file_factory) = Cache::new(
1097                fs.clone(),
1098                "/z".parse().unwrap(),
1099                ByteSize::b(bytes_used_target).into(),
1100                Logger::root(Discard, o!()),
1101                false,
1102            )
1103            .unwrap();
1104            Fixture {
1105                fs,
1106                cache,
1107                temp_file_factory,
1108            }
1109        }
1110
1111        #[track_caller]
1112        fn assert_fs(&self, expected_fs: Entry) {
1113            self.fs.assert_tree(expected_fs);
1114        }
1115
1116        #[track_caller]
1117        fn assert_fs_entry(&self, path: &str, expected_entry: Entry) {
1118            self.fs.assert_entry(Path::new(path), expected_entry);
1119        }
1120
1121        #[track_caller]
1122        fn assert_pending_recursive_rmdirs<const N: usize>(&self, paths: [&str; N]) {
1123            self.fs.assert_recursive_rmdirs(HashSet::from_iter(
1124                paths.into_iter().map(ToString::to_string),
1125            ))
1126        }
1127
1128        #[track_caller]
1129        fn assert_file_exists(&self, key: TestKey, expected_metadata: Metadata) {
1130            let cache_path = self.cache.cache_path(&key);
1131            assert_eq!(
1132                self.fs.metadata(&cache_path).unwrap().unwrap(),
1133                expected_metadata
1134            );
1135        }
1136
1137        #[track_caller]
1138        fn assert_file_does_not_exist(&self, key: TestKey) {
1139            let cache_path = self.cache.cache_path(&key);
1140            assert!(self.fs.metadata(&cache_path).unwrap().is_none());
1141        }
1142
1143        #[track_caller]
1144        fn assert_bytes_used(&self, expected_bytes_used: u64) {
1145            assert_eq!(self.cache.bytes_used, expected_bytes_used);
1146        }
1147
1148        #[track_caller]
1149        fn get_artifact(&mut self, key: TestKey, jid: JobId, expected: GetArtifact) {
1150            let result = self.cache.get_artifact(key, jid);
1151            assert_eq!(result, expected);
1152        }
1153
1154        #[track_caller]
1155        fn got_artifact_success_directory(
1156            &mut self,
1157            key: TestKey,
1158            contents: Entry,
1159            size: u64,
1160            expected: Vec<JobId>,
1161        ) {
1162            let source = self.temp_file_factory.temp_dir().unwrap();
1163            self.fs.graft(source.path(), contents);
1164            self.got_artifact_success(key, GotArtifact::directory(source, size), expected)
1165        }
1166
1167        #[track_caller]
1168        fn got_artifact_success_file(
1169            &mut self,
1170            key: TestKey,
1171            contents: &[u8],
1172            expected: Vec<JobId>,
1173        ) {
1174            let source = self.temp_file_factory.temp_file().unwrap();
1175            self.fs.graft(source.path(), Entry::file(contents));
1176            self.got_artifact_success(key, GotArtifact::file(source), expected)
1177        }
1178
1179        #[track_caller]
1180        fn got_artifact_success_symlink(
1181            &mut self,
1182            key: TestKey,
1183            target: &str,
1184            expected: Vec<JobId>,
1185        ) {
1186            let target = target.into();
1187            self.got_artifact_success(key, GotArtifact::symlink(target), expected)
1188        }
1189
1190        #[track_caller]
1191        fn got_artifact_success(
1192            &mut self,
1193            key: TestKey,
1194            artifact: GotArtifact<test::Fs>,
1195            expected: Vec<JobId>,
1196        ) {
1197            let result = self.cache.got_artifact_success(&key, artifact).unwrap();
1198            assert_eq!(result, expected);
1199        }
1200
1201        #[track_caller]
1202        fn got_artifact_failure(&mut self, key: TestKey, expected: Vec<JobId>) {
1203            let result = self.cache.got_artifact_failure(&key);
1204            assert_eq!(result, expected);
1205        }
1206
1207        fn try_increment_ref_count(&mut self, key: TestKey, expected: Option<u64>) {
1208            assert_eq!(self.cache.try_increment_ref_count(&key), expected);
1209        }
1210
1211        fn decrement_ref_count(&mut self, key: TestKey) {
1212            self.cache.decrement_ref_count(&key);
1213        }
1214    }
1215
1216    #[test]
1217    fn success_flow_with_single_getter() {
1218        let mut fixture = Fixture::new(10, fs! {});
1219        fixture.assert_bytes_used(0);
1220
1221        fixture.get_artifact(apple!(42), jid!(1), GetArtifact::Get);
1222        fixture.get_artifact(apple!(42), jid!(2), GetArtifact::Wait);
1223        fixture.get_artifact(apple!(42), jid!(3), GetArtifact::Wait);
1224        fixture.assert_file_does_not_exist(apple!(42));
1225        fixture.assert_bytes_used(0);
1226
1227        fixture.got_artifact_success_file(apple!(42), b"a", vec![jid!(1), jid!(2), jid!(3)]);
1228        fixture.assert_file_exists(apple!(42), Metadata::file(1));
1229        fixture.assert_bytes_used(1);
1230
1231        fixture.get_artifact(apple!(42), jid!(4), GetArtifact::Success);
1232        fixture.get_artifact(apple!(42), jid!(5), GetArtifact::Success);
1233        fixture.assert_file_exists(apple!(42), Metadata::file(1));
1234        fixture.assert_bytes_used(1);
1235    }
1236
1237    #[test]
1238    fn success_flow_with_multiple_getters() {
1239        let mut fixture = Fixture::new(10, fs! {});
1240        fixture.assert_bytes_used(0);
1241
1242        fixture.get_artifact(apple!(42), jid!(1), GetArtifact::Get);
1243        fixture.get_artifact(apple!(42), jid!(2), GetArtifact::Wait);
1244        fixture.get_artifact(apple!(42), jid!(100), GetArtifact::Get);
1245        fixture.get_artifact(apple!(42), jid!(101), GetArtifact::Wait);
1246        fixture.assert_file_does_not_exist(apple!(42));
1247        fixture.assert_bytes_used(0);
1248
1249        fixture.got_artifact_success_file(
1250            apple!(42),
1251            b"a",
1252            vec![jid!(1), jid!(2), jid!(100), jid!(101)],
1253        );
1254        fixture.assert_file_exists(apple!(42), Metadata::file(1));
1255        fixture.assert_bytes_used(1);
1256
1257        fixture.get_artifact(apple!(42), jid!(4), GetArtifact::Success);
1258        fixture.get_artifact(apple!(42), jid!(5), GetArtifact::Success);
1259        fixture.assert_file_exists(apple!(42), Metadata::file(1));
1260        fixture.assert_bytes_used(1);
1261    }
1262
1263    #[test]
1264    fn success_flow_with_files() {
1265        let mut fixture = Fixture::new(1, fs! {});
1266        fixture.assert_bytes_used(0);
1267
1268        fixture.get_artifact(apple!(1), jid!(1), GetArtifact::Get);
1269        fixture.get_artifact(apple!(1), jid!(2), GetArtifact::Wait);
1270        fixture.assert_bytes_used(0);
1271        fixture.assert_fs_entry("/z/sha256", fs! { apple {}, orange {} });
1272        fixture.assert_pending_recursive_rmdirs([]);
1273
1274        fixture.got_artifact_success_file(apple!(1), b"123456", vec![jid!(1), jid!(2)]);
1275        fixture.assert_bytes_used(6);
1276        fixture.assert_fs_entry(
1277            "/z/sha256",
1278            fs! {
1279                apple {
1280                    "0000000000000000000000000000000000000000000000000000000000000001"(b"123456"),
1281                },
1282                orange {},
1283            },
1284        );
1285        fixture.assert_pending_recursive_rmdirs([]);
1286
1287        fixture.decrement_ref_count(apple!(1));
1288        fixture.decrement_ref_count(apple!(1));
1289        fixture.assert_bytes_used(0);
1290        fixture.assert_fs_entry("/z/sha256", fs! { apple {}, orange {} });
1291        fixture.assert_pending_recursive_rmdirs([]);
1292    }
1293
1294    #[test]
1295    fn success_flow_with_symlinks() {
1296        let mut fixture = Fixture::new(1, fs! {});
1297        fixture.assert_bytes_used(0);
1298
1299        fixture.get_artifact(apple!(1), jid!(1), GetArtifact::Get);
1300        fixture.get_artifact(apple!(1), jid!(2), GetArtifact::Wait);
1301        fixture.assert_bytes_used(0);
1302        fixture.assert_fs_entry("/z/sha256", fs! { apple {}, orange {} });
1303        fixture.assert_pending_recursive_rmdirs([]);
1304
1305        fixture.got_artifact_success_symlink(apple!(1), "target", vec![jid!(1), jid!(2)]);
1306        fixture.assert_bytes_used(6);
1307        fixture.assert_fs_entry(
1308            "/z/sha256",
1309            fs! {
1310                apple {
1311                    "0000000000000000000000000000000000000000000000000000000000000001" -> "target",
1312                },
1313                orange {},
1314            },
1315        );
1316        fixture.assert_pending_recursive_rmdirs([]);
1317
1318        fixture.decrement_ref_count(apple!(1));
1319        fixture.decrement_ref_count(apple!(1));
1320        fixture.assert_bytes_used(0);
1321        fixture.assert_fs_entry("/z/sha256", fs! { apple {}, orange {} });
1322        fixture.assert_pending_recursive_rmdirs([]);
1323    }
1324
1325    #[test]
1326    fn success_flow_with_directories() {
1327        let mut fixture = Fixture::new(1, fs! {});
1328        fixture.assert_bytes_used(0);
1329
1330        fixture.get_artifact(apple!(1), jid!(1), GetArtifact::Get);
1331        fixture.get_artifact(apple!(1), jid!(2), GetArtifact::Wait);
1332        fixture.assert_bytes_used(0);
1333        fixture.assert_fs_entry("/z/sha256", fs! { apple {}, orange {} });
1334        fixture.assert_pending_recursive_rmdirs([]);
1335
1336        fixture.got_artifact_success_directory(
1337            apple!(1),
1338            fs! {
1339                foo(b"bar"),
1340                symlink -> "foo",
1341            },
1342            6,
1343            vec![jid!(1), jid!(2)],
1344        );
1345        fixture.assert_bytes_used(6);
1346        fixture.assert_fs_entry(
1347            "/z/sha256",
1348            fs! {
1349                apple {
1350                    "0000000000000000000000000000000000000000000000000000000000000001" {
1351                        foo(b"bar"),
1352                        symlink -> "foo",
1353                    },
1354                    "0000000000000000000000000000000000000000000000000000000000000001.size"(&6u64.to_be_bytes()),
1355                },
1356                orange {},
1357            },
1358        );
1359        fixture.assert_pending_recursive_rmdirs([]);
1360
1361        fixture.decrement_ref_count(apple!(1));
1362        fixture.decrement_ref_count(apple!(1));
1363        fixture.assert_bytes_used(0);
1364        fixture.assert_fs_entry("/z/sha256", fs! { apple {}, orange {} });
1365        fixture.assert_pending_recursive_rmdirs(["/z/removing/0000000000000001"]);
1366    }
1367
1368    #[test]
1369    fn success_flow_with_failure_persisting() {
1370        let mut fixture = Fixture::new(1, fs! {});
1371        fixture.assert_bytes_used(0);
1372
1373        fixture.get_artifact(apple!(1), jid!(1), GetArtifact::Get);
1374        fixture.get_artifact(apple!(1), jid!(2), GetArtifact::Wait);
1375        fixture.assert_bytes_used(0);
1376        fixture.assert_fs_entry("/z/sha256", fs! { apple {}, orange {} });
1377        fixture.assert_pending_recursive_rmdirs([]);
1378
1379        let source = fixture.temp_file_factory.temp_dir().unwrap();
1380        fixture.fs.graft(source.path(), fs! { foo(b"bar") });
1381        fixture.fs.set_failure(true);
1382        let (err, jids) = fixture
1383            .cache
1384            .got_artifact_success(&apple!(1), GotArtifact::directory(source, 6))
1385            .unwrap_err();
1386        assert_eq!(jids, vec![jid!(1), jid!(2)]);
1387        assert_eq!(err.to_string(), "Test");
1388
1389        fixture.assert_bytes_used(0);
1390        fixture.assert_fs_entry(
1391            "/z/sha256",
1392            fs! {
1393                apple {},
1394                orange {},
1395            },
1396        );
1397        fixture.assert_pending_recursive_rmdirs([]);
1398    }
1399
1400    #[test]
1401    fn artifact_stays_in_cache() {
1402        let mut fixture = Fixture::new(10, fs! {});
1403
1404        fixture.get_artifact(apple!(42), jid!(1), GetArtifact::Get);
1405        fixture.got_artifact_success_file(apple!(42), b"a", vec![jid!(1)]);
1406        fixture.decrement_ref_count(apple!(42));
1407
1408        fixture.assert_file_exists(apple!(42), Metadata::file(1));
1409        fixture.assert_bytes_used(1);
1410
1411        fixture.get_artifact(apple!(42), jid!(1), GetArtifact::Success);
1412        fixture.assert_file_exists(apple!(42), Metadata::file(1));
1413        fixture.assert_bytes_used(1);
1414    }
1415
1416    #[test]
1417    fn large_artifact_does_not_stay_in_cache() {
1418        let mut fixture = Fixture::new(1, fs! {});
1419
1420        fixture.get_artifact(apple!(42), jid!(1), GetArtifact::Get);
1421        fixture.got_artifact_success_file(apple!(42), b"123456789", vec![jid!(1)]);
1422        fixture.assert_bytes_used(9);
1423        fixture.decrement_ref_count(apple!(42));
1424
1425        fixture.assert_file_does_not_exist(apple!(42));
1426        fixture.assert_bytes_used(0);
1427        fixture.get_artifact(apple!(42), jid!(1), GetArtifact::Get);
1428    }
1429
1430    #[test]
1431    fn large_artifact_stays_in_cache_while_referenced() {
1432        let mut fixture = Fixture::new(1, fs! {});
1433
1434        fixture.get_artifact(apple!(42), jid!(1), GetArtifact::Get);
1435        fixture.got_artifact_success_file(apple!(42), b"123456789", vec![jid!(1)]);
1436
1437        fixture.get_artifact(apple!(42), jid!(1), GetArtifact::Success);
1438
1439        fixture.decrement_ref_count(apple!(42));
1440        fixture.assert_file_exists(apple!(42), Metadata::file(9));
1441        fixture.assert_bytes_used(9);
1442
1443        fixture.decrement_ref_count(apple!(42));
1444        fixture.assert_file_does_not_exist(apple!(42));
1445        fixture.assert_bytes_used(0);
1446        fixture.get_artifact(apple!(42), jid!(1), GetArtifact::Get);
1447    }
1448
1449    #[test]
1450    fn different_key_kinds_are_independent() {
1451        let mut fixture = Fixture::new(1, fs! {});
1452
1453        fixture.get_artifact(apple!(42), jid!(1), GetArtifact::Get);
1454        fixture.get_artifact(apple!(42), jid!(2), GetArtifact::Wait);
1455        fixture.get_artifact(apple!(42), jid!(3), GetArtifact::Wait);
1456        fixture.assert_file_does_not_exist(apple!(42));
1457        fixture.assert_bytes_used(0);
1458
1459        fixture.get_artifact(orange!(42), jid!(4), GetArtifact::Get);
1460        fixture.get_artifact(orange!(42), jid!(5), GetArtifact::Wait);
1461        fixture.assert_file_does_not_exist(orange!(42));
1462        fixture.assert_bytes_used(0);
1463
1464        fixture.got_artifact_success_file(apple!(42), b"abc", vec![jid!(1), jid!(2), jid!(3)]);
1465        fixture.assert_file_exists(apple!(42), Metadata::file(3));
1466        fixture.assert_file_does_not_exist(orange!(42));
1467        fixture.assert_bytes_used(3);
1468
1469        fixture.get_artifact(orange!(42), jid!(6), GetArtifact::Wait);
1470
1471        fixture.got_artifact_success_file(orange!(42), b"1234", vec![jid!(4), jid!(5), jid!(6)]);
1472        fixture.assert_file_exists(apple!(42), Metadata::file(3));
1473        fixture.assert_file_exists(orange!(42), Metadata::file(4));
1474        fixture.assert_bytes_used(7);
1475
1476        fixture.decrement_ref_count(apple!(42));
1477        fixture.decrement_ref_count(apple!(42));
1478        fixture.decrement_ref_count(apple!(42));
1479        fixture.assert_file_does_not_exist(apple!(42));
1480        fixture.assert_file_exists(orange!(42), Metadata::file(4));
1481        fixture.assert_bytes_used(4);
1482
1483        fixture.decrement_ref_count(orange!(42));
1484        fixture.decrement_ref_count(orange!(42));
1485        fixture.decrement_ref_count(orange!(42));
1486        fixture.assert_file_does_not_exist(apple!(42));
1487        fixture.assert_file_does_not_exist(orange!(42));
1488        fixture.assert_bytes_used(0);
1489    }
1490
1491    #[test]
1492    fn entries_are_removed_in_lru_order() {
1493        let mut fixture = Fixture::new(3, fs! {});
1494
1495        fixture.get_artifact(apple!(1), jid!(1), GetArtifact::Get);
1496        fixture.get_artifact(orange!(2), jid!(2), GetArtifact::Get);
1497        fixture.get_artifact(apple!(3), jid!(3), GetArtifact::Get);
1498        fixture.get_artifact(orange!(4), jid!(4), GetArtifact::Get);
1499        fixture.assert_bytes_used(0);
1500
1501        fixture.got_artifact_success_file(apple!(1), b"a", vec![jid!(1)]);
1502        fixture.got_artifact_success_file(orange!(2), b"b", vec![jid!(2)]);
1503        fixture.got_artifact_success_file(apple!(3), b"c", vec![jid!(3)]);
1504        fixture.got_artifact_success_file(orange!(4), b"d", vec![jid!(4)]);
1505
1506        fixture.assert_file_exists(apple!(1), Metadata::file(1));
1507        fixture.assert_file_exists(orange!(2), Metadata::file(1));
1508        fixture.assert_file_exists(apple!(3), Metadata::file(1));
1509        fixture.assert_file_exists(orange!(4), Metadata::file(1));
1510        fixture.assert_bytes_used(4);
1511
1512        fixture.decrement_ref_count(orange!(4));
1513
1514        fixture.assert_file_exists(apple!(1), Metadata::file(1));
1515        fixture.assert_file_exists(orange!(2), Metadata::file(1));
1516        fixture.assert_file_exists(apple!(3), Metadata::file(1));
1517        fixture.assert_file_does_not_exist(orange!(4));
1518        fixture.assert_bytes_used(3);
1519        fixture.get_artifact(orange!(4), jid!(14), GetArtifact::Get);
1520
1521        fixture.decrement_ref_count(apple!(3));
1522        fixture.decrement_ref_count(orange!(2));
1523        fixture.decrement_ref_count(apple!(1));
1524
1525        fixture.assert_file_exists(apple!(1), Metadata::file(1));
1526        fixture.assert_file_exists(orange!(2), Metadata::file(1));
1527        fixture.assert_file_exists(apple!(3), Metadata::file(1));
1528        fixture.assert_file_does_not_exist(orange!(4));
1529        fixture.assert_bytes_used(3);
1530
1531        fixture.get_artifact(apple!(5), jid!(5), GetArtifact::Get);
1532        fixture.got_artifact_success_file(apple!(5), b"e", vec![jid!(5)]);
1533
1534        fixture.assert_file_exists(apple!(1), Metadata::file(1));
1535        fixture.assert_file_exists(orange!(2), Metadata::file(1));
1536        fixture.assert_file_does_not_exist(apple!(3));
1537        fixture.assert_file_does_not_exist(orange!(4));
1538        fixture.assert_file_exists(apple!(5), Metadata::file(1));
1539        fixture.assert_bytes_used(3);
1540        fixture.get_artifact(apple!(3), jid!(13), GetArtifact::Get);
1541
1542        fixture.get_artifact(orange!(2), jid!(12), GetArtifact::Success);
1543        fixture.decrement_ref_count(orange!(2));
1544
1545        fixture.get_artifact(orange!(6), jid!(6), GetArtifact::Get);
1546        fixture.got_artifact_success_file(orange!(6), b"f", vec![jid!(6)]);
1547
1548        fixture.assert_file_does_not_exist(apple!(1));
1549        fixture.assert_file_exists(orange!(2), Metadata::file(1));
1550        fixture.assert_file_does_not_exist(apple!(3));
1551        fixture.assert_file_does_not_exist(orange!(4));
1552        fixture.assert_file_exists(apple!(5), Metadata::file(1));
1553        fixture.assert_file_exists(orange!(6), Metadata::file(1));
1554        fixture.assert_bytes_used(3);
1555        fixture.get_artifact(apple!(1), jid!(11), GetArtifact::Get);
1556    }
1557
1558    #[test]
1559    fn failure_flow() {
1560        let mut fixture = Fixture::new(10, fs! {});
1561        fixture.assert_bytes_used(0);
1562
1563        fixture.get_artifact(apple!(42), jid!(1), GetArtifact::Get);
1564        fixture.get_artifact(apple!(42), jid!(2), GetArtifact::Wait);
1565        fixture.get_artifact(apple!(42), jid!(3), GetArtifact::Wait);
1566        fixture.assert_file_does_not_exist(apple!(42));
1567        fixture.assert_bytes_used(0);
1568
1569        fixture.got_artifact_failure(apple!(42), vec![jid!(1), jid!(2), jid!(3)]);
1570        fixture.assert_file_does_not_exist(apple!(42));
1571        fixture.assert_bytes_used(0);
1572
1573        fixture.get_artifact(apple!(42), jid!(4), GetArtifact::Get);
1574        fixture.get_artifact(apple!(42), jid!(5), GetArtifact::Wait);
1575        fixture.get_artifact(apple!(42), jid!(6), GetArtifact::Wait);
1576
1577        fixture.got_artifact_success_file(apple!(42), b"a", vec![jid!(4), jid!(5), jid!(6)]);
1578        fixture.assert_file_exists(apple!(42), Metadata::file(1));
1579        fixture.assert_bytes_used(1);
1580    }
1581
1582    #[test]
1583    fn try_increment_ref_count() {
1584        let mut fixture = Fixture::new(10, fs! {});
1585
1586        fixture.get_artifact(apple!(2), jid!(1), GetArtifact::Get);
1587
1588        fixture.get_artifact(apple!(3), jid!(1), GetArtifact::Get);
1589        fixture.got_artifact_success_file(apple!(3), b"abc", vec![jid!(1)]);
1590        fixture.decrement_ref_count(apple!(3));
1591
1592        fixture.get_artifact(apple!(4), jid!(1), GetArtifact::Get);
1593        fixture.got_artifact_success_file(apple!(4), b"def", vec![jid!(1)]);
1594
1595        fixture.try_increment_ref_count(apple!(1), None);
1596        fixture.try_increment_ref_count(apple!(2), None);
1597        fixture.try_increment_ref_count(apple!(3), None);
1598        fixture.try_increment_ref_count(apple!(4), Some(3));
1599
1600        fixture.get_artifact(apple!(5), jid!(1), GetArtifact::Get);
1601        fixture.got_artifact_success_file(apple!(5), b"0123456789", vec![jid!(1)]);
1602        fixture.assert_bytes_used(13);
1603
1604        fixture.decrement_ref_count(apple!(4));
1605        fixture.assert_bytes_used(13);
1606
1607        fixture.decrement_ref_count(apple!(4));
1608        fixture.assert_bytes_used(10);
1609    }
1610
1611    #[test]
1612    fn got_artifact_failure_no_entry() {
1613        let mut fixture = Fixture::new(1, fs! {});
1614
1615        fixture.got_artifact_failure(apple!(1), vec![]);
1616    }
1617
1618    #[test]
1619    fn got_artifact_failure_in_use() {
1620        let mut fixture = Fixture::new(1, fs! {});
1621
1622        fixture.get_artifact(apple!(1), jid!(1), GetArtifact::Get);
1623        fixture.got_artifact_success_file(apple!(1), b"abc", vec![jid!(1)]);
1624
1625        fixture.got_artifact_failure(apple!(1), vec![]);
1626    }
1627
1628    #[test]
1629    fn got_artifact_failure_in_cache() {
1630        let mut fixture = Fixture::new(100, fs! {});
1631
1632        fixture.get_artifact(apple!(1), jid!(1), GetArtifact::Get);
1633        fixture.got_artifact_success_file(apple!(1), b"abc", vec![jid!(1)]);
1634        fixture.decrement_ref_count(apple!(1));
1635
1636        fixture.got_artifact_failure(apple!(1), vec![]);
1637    }
1638
1639    #[test]
1640    fn got_artifact_success_directory_no_entry() {
1641        let mut fixture = Fixture::new(10, fs! {});
1642
1643        fixture.got_artifact_success_directory(apple!(1), fs! { foo(b"foo") }, 1, vec![]);
1644        fixture.assert_fs(fs! {
1645            z {
1646                "CACHEDIR.TAG"(&CACHEDIR_TAG_CONTENTS),
1647                "lock"(b""),
1648                removing {
1649                    "0000000000000001" {
1650                        foo(b"foo"),
1651                    },
1652                },
1653                sha256 {
1654                    apple {},
1655                    orange {},
1656                },
1657                tmp {},
1658            },
1659        });
1660        fixture.assert_pending_recursive_rmdirs(["/z/removing/0000000000000001"]);
1661    }
1662
1663    #[test]
1664    fn got_artifact_success_file_no_entry() {
1665        let mut fixture = Fixture::new(1, fs! {});
1666
1667        fixture.got_artifact_success_file(apple!(1), b"abc", vec![]);
1668        fixture.assert_fs(fs! {
1669            z {
1670                "CACHEDIR.TAG"(&CACHEDIR_TAG_CONTENTS),
1671                "lock"(b""),
1672                removing {
1673                },
1674                sha256 {
1675                    apple {},
1676                    orange {},
1677                },
1678                tmp {},
1679            },
1680        });
1681        fixture.assert_pending_recursive_rmdirs([]);
1682    }
1683
1684    #[test]
1685    fn got_artifact_success_symlink_no_entry() {
1686        let mut fixture = Fixture::new(1, fs! {});
1687
1688        fixture.got_artifact_success_symlink(apple!(1), "/target", vec![]);
1689        fixture.assert_fs(fs! {
1690            z {
1691                "CACHEDIR.TAG"(&CACHEDIR_TAG_CONTENTS),
1692                "lock"(b""),
1693                removing {
1694                },
1695                sha256 {
1696                    apple {},
1697                    orange {},
1698                },
1699                tmp {},
1700            },
1701        });
1702        fixture.assert_pending_recursive_rmdirs([]);
1703    }
1704
1705    #[test]
1706    fn got_artifact_success_directory_in_use() {
1707        let mut fixture = Fixture::new(10, fs! {});
1708
1709        fixture.get_artifact(apple!(1), jid!(1), GetArtifact::Get);
1710        fixture.get_artifact(apple!(1), jid!(100), GetArtifact::Get);
1711        fixture.got_artifact_success_directory(
1712            apple!(1),
1713            fs! { foo(b"foo") },
1714            1,
1715            vec![jid!(1), jid!(100)],
1716        );
1717        fixture.got_artifact_success_directory(apple!(1), fs! { foo(b"bar") }, 1, vec![]);
1718        fixture.assert_fs(fs! {
1719            z {
1720                "CACHEDIR.TAG"(&CACHEDIR_TAG_CONTENTS),
1721                "lock"(b""),
1722                removing {
1723                    "0000000000000001" {
1724                        foo(b"bar"),
1725                    },
1726                },
1727                sha256 {
1728                    apple {
1729                        "0000000000000000000000000000000000000000000000000000000000000001" {
1730                            foo(b"foo"),
1731                        },
1732                        "0000000000000000000000000000000000000000000000000000000000000001.size"(b"\0\0\0\0\0\0\0\x01"),
1733                    },
1734                    orange {},
1735                },
1736                tmp {},
1737            },
1738        });
1739        fixture.assert_pending_recursive_rmdirs(["/z/removing/0000000000000001"]);
1740    }
1741
1742    #[test]
1743    fn got_artifact_success_file_in_use() {
1744        let mut fixture = Fixture::new(1, fs! {});
1745
1746        fixture.get_artifact(apple!(1), jid!(1), GetArtifact::Get);
1747        fixture.get_artifact(apple!(1), jid!(100), GetArtifact::Get);
1748        fixture.got_artifact_success_file(apple!(1), b"foo", vec![jid!(1), jid!(100)]);
1749        fixture.got_artifact_success_file(apple!(1), b"bar", vec![]);
1750        fixture.assert_fs(fs! {
1751            z {
1752                "CACHEDIR.TAG"(&CACHEDIR_TAG_CONTENTS),
1753                "lock"(b""),
1754                removing {
1755                },
1756                sha256 {
1757                    apple {
1758                        "0000000000000000000000000000000000000000000000000000000000000001"(b"foo"),
1759                    },
1760                    orange {},
1761                },
1762                tmp {},
1763            },
1764        });
1765        fixture.assert_pending_recursive_rmdirs([]);
1766    }
1767
1768    #[test]
1769    fn got_artifact_success_symlink_in_use() {
1770        let mut fixture = Fixture::new(1, fs! {});
1771
1772        fixture.get_artifact(apple!(1), jid!(1), GetArtifact::Get);
1773        fixture.get_artifact(apple!(1), jid!(100), GetArtifact::Get);
1774        fixture.got_artifact_success_symlink(apple!(1), "/foo", vec![jid!(1), jid!(100)]);
1775        fixture.got_artifact_success_symlink(apple!(1), "/bar", vec![]);
1776        fixture.assert_fs(fs! {
1777            z {
1778                "CACHEDIR.TAG"(&CACHEDIR_TAG_CONTENTS),
1779                "lock"(b""),
1780                removing {
1781                },
1782                sha256 {
1783                    apple {
1784                        "0000000000000000000000000000000000000000000000000000000000000001" -> "/foo",
1785                    },
1786                    orange {},
1787                },
1788                tmp {},
1789            },
1790        });
1791        fixture.assert_pending_recursive_rmdirs([]);
1792    }
1793
1794    #[test]
1795    fn got_artifact_success_directory_in_cache() {
1796        let mut fixture = Fixture::new(10, fs! {});
1797
1798        fixture.get_artifact(apple!(1), jid!(1), GetArtifact::Get);
1799        fixture.get_artifact(apple!(1), jid!(100), GetArtifact::Get);
1800        fixture.got_artifact_success_directory(
1801            apple!(1),
1802            fs! { foo(b"foo") },
1803            1,
1804            vec![jid!(1), jid!(100)],
1805        );
1806        fixture.decrement_ref_count(apple!(1));
1807        fixture.decrement_ref_count(apple!(1));
1808        fixture.got_artifact_success_directory(apple!(1), fs! { foo(b"bar") }, 1, vec![]);
1809        fixture.assert_fs(fs! {
1810            z {
1811                "CACHEDIR.TAG"(&CACHEDIR_TAG_CONTENTS),
1812                "lock"(b""),
1813                removing {
1814                    "0000000000000001" {
1815                        foo(b"bar"),
1816                    },
1817                },
1818                sha256 {
1819                    apple {
1820                        "0000000000000000000000000000000000000000000000000000000000000001" {
1821                            foo(b"foo"),
1822                        },
1823                        "0000000000000000000000000000000000000000000000000000000000000001.size"(b"\0\0\0\0\0\0\0\x01"),
1824                    },
1825                    orange {},
1826                },
1827                tmp {},
1828            },
1829        });
1830        fixture.assert_pending_recursive_rmdirs(["/z/removing/0000000000000001"]);
1831    }
1832
1833    #[test]
1834    fn got_artifact_success_file_in_cache() {
1835        let mut fixture = Fixture::new(10, fs! {});
1836
1837        fixture.get_artifact(apple!(1), jid!(1), GetArtifact::Get);
1838        fixture.get_artifact(apple!(1), jid!(100), GetArtifact::Get);
1839        fixture.got_artifact_success_file(apple!(1), b"foo", vec![jid!(1), jid!(100)]);
1840        fixture.decrement_ref_count(apple!(1));
1841        fixture.decrement_ref_count(apple!(1));
1842        fixture.got_artifact_success_file(apple!(1), b"bar", vec![]);
1843        fixture.assert_fs(fs! {
1844            z {
1845                "CACHEDIR.TAG"(&CACHEDIR_TAG_CONTENTS),
1846                "lock"(b""),
1847                removing {
1848                },
1849                sha256 {
1850                    apple {
1851                        "0000000000000000000000000000000000000000000000000000000000000001"(b"foo"),
1852                    },
1853                    orange {},
1854                },
1855                tmp {},
1856            },
1857        });
1858        fixture.assert_pending_recursive_rmdirs([]);
1859    }
1860
1861    #[test]
1862    fn got_artifact_success_symlink_in_cache() {
1863        let mut fixture = Fixture::new(10, fs! {});
1864
1865        fixture.get_artifact(apple!(1), jid!(1), GetArtifact::Get);
1866        fixture.get_artifact(apple!(1), jid!(100), GetArtifact::Get);
1867        fixture.got_artifact_success_symlink(apple!(1), "/foo", vec![jid!(1), jid!(100)]);
1868        fixture.decrement_ref_count(apple!(1));
1869        fixture.decrement_ref_count(apple!(1));
1870        fixture.got_artifact_success_symlink(apple!(1), "/bar", vec![]);
1871        fixture.assert_fs(fs! {
1872            z {
1873                "CACHEDIR.TAG"(&CACHEDIR_TAG_CONTENTS),
1874                "lock"(b""),
1875                removing {
1876                },
1877                sha256 {
1878                    apple {
1879                        "0000000000000000000000000000000000000000000000000000000000000001" -> "/foo",
1880                    },
1881                    orange {},
1882                },
1883                tmp {},
1884            },
1885        });
1886        fixture.assert_pending_recursive_rmdirs([]);
1887    }
1888
1889    #[test]
1890    fn getter_disconnected_getting() {
1891        let mut fixture = Fixture::new(1, fs! {});
1892
1893        fixture.get_artifact(apple!(1), jid!(1), GetArtifact::Get);
1894        fixture.get_artifact(apple!(1), jid!(100), GetArtifact::Get);
1895
1896        fixture.cache.getter_disconnected(TestGetter::Large);
1897
1898        // Only jid!(1) is returned.
1899        fixture.got_artifact_success_file(apple!(1), b"foo", vec![jid!(1)]);
1900
1901        fixture.assert_bytes_used(3);
1902        fixture.decrement_ref_count(apple!(1));
1903        fixture.assert_bytes_used(0);
1904    }
1905
1906    #[test]
1907    fn getter_disconnected_in_use() {
1908        let mut fixture = Fixture::new(1, fs! {});
1909
1910        fixture.get_artifact(apple!(1), jid!(1), GetArtifact::Get);
1911        fixture.get_artifact(apple!(1), jid!(100), GetArtifact::Get);
1912
1913        fixture.got_artifact_success_file(apple!(1), b"foo", vec![jid!(1), jid!(100)]);
1914
1915        // No effect since the cache has already turned the jid into a refcount.
1916        fixture.cache.getter_disconnected(TestGetter::Large);
1917
1918        fixture.assert_bytes_used(3);
1919        fixture.decrement_ref_count(apple!(1));
1920        fixture.assert_bytes_used(3);
1921        fixture.decrement_ref_count(apple!(1));
1922        fixture.assert_bytes_used(0);
1923    }
1924
1925    #[test]
1926    fn cache_path() {
1927        let fixture = Fixture::new(10, fs! {});
1928        assert_eq!(
1929            fixture.cache.cache_path(&apple!(42)).into_path_buf(),
1930            PathBuf::from(format!("/z/sha256/apple/{:0>64x}", 42)),
1931        );
1932    }
1933
1934    #[test]
1935    fn temp_file() {
1936        let fixture = Fixture::new(10, fs! {});
1937        let temp_file = fixture.temp_file_factory.temp_file().unwrap();
1938        assert_eq!(temp_file.path(), Path::new("/z/tmp/000"));
1939        assert_eq!(
1940            fixture.fs.metadata(temp_file.path()).unwrap().unwrap(),
1941            Metadata::file(0)
1942        );
1943    }
1944
1945    #[test]
1946    fn temp_dir() {
1947        let fixture = Fixture::new(10, fs! {});
1948        let temp_dir = fixture.temp_file_factory.temp_dir().unwrap();
1949        assert_eq!(temp_dir.path(), Path::new("/z/tmp/000"));
1950        assert_eq!(
1951            fixture.fs.metadata(temp_dir.path()).unwrap().unwrap(),
1952            Metadata::directory(0)
1953        );
1954    }
1955
1956    #[test]
1957    fn new_with_no_cache_directory() {
1958        let fixture = Fixture::new(1000, fs! {});
1959        fixture.assert_fs(fs! {
1960            z {
1961                "CACHEDIR.TAG"(&CACHEDIR_TAG_CONTENTS),
1962                "lock"(b""),
1963                removing {
1964                },
1965                sha256 {
1966                    apple {},
1967                    orange {},
1968                },
1969                tmp {},
1970            },
1971        });
1972        fixture.assert_pending_recursive_rmdirs([]);
1973    }
1974
1975    #[test]
1976    fn new_without_cachedir_tag_does_proper_cleanup_with_removing_dir() {
1977        let fixture = Fixture::new(
1978            1000,
1979            fs! {
1980                z {
1981                    removing {
1982                        "0000000000000001" { a(b"a contents") },
1983                        "0000000000000002"(b"2 contents"),
1984                        "0000000000000003" -> "foo",
1985                    },
1986                    sha256 {
1987                        apple {
1988                            bad_apple {
1989                                foo(b"foo contents"),
1990                                symlink -> "bad_apple",
1991                                more_bad_apple {
1992                                    bar(b"bar contents"),
1993                                },
1994                            },
1995                        },
1996                        banana {
1997                            bread(b"bread contents"),
1998                        },
1999                    },
2000                    garbage {
2001                        foo(b"more foo contents"),
2002                        symlink -> "garbage",
2003                        more_garbage {
2004                            bar(b"more bar contents"),
2005                        },
2006                    },
2007            foo(b"foo contents"),
2008            symlink -> "garbage",
2009                },
2010            },
2011        );
2012        fixture.assert_fs(fs! {
2013            z {
2014                "CACHEDIR.TAG"(&CACHEDIR_TAG_CONTENTS),
2015                "lock"(b""),
2016                removing {
2017                    "0000000000000002" {
2018                        foo(b"more foo contents"),
2019                        symlink -> "garbage",
2020                        more_garbage {
2021                            bar(b"more bar contents"),
2022                        },
2023                    },
2024                    "0000000000000003" {
2025                        "0000000000000001" { a(b"a contents") },
2026                        "0000000000000002"(b"2 contents"),
2027                        "0000000000000003" -> "foo",
2028                    },
2029                    "0000000000000004" {
2030                        apple {
2031                            bad_apple {
2032                                foo(b"foo contents"),
2033                                symlink -> "bad_apple",
2034                                more_bad_apple {
2035                                    bar(b"bar contents"),
2036                                },
2037                            },
2038                        },
2039                        banana {
2040                            bread(b"bread contents"),
2041                        },
2042                    },
2043                },
2044                sha256 {
2045                    apple {},
2046                    orange {},
2047                },
2048                tmp {},
2049            },
2050        });
2051        fixture.assert_pending_recursive_rmdirs([
2052            "/z/removing/0000000000000002",
2053            "/z/removing/0000000000000003",
2054            "/z/removing/0000000000000004",
2055        ]);
2056    }
2057
2058    #[test]
2059    fn new_without_cachedir_tag_does_proper_cleanup_with_removing_file() {
2060        let fixture = Fixture::new(
2061            1000,
2062            fs! {
2063                z {
2064                    removing (b"removing"),
2065                    sha256 {
2066                        apple {
2067                            bad_apple {
2068                                foo(b"foo contents"),
2069                                symlink -> "bad_apple",
2070                                more_bad_apple {
2071                                    bar(b"bar contents"),
2072                                },
2073                            },
2074                        },
2075                        banana {
2076                            bread(b"bread contents"),
2077                        },
2078                    },
2079                    garbage {
2080                        foo(b"more foo contents"),
2081                        symlink -> "garbage",
2082                        more_garbage {
2083                            bar(b"more bar contents"),
2084                        },
2085                    },
2086            foo(b"foo contents"),
2087            symlink -> "garbage",
2088                },
2089            },
2090        );
2091        fixture.assert_fs(fs! {
2092            z {
2093                "CACHEDIR.TAG"(&CACHEDIR_TAG_CONTENTS),
2094                "lock"(b""),
2095                removing {
2096                    "0000000000000001" {
2097                        foo(b"more foo contents"),
2098                        symlink -> "garbage",
2099                        more_garbage {
2100                            bar(b"more bar contents"),
2101                        },
2102                    },
2103                    "0000000000000002" {
2104                        apple {
2105                            bad_apple {
2106                                foo(b"foo contents"),
2107                                symlink -> "bad_apple",
2108                                more_bad_apple {
2109                                    bar(b"bar contents"),
2110                                },
2111                            },
2112                        },
2113                        banana {
2114                            bread(b"bread contents"),
2115                        },
2116                    },
2117                },
2118                sha256 {
2119                    apple {},
2120                    orange {},
2121                },
2122                tmp {},
2123            },
2124        });
2125        fixture.assert_pending_recursive_rmdirs([
2126            "/z/removing/0000000000000001",
2127            "/z/removing/0000000000000002",
2128        ]);
2129    }
2130
2131    #[test]
2132    fn new_without_cachedir_tag_does_proper_cleanup_with_no_removing_file_or_dir() {
2133        let fixture = Fixture::new(
2134            1000,
2135            fs! {
2136                z {
2137                    sha256 {
2138                        apple {
2139                            bad_apple {
2140                                foo(b"foo contents"),
2141                                symlink -> "bad_apple",
2142                                more_bad_apple {
2143                                    bar(b"bar contents"),
2144                                },
2145                            },
2146                        },
2147                        banana {
2148                            bread(b"bread contents"),
2149                        },
2150                    },
2151                    garbage {
2152                        foo(b"more foo contents"),
2153                        symlink -> "garbage",
2154                        more_garbage {
2155                            bar(b"more bar contents"),
2156                        },
2157                    },
2158            foo(b"foo contents"),
2159            symlink -> "garbage",
2160                },
2161            },
2162        );
2163        fixture.assert_fs(fs! {
2164            z {
2165                "CACHEDIR.TAG"(&CACHEDIR_TAG_CONTENTS),
2166                "lock"(b""),
2167                removing {
2168                    "0000000000000001" {
2169                        foo(b"more foo contents"),
2170                        symlink -> "garbage",
2171                        more_garbage {
2172                            bar(b"more bar contents"),
2173                        },
2174                    },
2175                    "0000000000000002" {
2176                        apple {
2177                            bad_apple {
2178                                foo(b"foo contents"),
2179                                symlink -> "bad_apple",
2180                                more_bad_apple {
2181                                    bar(b"bar contents"),
2182                                },
2183                            },
2184                        },
2185                        banana {
2186                            bread(b"bread contents"),
2187                        },
2188                    },
2189                },
2190                sha256 {
2191                    apple {},
2192                    orange {},
2193                },
2194                tmp {},
2195            },
2196        });
2197        fixture.assert_pending_recursive_rmdirs([
2198            "/z/removing/0000000000000001",
2199            "/z/removing/0000000000000002",
2200        ]);
2201    }
2202
2203    #[test]
2204    fn new_with_cachedir() {
2205        let mut fixture = Fixture::new(
2206            1000,
2207            fs! {
2208                z {
2209                    "CACHEDIR.TAG"(&CACHEDIR_TAG_CONTENTS),
2210                    removing {
2211                        "0000000000000001" { a(b"a contents") },
2212                        "0000000000000002"(b"2 contents"),
2213                        "0000000000000003" -> "foo",
2214                    },
2215                    sha256 {
2216                        apple {
2217                            "0000000000000000000000000000000000000000000000000000000000000001" -> "target",
2218                            "0000000000000000000000000000000000000000000000000000000000000002"(b"02"),
2219                            "0000000000000000000000000000000000000000000000000000000000000003" {
2220                                foo(b"foo contents"),
2221                                symlink -> "bad_apple",
2222                                more_bad_apple {
2223                                    bar(b"bar contents"),
2224                                },
2225                            },
2226                            "0000000000000000000000000000000000000000000000000000000000000004.size"(b"ab"),
2227                            garbage_file(b"ab"),
2228                            garbage_directory {
2229                                blah(b"blah contents"),
2230                            },
2231                            "0000000000000000000000000000000000000000000000000000000000000005" {
2232                                keep(b"keep"),
2233                            },
2234                            "0000000000000000000000000000000000000000000000000000000000000005.size"(b"\0\0\0\0\0\0\0\xff"),
2235                        },
2236                        banana {
2237                            bread(b"bread contents"),
2238                        },
2239                    },
2240                    garbage {
2241                        foo(b"more foo contents"),
2242                        symlink -> "garbage",
2243                        more_garbage {
2244                            bar(b"more bar contents"),
2245                        },
2246                    },
2247                    foo(b"foo contents"),
2248                    symlink -> "garbage",
2249                    tmp {
2250                        "tmp.1"(b"tmp.1"),
2251                    },
2252                },
2253            },
2254        );
2255        fixture.assert_fs(fs! {
2256            z {
2257                "CACHEDIR.TAG"(&CACHEDIR_TAG_CONTENTS),
2258                "lock"(b""),
2259                removing {
2260                    "0000000000000002" {
2261                        foo(b"more foo contents"),
2262                        symlink -> "garbage",
2263                        more_garbage {
2264                            bar(b"more bar contents"),
2265                        },
2266                    },
2267                    "0000000000000003" {
2268                        "0000000000000001" { a(b"a contents") },
2269                        "0000000000000002"(b"2 contents"),
2270                        "0000000000000003" -> "foo",
2271                    },
2272                    "0000000000000004" {
2273                        "tmp.1"(b"tmp.1"),
2274                    },
2275                    "0000000000000005" {
2276                        bread(b"bread contents"),
2277                    },
2278                    "0000000000000006" {
2279                        blah(b"blah contents"),
2280                    },
2281                    "0000000000000007" {
2282                        foo(b"foo contents"),
2283                        symlink -> "bad_apple",
2284                        more_bad_apple {
2285                            bar(b"bar contents"),
2286                        },
2287                    },
2288                },
2289                sha256 {
2290                    apple {
2291                        "0000000000000000000000000000000000000000000000000000000000000002"(b"02"),
2292                        "0000000000000000000000000000000000000000000000000000000000000005" {
2293                            keep(b"keep"),
2294                        },
2295                        "0000000000000000000000000000000000000000000000000000000000000005.size"(b"\0\0\0\0\0\0\0\xff"),
2296                    },
2297                    orange {},
2298                },
2299                tmp {},
2300            },
2301        });
2302        fixture.assert_pending_recursive_rmdirs([
2303            "/z/removing/0000000000000002",
2304            "/z/removing/0000000000000003",
2305            "/z/removing/0000000000000004",
2306            "/z/removing/0000000000000005",
2307            "/z/removing/0000000000000006",
2308            "/z/removing/0000000000000007",
2309        ]);
2310
2311        fixture.assert_bytes_used(257);
2312        fixture.get_artifact(apple!(2), jid!(1), GetArtifact::Success);
2313        fixture.get_artifact(apple!(5), jid!(1), GetArtifact::Success);
2314    }
2315
2316    #[test]
2317    fn new_without_cachedir_tag_with_lock_file_contends() {
2318        let fixture = Fixture::new(
2319            1000,
2320            fs! {
2321                z {
2322                    "lock"(b""),
2323                },
2324            },
2325        );
2326        fixture.assert_fs(fs! {
2327            z {
2328                "CACHEDIR.TAG"(&CACHEDIR_TAG_CONTENTS),
2329                "lock"(b""),
2330                removing {},
2331                sha256 {
2332                    apple {},
2333                    orange {},
2334                },
2335                tmp {},
2336            },
2337        });
2338        fixture.assert_pending_recursive_rmdirs([]);
2339
2340        let Err(err) = Cache::<test::Fs, TestKey, TestGetStrategy>::new(
2341            fixture.fs.clone(),
2342            "/z".parse().unwrap(),
2343            ByteSize::b(1).into(),
2344            Logger::root(Discard, o!()),
2345            false,
2346        ) else {
2347            panic!("expected error");
2348        };
2349
2350        assert_eq!(
2351            err.to_string(),
2352            "lock file \"/z/lock\" is held by a different process"
2353        );
2354    }
2355
2356    #[test]
2357    fn new_without_cachedir_tag_without_lock_file_contends() {
2358        let fixture = Fixture::new(1000, fs! {});
2359        fixture.assert_fs(fs! {
2360            z {
2361                "CACHEDIR.TAG"(&CACHEDIR_TAG_CONTENTS),
2362                "lock"(b""),
2363                removing {},
2364                sha256 {
2365                    apple {},
2366                    orange {},
2367                },
2368                tmp {},
2369            },
2370        });
2371        fixture.assert_pending_recursive_rmdirs([]);
2372
2373        let Err(err) = Cache::<test::Fs, TestKey, TestGetStrategy>::new(
2374            fixture.fs.clone(),
2375            "/z".parse().unwrap(),
2376            ByteSize::b(1).into(),
2377            Logger::root(Discard, o!()),
2378            false,
2379        ) else {
2380            panic!("expected error");
2381        };
2382
2383        assert_eq!(
2384            err.to_string(),
2385            "lock file \"/z/lock\" is held by a different process"
2386        );
2387    }
2388
2389    #[test]
2390    fn new_with_cachedir_tag_with_lock_file_contends() {
2391        let fixture = Fixture::new(
2392            1000,
2393            fs! {
2394                z {
2395                    "CACHEDIR.TAG"(&CACHEDIR_TAG_CONTENTS),
2396                    "lock"(b""),
2397                    removing {},
2398                    sha256 {
2399                        apple {},
2400                        orange {},
2401                    },
2402                    tmp {},
2403                },
2404            },
2405        );
2406        fixture.assert_pending_recursive_rmdirs([
2407            "/z/removing/0000000000000002",
2408            "/z/removing/0000000000000003",
2409        ]);
2410
2411        let Err(err) = Cache::<test::Fs, TestKey, TestGetStrategy>::new(
2412            fixture.fs.clone(),
2413            "/z".parse().unwrap(),
2414            ByteSize::b(1).into(),
2415            Logger::root(Discard, o!()),
2416            false,
2417        ) else {
2418            panic!("expected error");
2419        };
2420
2421        assert_eq!(
2422            err.to_string(),
2423            "lock file \"/z/lock\" is held by a different process"
2424        );
2425    }
2426}