turbo_tasks_backend/database/
db_versioning.rs1use std::{
2 env,
3 ffi::{OsStr, OsString},
4 fs::{DirEntry, read_dir, remove_dir_all, rename},
5 path::{Path, PathBuf},
6 time::Duration,
7};
8
9use anyhow::Result;
10
11pub struct GitVersionInfo<'a> {
17 pub describe: &'a str,
19 pub dirty: bool,
22}
23
24const DEFAULT_MAX_OTHER_DB_VERSIONS: usize = 2;
28
29const DELETION_PREFIX: &str = "__stale_";
32
33pub fn handle_db_versioning(
43 base_path: &Path,
44 version_info: &GitVersionInfo,
45 is_ci: bool,
46) -> Result<PathBuf> {
47 if let Ok(version) = env::var("TURBO_ENGINE_VERSION") {
48 return Ok(base_path.join(version));
49 }
50 let ignore_dirty = env::var("TURBO_ENGINE_IGNORE_DIRTY").ok().is_some();
51 let disabled_versioning = env::var("TURBO_ENGINE_DISABLE_VERSIONING").ok().is_some();
52 let version = if disabled_versioning {
53 println!(
54 "WARNING: Persistent Caching versioning is disabled. Manual removal of the persistent \
55 caching database might be required."
56 );
57 Some("unversioned")
58 } else if !version_info.dirty {
59 Some(version_info.describe)
60 } else if ignore_dirty {
61 println!(
62 "WARNING: The git repository is dirty, but Persistent Caching is still enabled. \
63 Manual removal of the persistent caching database might be required."
64 );
65 Some(version_info.describe)
66 } else {
67 println!(
68 "WARNING: The git repository is dirty: Persistent Caching is disabled. Use \
69 TURBO_ENGINE_IGNORE_DIRTY=1 to ignore dirtiness of the repository."
70 );
71 None
72 };
73 let path;
74 if let Some(version) = version {
75 path = base_path.join(version);
76
77 let max_other_db_versions = if is_ci {
78 0
79 } else {
80 DEFAULT_MAX_OTHER_DB_VERSIONS
81 };
82
83 if let Ok(read_dir) = read_dir(base_path) {
84 let mut old_dbs = Vec::new();
85 for entry in read_dir {
86 let Ok(entry) = entry else { continue };
87
88 let name = entry.file_name();
90 if name == version {
91 continue;
92 }
93
94 let Ok(file_type) = entry.file_type() else {
96 continue;
97 };
98 if !file_type.is_dir() {
99 continue;
100 }
101
102 if name
104 .as_encoded_bytes()
105 .starts_with(AsRef::<OsStr>::as_ref(DELETION_PREFIX).as_encoded_bytes())
106 {
107 let _ = remove_dir_all(entry.path());
109 continue;
110 }
111
112 old_dbs.push(entry);
113 }
114
115 if old_dbs.len() > max_other_db_versions {
116 old_dbs.sort_by_cached_key(|entry| {
117 fn get_age(e: &DirEntry) -> Result<Duration> {
118 let m = e.metadata()?;
119 Ok(m.accessed().or_else(|_| m.modified())?.elapsed()?)
126 }
127 get_age(entry).unwrap_or(Duration::MAX)
128 });
129 for entry in old_dbs.into_iter().skip(max_other_db_versions) {
130 let mut new_name = OsString::from(DELETION_PREFIX);
131 new_name.push(entry.file_name());
132 let new_path = base_path.join(new_name);
133 let rename_result = rename(entry.path(), &new_path);
135 if rename_result.is_ok() {
138 let _ = remove_dir_all(&new_path);
140 }
141 }
142 }
143 }
144 } else {
145 path = base_path.join("temp");
146 if path.exists() {
147 remove_dir_all(&path)?;
150 }
151 }
152
153 Ok(path)
154}
155
156#[cfg(test)]
157mod tests {
158 use std::{fs, thread::sleep};
159
160 use rstest::rstest;
161 use tempfile::TempDir;
162
163 use super::*;
164
165 fn count_entries(base_path: &Path) -> usize {
166 fs::read_dir(base_path)
167 .unwrap()
168 .collect::<Result<Vec<_>, _>>()
169 .unwrap()
170 .len()
171 }
172
173 #[rstest]
174 #[case::not_ci(false, DEFAULT_MAX_OTHER_DB_VERSIONS)]
175 #[case::ci(true, 0)]
176 fn test_max_versions(#[case] is_ci: bool, #[case] max_other_db_versions: usize) {
177 let tmp_dir = TempDir::new().unwrap();
178 let base_path = tmp_dir.path();
179 let current_version_name = "mock-version";
180
181 let version_info = GitVersionInfo {
182 describe: current_version_name,
183 dirty: false,
184 };
185
186 fs::create_dir(base_path.join(current_version_name)).unwrap();
187
188 sleep(Duration::from_millis(100));
191
192 let num_other_dirs = max_other_db_versions + 3;
193 for i in 0..num_other_dirs {
194 fs::create_dir(base_path.join(format!("other-dir-{i}"))).unwrap();
195 }
196
197 assert_eq!(
198 count_entries(base_path),
199 num_other_dirs + 1, );
201
202 let versioned_path = handle_db_versioning(base_path, &version_info, is_ci).unwrap();
203
204 assert_eq!(versioned_path, base_path.join(current_version_name));
205 assert!(base_path.join(current_version_name).exists());
206 assert_eq!(
207 count_entries(base_path),
208 max_other_db_versions + 1, );
210 }
211
212 #[test]
213 fn test_cleanup_of_prefixed_items() {
214 let tmp_dir = TempDir::new().unwrap();
215 let base_path = tmp_dir.path();
216 let current_version_name = "mock-version";
217
218 let version_info = GitVersionInfo {
219 describe: current_version_name,
220 dirty: false,
221 };
222
223 for i in 0..5 {
224 fs::create_dir(base_path.join(format!("{DELETION_PREFIX}other-dir-{i}"))).unwrap();
225 }
226
227 assert_eq!(count_entries(base_path), 5);
228
229 handle_db_versioning(base_path, &version_info, false).unwrap();
230
231 assert_eq!(count_entries(base_path), 0);
232 }
233}