Skip to content

Commit bf8440b

Browse files
committed
add freeing disk space functionliaty
1 parent a82e6dc commit bf8440b

File tree

3 files changed

+127
-8
lines changed

3 files changed

+127
-8
lines changed

Diff for: Cargo.lock

+24
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ bzip2 = "0.4.4"
6969
serde_cbor = "0.11.1"
7070
getrandom = "0.2.1"
7171
itertools = { version = "0.10.5", optional = true}
72+
sysinfo = { version = "0.28.2", default-features = false }
7273

7374
# Async
7475
tokio = { version = "1.0", features = ["rt-multi-thread", "signal", "macros"] }

Diff for: src/docbuilder/caching.rs

+102-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,33 @@
1-
use anyhow::{Context as _, Result};
1+
use anyhow::{bail, Context as _, Result};
22
use std::{
3-
fs,
3+
collections::HashMap,
4+
fs::{self, OpenOptions},
45
path::{Path, PathBuf},
6+
time::SystemTime,
57
};
8+
use sysinfo::{DiskExt, RefreshKind, System, SystemExt};
69
use tracing::{debug, instrument, warn};
710

11+
static LAST_ACCESSED_FILE_NAME: &str = "docsrs_last_accessed";
12+
13+
/// gives you the percentage of free disk space on the
14+
/// filesystem where the given `path` lives on.
15+
/// Return value is between 0 and 1.
16+
fn free_disk_space_ratio<P: AsRef<Path>>(path: P) -> Result<f32> {
17+
let sys = System::new_with_specifics(RefreshKind::new().with_disks());
18+
19+
let disk_by_mount_point: HashMap<_, _> =
20+
sys.disks().iter().map(|d| (d.mount_point(), d)).collect();
21+
22+
let path = path.as_ref();
23+
24+
if let Some(disk) = path.ancestors().find_map(|p| disk_by_mount_point.get(p)) {
25+
Ok((disk.available_space() as f64 / disk.total_space() as f64) as f32)
26+
} else {
27+
bail!("could not find mount point for path {}", path.display());
28+
}
29+
}
30+
831
/// artifact caching with cleanup
932
#[derive(Debug)]
1033
pub(crate) struct ArtifactCache {
@@ -23,10 +46,8 @@ impl ArtifactCache {
2346

2447
/// clean up a target directory.
2548
///
26-
/// Should delete all things that shouldn't leak between
27-
/// builds, so:
28-
/// - doc-output
29-
/// - ...?
49+
/// Will:
50+
/// * delete the doc output in the root & target directories
3051
#[instrument(skip(self))]
3152
fn cleanup(&self, target_dir: &Path) -> Result<()> {
3253
// proc-macro crates have a `doc` directory
@@ -50,6 +71,78 @@ impl ArtifactCache {
5071
Ok(())
5172
}
5273

74+
fn cache_dir_for_key(&self, cache_key: &str) -> PathBuf {
75+
self.cache_dir.join(cache_key)
76+
}
77+
78+
/// update the "last used" marker for the cache key
79+
fn touch(&self, cache_key: &str) -> Result<()> {
80+
let file = self
81+
.cache_dir_for_key(cache_key)
82+
.join(LAST_ACCESSED_FILE_NAME);
83+
84+
fs::create_dir_all(file.parent().expect("we always have a parent"))?;
85+
if file.exists() {
86+
fs::remove_file(&file)?;
87+
}
88+
OpenOptions::new().create(true).write(true).open(&file)?;
89+
Ok(())
90+
}
91+
92+
/// return the list of cache-directories, sorted by last usage.
93+
///
94+
/// The oldest / least used cache will be first.
95+
/// To be used for cleanup.
96+
///
97+
/// A missing age-marker file is interpreted as "old age".
98+
fn all_cache_folders_by_age(&self) -> Result<Vec<PathBuf>> {
99+
let mut entries: Vec<(PathBuf, Option<SystemTime>)> = fs::read_dir(&self.cache_dir)?
100+
.filter_map(Result::ok)
101+
.filter_map(|entry| {
102+
let path = entry.path();
103+
path.is_dir().then(|| {
104+
let last_accessed = path
105+
.join(LAST_ACCESSED_FILE_NAME)
106+
.metadata()
107+
.and_then(|metadata| metadata.modified())
108+
.ok();
109+
(path, last_accessed)
110+
})
111+
})
112+
.collect();
113+
114+
// `None` will appear first after sorting
115+
entries.sort_by_key(|(_, time)| *time);
116+
117+
Ok(entries.into_iter().map(|(path, _)| path).collect())
118+
}
119+
120+
/// free up disk space by deleting the oldest cache folders.
121+
///
122+
/// Deletes cache folders until the `free_percent_goal` is reached.
123+
pub(crate) fn clear_disk_space(&self, free_percent_goal: f32) -> Result<()> {
124+
let space_ok =
125+
|| -> Result<bool> { Ok(free_disk_space_ratio(&self.cache_dir)? >= free_percent_goal) };
126+
127+
if space_ok()? {
128+
return Ok(());
129+
}
130+
131+
for folder in self.all_cache_folders_by_age()? {
132+
warn!(
133+
?folder,
134+
"freeing up disk space by deleting oldest cache folder"
135+
);
136+
fs::remove_dir_all(&folder)?;
137+
138+
if space_ok()? {
139+
break;
140+
}
141+
}
142+
143+
Ok(())
144+
}
145+
53146
/// restore a cached target directory.
54147
///
55148
/// Will just move the cache folder into the rustwide
@@ -67,7 +160,7 @@ impl ArtifactCache {
67160
fs::remove_dir_all(target_dir).context("could not clean target directory")?;
68161
}
69162

70-
let cache_dir = self.cache_dir.join(cache_key);
163+
let cache_dir = self.cache_dir_for_key(cache_key);
71164
if !cache_dir.exists() {
72165
// when there is no existing cache dir,
73166
// we can just create an empty target.
@@ -85,14 +178,15 @@ impl ArtifactCache {
85178
cache_key: &str,
86179
target_dir: P,
87180
) -> Result<()> {
88-
let cache_dir = self.cache_dir.join(cache_key);
181+
let cache_dir = self.cache_dir_for_key(cache_key);
89182
if cache_dir.exists() {
90183
fs::remove_dir_all(&cache_dir)?;
91184
}
92185

93186
debug!(?target_dir, ?cache_dir, "saving artifact cache");
94187
fs::rename(&target_dir, &cache_dir).context("could not move target directory to cache")?;
95188
self.cleanup(&cache_dir)?;
189+
self.touch(cache_key)?;
96190
Ok(())
97191
}
98192
}

0 commit comments

Comments
 (0)