Skip to main content

heddle_pack/store/pack/
scratch.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Crash-reclaimable scratch. Owners hold a shared OS lock until their files
3//! close; store-open sweeping takes an exclusive lock before deleting an entry.
4use std::{
5    fs::{self, File, OpenOptions},
6    io,
7    path::Path,
8    time::{Duration, SystemTime},
9};
10
11pub(super) const LEASE_FILE: &str = ".lease";
12
13/// A live scratch directory, also usable for caller-owned transfer staging.
14#[derive(Debug)]
15pub struct ScratchLease(File);
16impl ScratchLease {
17    pub fn acquire(path: &Path) -> io::Result<Self> {
18        let file = OpenOptions::new()
19            .read(true)
20            .write(true)
21            .create(true)
22            .truncate(false)
23            .open(path.join(LEASE_FILE))?;
24        file.lock_shared()?;
25        Ok(Self(file))
26    }
27}
28impl Drop for ScratchLease {
29    fn drop(&mut self) {
30        let _ = self.0.unlock();
31    }
32}
33
34/// Removes its files on normal drop; the lease protects even old scratch from
35/// another process's store-open sweep. The OS releases the lease after a crash.
36#[derive(Debug)]
37pub struct ScratchDir {
38    _lease: ScratchLease,
39    directory: tempfile::TempDir,
40}
41impl ScratchDir {
42    pub fn new(root: &Path, prefix: &str) -> io::Result<Self> {
43        fs::create_dir_all(root)?;
44        let directory = tempfile::Builder::new().prefix(prefix).tempdir_in(root)?;
45        let lease = ScratchLease::acquire(directory.path())?;
46        Ok(Self {
47            _lease: lease,
48            directory,
49        })
50    }
51    pub fn path(&self) -> &Path {
52        self.directory.path()
53    }
54}
55
56/// Best-effort reclamation at store open. Unknown ages and unavailable locks
57/// are kept. Symlinks are never followed. Fresh entries have a full day to
58/// finish publishing their lease before they can become sweep candidates.
59pub fn sweep_scratch(root: &Path) {
60    let Ok(entries) = fs::read_dir(root) else {
61        return;
62    };
63    let now = SystemTime::now();
64    for entry in entries.flatten() {
65        let path = entry.path();
66        let Ok(metadata) = fs::symlink_metadata(&path) else {
67            continue;
68        };
69        if metadata.file_type().is_symlink() {
70            continue;
71        }
72        let expired = metadata
73            .modified()
74            .ok()
75            .and_then(|modified| now.duration_since(modified).ok())
76            .is_some_and(|age| age > Duration::from_secs(86400));
77        if !expired {
78            continue;
79        }
80        let lease_path = if metadata.is_dir() {
81            path.join(LEASE_FILE)
82        } else {
83            path.clone()
84        };
85        // Old abandoned entries predate leases. New directories always contain
86        // a lease before any transfer writes, and remain locked until drop.
87        let lease = match File::open(&lease_path) {
88            Ok(file) => Some(file),
89            Err(error) if metadata.is_dir() && error.kind() == io::ErrorKind::NotFound => None,
90            Err(_) => continue,
91        };
92        if lease.as_ref().is_some_and(|file| file.try_lock().is_err()) {
93            continue;
94        }
95        if metadata.is_dir() {
96            let _ = fs::remove_dir_all(&path);
97        } else {
98            let _ = fs::remove_file(&path);
99        }
100    }
101}