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: File System Cache versioning is disabled. Manual removal of the filesystem \
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 File System Cache is still enabled. Manual \
63             removal of the filesystem cache database might be required."
64        );
65        Some(version_info.describe)
66    } else {
67        println!(
68            "WARNING: The git repository is dirty: File System Cache 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}