kismet_cache/
raw_cache.rs

1//! The raw cache module manages directories of read-only files
2//! subject to a (batched) Second Chance eviction policy.  Calling
3//! [`prune`] deletes files to make sure a cache directory does not
4//! exceed its capacity, in file count.  The deletions will obey a
5//! Second Chance policy as long as insertions and updates go through
6//! [`insert_or_update`] or [`insert_or_touch`], in order to update the
7//! cached files' modification times correctly.  Opening the cached
8//! file will automatically update its metadata to take that access
9//! into account, but a path can also be [`touch`]ed explicitly.
10//!
11//! This module implements mechanisms, but does not hardcode any
12//! policy... except the use of a second chance strategy.
13use filetime::FileTime;
14use std::fs::DirEntry;
15use std::io::ErrorKind;
16use std::io::Result;
17use std::path::Path;
18use std::path::PathBuf;
19
20use crate::second_chance;
21
22/// When clearing the accessed bit on a file, make sure atime is this
23/// many seconds before mtime: some filesystems force relatime,
24/// potentially with a coarse timestamp granularity. Setting atime
25/// clearly earlier than mtime will force a relatime update on the
26/// next access.
27///
28/// The current value, at 2 minutes, should be large enough for most
29/// filesystems (usual coarse granularities are 1-2 seconds).
30const ENFORCED_ATIME_MTIME_DELTA_SEC: i64 = 120;
31
32/// A `CachedFile` represents what we know about a given file in a raw
33/// cache directory: its direntry, mtime, and atime.
34struct CachedFile {
35    entry: DirEntry,
36    mtime: FileTime,
37    accessed: bool,
38}
39
40/// Removes a file if it exists.
41fn ensure_file_removed(path: &Path) -> Result<()> {
42    match std::fs::remove_file(path) {
43        Ok(()) => Ok(()),
44        Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
45        err => err,
46    }
47}
48
49/// Moves the file at `path` to the back of the second chance list.
50fn move_to_back_of_list(path: &Path) -> Result<()> {
51    let mtime = FileTime::now();
52    // Make sure the atime is firmly in the past compared to mtime,
53    // and preserve the "fraction of a second" part to hint at the
54    // artificial relationship with mtime.
55    let atime = FileTime::from_unix_time(
56        mtime
57            .unix_seconds()
58            .saturating_sub(ENFORCED_ATIME_MTIME_DELTA_SEC),
59        mtime.nanoseconds(),
60    );
61    filetime::set_file_times(path, atime, mtime)
62}
63
64/// Marks the file at `path` as read-only.
65fn set_read_only(path: &Path) -> Result<()> {
66    let mut permissions = std::fs::symlink_metadata(path)?.permissions();
67
68    permissions.set_readonly(true);
69    std::fs::set_permissions(path, permissions)
70}
71
72/// Sets the access bit to true for the file at `path`: the next time
73/// that file is up for eviction, it will get a second chance.  Returns
74/// true if the file was found, false otherwise.
75///
76/// In most cases, there is no need to explicitly call this function:
77/// the operating system will automatically perform the required
78/// update while opening the file at `path`.
79pub fn touch(path: impl AsRef<Path>) -> Result<bool> {
80    fn run(path: &Path) -> Result<bool> {
81        match filetime::set_file_atime(path, FileTime::now()) {
82            Ok(()) => Ok(true),
83            // It's OK if the file we're trying to touch was removed:
84            // things do disappear from caches.
85            Err(e) if e.kind() == ErrorKind::NotFound => Ok(false),
86            Err(e) => Err(e),
87        }
88    }
89
90    run(path.as_ref())
91}
92
93/// Consumes the file `from` and publishes it to the raw cache file
94/// `to`, a file directly under the raw cache directory.
95///
96/// On success, `from` is deleted.  On failure, the cache directory is
97/// always in a valid state.
98///
99/// The rename will be atomic, but the source file must be private to
100/// the caller: this function accesses the source file multiple time.
101///
102/// If durability is necessary, the caller is responsible for
103/// `sync_data`ing the contents of `from`.  This function does not
104/// fsync the cache directory itself: it's a cache, so stale contents
105/// are assumed safe.
106pub fn insert_or_update(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
107    fn run(from: &Path, to: &Path) -> Result<()> {
108        // Move to the back of the list before publishing: if a reader
109        // comes in right away, we want it to set the access bit.
110        move_to_back_of_list(from)?;
111        set_read_only(from)?;
112        std::fs::rename(from, to)?;
113        ensure_file_removed(from)
114    }
115
116    run(from.as_ref(), to.as_ref())
117}
118
119/// Consumes the file `from` and publishes it to the raw cache file
120/// `to`, a file directly under the raw cache directly, only if that
121/// file does not already exist.
122///
123/// If the file exists, `from` is still consumed, but `to` is not
124/// updated; instead, its access bit is set to true.
125///
126/// On success, `from` is deleted.  On failure, the cache directory is
127/// always in a valid state.
128///
129/// The link will be atomic, but the source file must be private to
130/// the caller: this function accesses the source file multiple times.
131///
132/// If durability is necessary, the caller is responsible for
133/// `sync_data`ing the contents of `from`.  This function does not
134/// fsync the cache directory itself: it's a cache, so stale contents
135/// are assumed safe.
136pub fn insert_or_touch(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
137    fn run(from: &Path, to: &Path) -> Result<()> {
138        // Optimise for the successful publication case: we expect callers
139        // to only `insert_or_touch` after a failed lookup, so the `link`
140        // call will only fail with EEXIST if another writer raced with us.
141        move_to_back_of_list(from)?;
142        set_read_only(from)?;
143        match std::fs::hard_link(from, to) {
144            Ok(()) => {}
145            // The destination file already exists; we just have to mark
146            // it as accessed.
147            Err(e) if e.kind() == ErrorKind::AlreadyExists => {
148                touch(to)?;
149            }
150            err => err?,
151        }
152
153        ensure_file_removed(from)
154    }
155
156    run(from.as_ref(), to.as_ref())
157}
158
159impl second_chance::Entry for CachedFile {
160    type Rank = FileTime;
161
162    #[inline]
163    fn rank(&self) -> FileTime {
164        self.mtime
165    }
166
167    #[inline]
168    fn accessed(&self) -> bool {
169        self.accessed
170    }
171}
172
173impl CachedFile {
174    fn new(entry: DirEntry, meta: &std::fs::Metadata) -> CachedFile {
175        let atime = FileTime::from_last_access_time(meta);
176        let mtime = FileTime::from_last_modification_time(meta);
177
178        CachedFile {
179            entry,
180            mtime,
181            accessed: atime > mtime,
182        }
183    }
184}
185
186/// Attempts to list all the cached files directly under `cache_dir`.
187///
188/// On success, returns the list of cached files and an estimate of
189/// the number of files under `cache_dir`.
190///
191/// That estimate is always greater than or equal to the number of
192/// `CachedFile`s returned in the vector.
193fn collect_cached_files(cache_dir: &Path) -> Result<(Vec<CachedFile>, u64)> {
194    let mut cache = Vec::new();
195    let mut count = 0;
196    for maybe_entry in std::fs::read_dir(cache_dir)? {
197        count += 1;
198        if let Ok(entry) = maybe_entry {
199            let meta = match entry.metadata() {
200                Ok(meta) => meta,
201                Err(e) if e.kind() == ErrorKind::NotFound => continue,
202                err => err?,
203            };
204
205            if meta.is_dir() {
206                // Don't count subdirectories, we never delete them.
207                count -= 1;
208            } else {
209                cache.push(CachedFile::new(entry, &meta));
210            }
211        }
212    }
213
214    // We increment before pushing to `cache`.
215    assert!(count >= cache.len() as u64);
216    Ok((cache, count))
217}
218
219/// Applies the `update` to files in `parent`.
220fn apply_update(parent: PathBuf, update: second_chance::Update<CachedFile>) -> Result<()> {
221    // When the `parent` path is long, we can spend most of our time
222    // constructing paths with `DirEntry::path`.  Directly push/pop
223    // after the parent directory instead.
224    let mut cached = parent;
225
226    for entry in update.to_evict {
227        cached.push(entry.entry.file_name());
228        ensure_file_removed(&cached)?;
229        cached.pop();
230    }
231
232    for entry in update.to_move_back {
233        cached.push(entry.entry.file_name());
234        match move_to_back_of_list(&cached) {
235            Ok(()) => {}
236            // Silently ignore ENOENT: things do disappear from caches.
237            Err(e) if e.kind() == ErrorKind::NotFound => {}
238            err => err?,
239        }
240        cached.pop();
241    }
242
243    Ok(())
244}
245
246/// Attempts to prune the contents of `cache_dir` down to at most
247/// `capacity` files, with a second chance policy.
248///
249/// On success, returns an estimate of the number of files remaining
250/// in `cache_dir` and the number of files deleted.
251///
252/// Ignores any subdirectory of `cache_dir`.
253pub fn prune(cache_dir: PathBuf, capacity: usize) -> Result<(u64, usize)> {
254    let (cached_files, count) = collect_cached_files(&cache_dir)?;
255    let update = second_chance::Update::new(cached_files, capacity);
256    let num_evicted = update.to_evict.len();
257
258    // `CachedFile` doesn't implement `Clone` (nor can `Update::new`
259    // assume the trait is available), so the values in `to_evict`
260    // must all come from `cached_files`; since `cached_files.len() <=
261    // count`, we must find `num_evicted <= count`.
262    assert!(num_evicted as u64 <= count);
263
264    apply_update(cache_dir, update)?;
265    Ok((count - (num_evicted as u64), num_evicted))
266}
267
268/// Removing a file should remove that file.
269#[test]
270fn test_remove_file() {
271    use test_dir::{DirBuilder, FileType, TestDir};
272    let temp = TestDir::temp().create("cache_file", FileType::ZeroFile(10));
273
274    let path = temp.path("cache_file");
275    assert!(std::fs::metadata(&path).is_ok());
276    assert!(ensure_file_removed(&path).is_ok());
277
278    assert!(matches!(std::fs::metadata(&path),
279                     Err(e) if e.kind() == ErrorKind::NotFound));
280
281    // Removing a file that does not exist is ok.
282    assert!(ensure_file_removed(&path).is_ok());
283    assert!(matches!(std::fs::metadata(&path),
284                     Err(e) if e.kind() == ErrorKind::NotFound));
285}
286
287/// ensure_file_removed should succeed if the file is already gone.
288#[test]
289fn test_remove_inexistent_file() {
290    use test_dir::{DirBuilder, TestDir};
291    let temp = TestDir::temp();
292
293    let path = temp.path("cache_file");
294    assert!(matches!(std::fs::metadata(&path),
295                     Err(e) if e.kind() == ErrorKind::NotFound));
296
297    // Removing a file that does not exist is ok.
298    assert!(ensure_file_removed(&path).is_ok());
299    assert!(matches!(std::fs::metadata(&path),
300                     Err(e) if e.kind() == ErrorKind::NotFound));
301}
302
303/// Ensures enough time has elapsed for atime / mtime to change: some
304/// file systems only work at second granularity.
305#[cfg(test)]
306fn advance_time() {
307    std::thread::sleep(std::time::Duration::from_secs_f64(1.5));
308}
309
310/// Moving to the back of the list should increase the file's rank and
311/// not set the accessed bit.
312#[test]
313fn test_back_of_list() {
314    use crate::second_chance::Entry;
315    use test_dir::{DirBuilder, FileType, TestDir};
316
317    let temp = TestDir::temp().create("old_cache_file", FileType::ZeroFile(10));
318
319    let path = temp.path("old_cache_file");
320
321    let get_entry = || {
322        let (mut files, count) =
323            collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
324        assert_eq!(count, 1);
325        assert_eq!(files.len(), 1);
326
327        files.pop().expect("vec is non-empty")
328    };
329
330    let old_entry = get_entry();
331    advance_time();
332    move_to_back_of_list(&path).expect("call should succeed");
333    let new_entry = get_entry();
334
335    assert!(new_entry.rank() > old_entry.rank());
336    assert!(!new_entry.accessed());
337}
338
339/// Setting a file read only should... make it read only.
340#[test]
341fn test_set_read_only() {
342    use test_dir::{DirBuilder, FileType, TestDir};
343
344    let temp = TestDir::temp().create("old_cache_file", FileType::ZeroFile(10));
345
346    let path = temp.path("old_cache_file");
347
348    // The file should be initially read-write
349    {
350        let permissions = std::fs::metadata(&path)
351            .expect("metadata should succeed")
352            .permissions();
353
354        assert!(!permissions.readonly());
355    }
356
357    set_read_only(&path).expect("set_read_only should succeed");
358
359    // The file should now be read-only.
360    {
361        let permissions = std::fs::metadata(&path)
362            .expect("metadata should succeed")
363            .permissions();
364
365        assert!(permissions.readonly());
366    }
367}
368
369/// Touching should set the accessed bit, but not change the rank.
370#[test]
371fn test_touch() {
372    use crate::second_chance::Entry;
373    use test_dir::{DirBuilder, FileType, TestDir};
374
375    let temp = TestDir::temp().create("old_cache_file", FileType::ZeroFile(10));
376
377    let path = temp.path("old_cache_file");
378
379    let get_entry = || {
380        let (mut files, count) =
381            collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
382        assert_eq!(count, 1);
383        assert_eq!(files.len(), 1);
384
385        files.pop().expect("vec is non-empty")
386    };
387
388    move_to_back_of_list(&path).expect("call should succeed");
389    let old_entry = get_entry();
390    assert!(!old_entry.accessed());
391
392    advance_time();
393    // Should return true: the file exists.
394    assert!(touch(&path).expect("call should succeed"));
395    let new_entry = get_entry();
396
397    assert_eq!(new_entry.rank(), old_entry.rank());
398    assert!(new_entry.accessed());
399}
400
401/// Touching should set the accessed bit, but not change the rank.
402#[test]
403fn test_touch_missing() {
404    use test_dir::{DirBuilder, TestDir};
405
406    let temp = TestDir::temp();
407    // Should return file: the file does not exist.
408    assert!(!touch(&temp.path("absent")).expect("should succeed on missing files"));
409}
410
411/// Reading a file should set the accessed bit, but not change the rank.
412#[test]
413fn test_touch_by_open() {
414    use crate::second_chance::Entry;
415    use test_dir::{DirBuilder, FileType, TestDir};
416
417    let temp = TestDir::temp().create("old_cache_file", FileType::ZeroFile(10));
418
419    let path = temp.path("old_cache_file");
420
421    let get_entry = || {
422        let (mut files, count) =
423            collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
424        assert_eq!(count, 1);
425        assert_eq!(files.len(), 1);
426
427        files.pop().expect("vec is non-empty")
428    };
429
430    move_to_back_of_list(&path).expect("call should succeed");
431    let old_entry = get_entry();
432    assert!(!old_entry.accessed());
433
434    advance_time();
435    let _ = std::fs::read(&path).expect("read should succeed");
436    let new_entry = get_entry();
437
438    assert_eq!(new_entry.rank(), old_entry.rank());
439    assert!(new_entry.accessed());
440}
441
442/// Moving to the back of the list should clear the access bit.
443#[test]
444fn test_back_of_list_after_touch() {
445    use crate::second_chance::Entry;
446    use test_dir::{DirBuilder, FileType, TestDir};
447
448    let temp = TestDir::temp().create("old_cache_file", FileType::ZeroFile(10));
449
450    let path = temp.path("old_cache_file");
451
452    let get_entry = || {
453        let (mut files, count) =
454            collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
455        assert_eq!(count, 1);
456        assert_eq!(files.len(), 1);
457
458        files.pop().expect("vec is non-empty")
459    };
460
461    advance_time();
462    touch(&path).expect("call should succeed");
463    let old_entry = get_entry();
464    assert!(old_entry.accessed());
465
466    advance_time();
467    move_to_back_of_list(&path).expect("call should succeed");
468    let new_entry = get_entry();
469
470    assert!(new_entry.rank() > old_entry.rank());
471    assert!(!new_entry.accessed());
472}
473
474/// Moving to the back of the list should clear the access bit, even when
475/// set by opening the file for read.
476#[test]
477fn test_back_of_list_after_open() {
478    use crate::second_chance::Entry;
479    use test_dir::{DirBuilder, FileType, TestDir};
480
481    let temp = TestDir::temp().create("old_cache_file", FileType::ZeroFile(10));
482
483    let path = temp.path("old_cache_file");
484
485    let get_entry = || {
486        let (mut files, count) =
487            collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
488        assert_eq!(count, 1);
489        assert_eq!(files.len(), 1);
490
491        files.pop().expect("vec is non-empty")
492    };
493
494    advance_time();
495    let _ = std::fs::read(&path).expect("read should succeed");
496    let old_entry = get_entry();
497    assert!(old_entry.accessed());
498
499    advance_time();
500    move_to_back_of_list(&path).expect("call should succeed");
501    let new_entry = get_entry();
502
503    assert!(new_entry.rank() > old_entry.rank());
504    assert!(!new_entry.accessed());
505}
506
507/// Inserting a new file should move to the destination, and mark the
508/// file as not yet accessed.
509#[test]
510fn test_insert_empty() {
511    use crate::second_chance::Entry;
512    use test_dir::{DirBuilder, FileType, TestDir};
513
514    let temp = TestDir::temp().create("temp_file", FileType::RandomFile(10));
515
516    let path = temp.path("temp_file");
517    let dst = temp.path("cache");
518
519    let get_entry = || {
520        let (mut files, count) =
521            collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
522        assert_eq!(count, 1);
523        assert_eq!(files.len(), 1);
524
525        files.pop().expect("vec is non-empty")
526    };
527
528    // Read *after* the file is created, to make sure it would have
529    // the accessed bit set.
530    advance_time();
531    let payload = std::fs::read(&path).expect("read should succeed");
532
533    insert_or_update(&path, &dst).expect("insert_or_touch should succeed");
534    // The old file should be gone.
535    assert!(matches!(std::fs::metadata(&path),
536                     Err(e) if e.kind() == ErrorKind::NotFound));
537
538    // The destination file should not be marked as accessed.
539    assert!(!get_entry().accessed());
540    // The destination file should have the original payload.
541    assert_eq!(&payload, &std::fs::read(&dst).expect("read should succeed"));
542    // The destination file should now be read-only.
543    assert!(std::fs::metadata(&dst)
544        .expect("metadata should succeed")
545        .permissions()
546        .readonly());
547}
548
549/// Inserting a new file over an old one should overwrite, and mark
550/// the file as not yet accessed.
551#[test]
552fn test_insert_overwrite() {
553    use crate::second_chance::Entry;
554    use test_dir::{DirBuilder, FileType, TestDir};
555
556    let temp = TestDir::temp()
557        .create("temp_file", FileType::RandomFile(10))
558        .create("cache", FileType::ZeroFile(100));
559
560    let path = temp.path("temp_file");
561    let dst = temp.path("cache");
562
563    let get_entry = || {
564        let (mut files, count) =
565            collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
566        assert_eq!(count, 1);
567        assert_eq!(files.len(), 1);
568
569        files.pop().expect("vec is non-empty")
570    };
571
572    // Read *after* the file is created, to make sure it would have
573    // the accessed bit set.
574    advance_time();
575    let payload = std::fs::read(&path).expect("read should succeed");
576
577    insert_or_update(&path, &dst).expect("insert_or_touch should succeed");
578    // The old file should be gone.
579    assert!(matches!(std::fs::metadata(&path),
580                     Err(e) if e.kind() == ErrorKind::NotFound));
581
582    // The destination file should not be marked as accessed.
583    assert!(!get_entry().accessed());
584    // The destination file should have the original payload.
585    assert_eq!(&payload, &std::fs::read(&dst).expect("read should succeed"));
586}
587
588/// Inserting a new file should move to the destination, and mark the
589/// file as not yet accessed.
590#[test]
591fn test_insert_or_touch_empty() {
592    use crate::second_chance::Entry;
593    use test_dir::{DirBuilder, FileType, TestDir};
594
595    let temp = TestDir::temp().create("temp_file", FileType::RandomFile(10));
596
597    let path = temp.path("temp_file");
598    let dst = temp.path("cache");
599
600    let get_entry = || {
601        let (mut files, count) =
602            collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
603        assert_eq!(count, 1);
604        assert_eq!(files.len(), 1);
605
606        files.pop().expect("vec is non-empty")
607    };
608
609    // Read *after* the file is created, to make sure it would have
610    // the accessed bit set.
611    advance_time();
612    let payload = std::fs::read(&path).expect("read should succeed");
613
614    insert_or_touch(&path, &dst).expect("insert_or_touch should succeed");
615    // The old file should be gone.
616    assert!(matches!(std::fs::metadata(&path),
617                     Err(e) if e.kind() == ErrorKind::NotFound));
618
619    // The destination file should not be marked as accessed.
620    assert!(!get_entry().accessed());
621    // The destination file should have the original payload.
622    assert_eq!(&payload, &std::fs::read(&dst).expect("read should succeed"));
623    // The destination file should now be read-only.
624    assert!(std::fs::metadata(&dst)
625        .expect("metadata should succeed")
626        .permissions()
627        .readonly());
628}
629
630/// insert_or_touch'ing over an old file should not overwrite, but
631/// mark the file as newly accessed.
632#[test]
633fn test_insert_touch_overwrite() {
634    use crate::second_chance::Entry;
635    use test_dir::{DirBuilder, FileType, TestDir};
636
637    let temp = TestDir::temp()
638        .create("temp_file", FileType::RandomFile(10))
639        .create("cache", FileType::RandomFile(10));
640
641    let path = temp.path("temp_file");
642    let dst = temp.path("cache");
643
644    let get_entry = || {
645        let (mut files, count) =
646            collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
647        assert_eq!(count, 1);
648        assert_eq!(files.len(), 1);
649
650        files.pop().expect("vec is non-empty")
651    };
652
653    let payload = std::fs::read(&dst).expect("read should succeed");
654    // Clear the access bit.
655    move_to_back_of_list(&dst).expect("move to back should succeed");
656
657    advance_time();
658
659    insert_or_touch(&path, &dst).expect("insert_or_touch should succeed");
660    // The old file should be gone.
661    assert!(matches!(std::fs::metadata(&path),
662                     Err(e) if e.kind() == ErrorKind::NotFound));
663
664    // The destination file should be marked as accessed.
665    assert!(get_entry().accessed());
666    // The destination file should have the original payload.
667    assert_eq!(&payload, &std::fs::read(&dst).expect("read should succeed"));
668}
669
670/// Smoke test that we find the order we expect.
671#[test]
672fn test_collect_cached_files() {
673    use crate::second_chance::Entry;
674    use std::ffi::OsString;
675    use test_dir::{DirBuilder, FileType, TestDir};
676
677    let temp = TestDir::temp()
678        .create("a", FileType::RandomFile(10))
679        .create("b", FileType::RandomFile(10))
680        .create("c", FileType::RandomFile(10))
681        .create("d", FileType::RandomFile(10))
682        // The directory should be ignored.
683        .create("a_directory", FileType::Dir);
684
685    // Set up the order: d, a, c, b
686    advance_time();
687    move_to_back_of_list(&temp.path("d")).expect("should succeed");
688    advance_time();
689    move_to_back_of_list(&temp.path("a")).expect("should succeed");
690    advance_time();
691    move_to_back_of_list(&temp.path("c")).expect("should succeed");
692    advance_time();
693    move_to_back_of_list(&temp.path("b")).expect("should succeed");
694
695    // Touch d and c.
696    advance_time();
697    touch(&temp.path("d")).expect("should succeed");
698    touch(&temp.path("c")).expect("should succeed");
699
700    let (mut cached, count) = collect_cached_files(&temp.path(".")).expect("should succeed");
701
702    assert_eq!(count, 4);
703    cached.sort_by_key(|e| e.rank());
704
705    assert_eq!(
706        cached
707            .iter()
708            .map(|e| e.entry.file_name())
709            .collect::<Vec<OsString>>(),
710        vec!["d", "a", "c", "b"]
711    );
712
713    assert_eq!(
714        cached.iter().map(|e| e.accessed()).collect::<Vec<bool>>(),
715        vec![true, false, true, false]
716    );
717}
718
719/// Similar setup, but now directly delete / move files.
720#[test]
721fn test_apply_update() {
722    use crate::second_chance::Entry;
723    use std::ffi::OsString;
724    use test_dir::{DirBuilder, FileType, TestDir};
725
726    let temp = TestDir::temp()
727        .create("a", FileType::RandomFile(10))
728        .create("b", FileType::RandomFile(10))
729        .create("c", FileType::RandomFile(10))
730        .create("d", FileType::RandomFile(10))
731        // We will add these files to the update before deleting them;
732        // `apply_update` shouldn't blow up.
733        .create("deleted_touch", FileType::RandomFile(10))
734        .create("already_deleted", FileType::RandomFile(10));
735
736    // Set up the order: d, a, c, b
737    advance_time();
738    move_to_back_of_list(&temp.path("d")).expect("should succeed");
739    advance_time();
740    move_to_back_of_list(&temp.path("a")).expect("should succeed");
741    advance_time();
742    move_to_back_of_list(&temp.path("c")).expect("should succeed");
743    advance_time();
744    move_to_back_of_list(&temp.path("b")).expect("should succeed");
745
746    // Move deleted_touch and already_deleted to the back.
747    advance_time();
748    move_to_back_of_list(&temp.path("deleted_touch")).expect("should succeed");
749    advance_time();
750    move_to_back_of_list(&temp.path("already_deleted")).expect("should succeed");
751
752    // Touch d and c.
753    advance_time();
754    touch(&temp.path("d")).expect("should succeed");
755    touch(&temp.path("c")).expect("should succeed");
756
757    // move d to the back, delete a.
758    {
759        let (mut cached, count) = collect_cached_files(&temp.path(".")).expect("should succeed");
760
761        assert_eq!(count, 6);
762        cached.sort_by_key(|e| e.rank());
763
764        assert_eq!(
765            cached
766                .iter()
767                .map(|e| e.entry.file_name())
768                .collect::<Vec<OsString>>(),
769            vec!["d", "a", "c", "b", "deleted_touch", "already_deleted"]
770        );
771
772        let mut to_evict = Vec::new();
773        let mut to_move_back = Vec::new();
774
775        to_move_back.extend(cached.drain(0..1));
776        to_evict.extend(cached.drain(0..1));
777        to_evict.push(cached.pop().expect("is non-empty"));
778        to_move_back.push(cached.pop().expect("is non-empty"));
779
780        // Delete these files before applying the update; it should
781        // just ignore their entries.
782        std::fs::remove_file(&temp.path("deleted_touch")).expect("deletion should succeed");
783        std::fs::remove_file(&temp.path("already_deleted")).expect("deletion should succeed");
784
785        apply_update(
786            temp.path("."),
787            second_chance::Update {
788                to_evict,
789                to_move_back,
790            },
791        )
792        .expect("update should succeed");
793    }
794
795    let (mut cached, count) = collect_cached_files(&temp.path(".")).expect("should succeed");
796
797    assert_eq!(count, 3);
798    cached.sort_by_key(|e| e.rank());
799
800    // We should find 3 files, in this order
801    assert_eq!(
802        cached
803            .iter()
804            .map(|e| e.entry.file_name())
805            .collect::<Vec<OsString>>(),
806        vec!["c", "b", "d"]
807    );
808
809    // And "d"'s accessed bit should be reset.
810    assert_eq!(
811        cached.iter().map(|e| e.accessed()).collect::<Vec<bool>>(),
812        vec![true, false, false]
813    );
814}
815
816/// Same setup, but now directly delete / move files.
817#[test]
818fn test_prune() {
819    use crate::second_chance::Entry;
820    use std::ffi::OsString;
821    use test_dir::{DirBuilder, FileType, TestDir};
822
823    let temp = TestDir::temp()
824        .create("a", FileType::RandomFile(10))
825        .create("b", FileType::RandomFile(10))
826        .create("c", FileType::RandomFile(10))
827        .create("d", FileType::RandomFile(10));
828
829    // Set up the order: d, a, c, b
830    advance_time();
831    move_to_back_of_list(&temp.path("d")).expect("should succeed");
832    advance_time();
833    move_to_back_of_list(&temp.path("a")).expect("should succeed");
834    advance_time();
835    move_to_back_of_list(&temp.path("c")).expect("should succeed");
836    advance_time();
837    move_to_back_of_list(&temp.path("b")).expect("should succeed");
838
839    // Touch d and c.
840    advance_time();
841    touch(&temp.path("d")).expect("should succeed");
842    touch(&temp.path("c")).expect("should succeed");
843
844    // With capacity for 3 files, we should move "d" to the back,
845    // and delete "a".
846    //
847    // That leaves us with 3 files (1 deletion).
848    assert_eq!(
849        prune(temp.path("."), 3).expect("prune should succeed"),
850        (3, 1)
851    );
852
853    let (mut cached, count) = collect_cached_files(&temp.path(".")).expect("should succeed");
854
855    assert_eq!(count, 3);
856    cached.sort_by_key(|e| e.rank());
857
858    // We should find 3 files, in this order
859    assert_eq!(
860        cached
861            .iter()
862            .map(|e| e.entry.file_name())
863            .collect::<Vec<OsString>>(),
864        vec!["c", "b", "d"]
865    );
866
867    // And "d"'s accessed bit should be reset.
868    assert_eq!(
869        cached.iter().map(|e| e.accessed()).collect::<Vec<bool>>(),
870        vec![true, false, false]
871    );
872}