From bc21abbd79ba45a2a770dde28f26479ccc5a2e4e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 3 Sep 2026 07:43:04 -0400 Subject: [PATCH 1/3] Garbage-collect old incremental compilation sessions --- compiler/rustc_incremental/src/persist/fs.rs | 8 +++-- .../run-make/incremental-session-gc/empty.rs | 2 ++ .../run-make/incremental-session-gc/rmake.rs | 33 +++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 tests/run-make/incremental-session-gc/empty.rs create mode 100644 tests/run-make/incremental-session-gc/rmake.rs diff --git a/compiler/rustc_incremental/src/persist/fs.rs b/compiler/rustc_incremental/src/persist/fs.rs index de543ef0c53bc..6dcccccf0d31a 100644 --- a/compiler/rustc_incremental/src/persist/fs.rs +++ b/compiler/rustc_incremental/src/persist/fs.rs @@ -702,8 +702,10 @@ pub(crate) fn garbage_collect_session_directories( lock_file_to_session_dir.items().filter_map(|(lock_file_name, directory_name)| { debug!("garbage_collect_session_directories() - inspecting: {}", directory_name); - if directory_name.as_str() == current_session_directory_name { - // Skipping our own directory is, unfortunately, important for correctness. + if directory_name.as_str() == current_session_directory_name + && !is_finalized(directory_name) + { + // Skipping our own active directory is important for correctness. // // To summarize #147821: we will try to lock directories before deciding they can be // garbage collected, but the ability of `flock::Lock` to detect a lock held *by the @@ -722,6 +724,8 @@ pub(crate) fn garbage_collect_session_directories( // It's not clear that `flock::Lock` can be fixed for this in general, and our own // incremental session directory is the only one which this process may own, so skip // it here and avoid the problem. We know it's not garbage anyway: we're using it. + // Once finalized, its lock is released. Include it in collection so we keep only + // the newest completed session. return None; } diff --git a/tests/run-make/incremental-session-gc/empty.rs b/tests/run-make/incremental-session-gc/empty.rs new file mode 100644 index 0000000000000..da27b7f3463da --- /dev/null +++ b/tests/run-make/incremental-session-gc/empty.rs @@ -0,0 +1,2 @@ +#![feature(no_core)] +#![no_core] diff --git a/tests/run-make/incremental-session-gc/rmake.rs b/tests/run-make/incremental-session-gc/rmake.rs new file mode 100644 index 0000000000000..a8ffe15b26dfb --- /dev/null +++ b/tests/run-make/incremental-session-gc/rmake.rs @@ -0,0 +1,33 @@ +//! Successful sequential builds should retain only the newest incremental session. +//! The current session must participate in garbage collection once it is finalized. + +use std::path::PathBuf; + +use run_make_support::{rfs, rustc, shallow_find_directories}; + +fn main() { + let compile = || { + rustc().input("empty.rs").crate_type("rlib").emit("metadata").incremental("incr").run(); + }; + + compile(); + let mut previous = session_dir(); + rfs::write(previous.join("sentinel"), "previous session"); + + for _ in 0..2 { + compile(); + let current = session_dir(); + assert_ne!(previous, current); + assert!(!previous.exists(), "superseded session was not collected: {previous:?}"); + assert_eq!(rfs::read_to_string(current.join("sentinel")), "previous session"); + previous = current; + } +} + +fn session_dir() -> PathBuf { + let crate_dirs = shallow_find_directories("incr", |_| true); + assert_eq!(crate_dirs.len(), 1); + let sessions = shallow_find_directories(&crate_dirs[0], |_| true); + assert_eq!(sessions.len(), 1, "expected only the newest completed session: {sessions:?}"); + sessions.into_iter().next().unwrap() +} From f178bea2aa40b5b9840caded683a5b4b7f5e208a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 4 Sep 2026 12:34:09 -0400 Subject: [PATCH 2/3] Preserve the current finalized incremental session --- compiler/rustc_incremental/src/persist/fs.rs | 6 +++++- tests/run-make/incremental-session-gc/rmake.rs | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/compiler/rustc_incremental/src/persist/fs.rs b/compiler/rustc_incremental/src/persist/fs.rs index 6dcccccf0d31a..a62250139e36e 100644 --- a/compiler/rustc_incremental/src/persist/fs.rs +++ b/compiler/rustc_incremental/src/persist/fs.rs @@ -724,7 +724,7 @@ pub(crate) fn garbage_collect_session_directories( // It's not clear that `flock::Lock` can be fixed for this in general, and our own // incremental session directory is the only one which this process may own, so skip // it here and avoid the problem. We know it's not garbage anyway: we're using it. - // Once finalized, its lock is released. Include it in collection so we keep only + // Once finalized, its lock is released. Include it in collection so we keep // the newest completed session. return None; } @@ -822,6 +822,10 @@ pub(crate) fn garbage_collect_session_directories( // Delete all but the most recent of the candidates all_except_most_recent(deletion_candidates).into_items().all(|(path, lock)| { + if path.file_name() == Some(current_session_directory_name) { + return true; + } + debug!("garbage_collect_session_directories() - deleting `{}`", path.display()); if let Err(err) = std_fs::remove_dir_all(&path) { diff --git a/tests/run-make/incremental-session-gc/rmake.rs b/tests/run-make/incremental-session-gc/rmake.rs index a8ffe15b26dfb..dd4b300a5ced0 100644 --- a/tests/run-make/incremental-session-gc/rmake.rs +++ b/tests/run-make/incremental-session-gc/rmake.rs @@ -22,6 +22,23 @@ fn main() { assert_eq!(rfs::read_to_string(current.join("sentinel")), "previous session"); previous = current; } + + let crate_dir = previous.parent().unwrap(); + let (prefix, hash) = previous.file_name().unwrap().to_str().unwrap().rsplit_once('-').unwrap(); + let newer = crate_dir.join(format!("s-zzzzzzzzzz-0000000-{hash}")); + rfs::rename(&previous, &newer); + rfs::rename( + crate_dir.join(format!("{prefix}.lock")), + crate_dir.join("s-zzzzzzzzzz-0000000.lock"), + ); + + compile(); + let sessions = shallow_find_directories(crate_dir, |_| true); + assert_eq!(sessions.len(), 2, "{sessions:?}"); + assert!(sessions.contains(&newer)); + let current = sessions.into_iter().find(|session| *session != newer).unwrap(); + assert!(!current.file_name().unwrap().to_str().unwrap().ends_with("-working")); + assert_eq!(rfs::read_to_string(current.join("sentinel")), "previous session"); } fn session_dir() -> PathBuf { From b4361c91eab43264f000d7c6785ee7a541db521f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 11 Sep 2026 12:45:15 -0400 Subject: [PATCH 3/3] Move guard into is_old_enough_to_be_collected branch --- compiler/rustc_incremental/src/persist/fs.rs | 52 ++++++++++---------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/compiler/rustc_incremental/src/persist/fs.rs b/compiler/rustc_incremental/src/persist/fs.rs index a62250139e36e..5254123b1ca11 100644 --- a/compiler/rustc_incremental/src/persist/fs.rs +++ b/compiler/rustc_incremental/src/persist/fs.rs @@ -702,33 +702,6 @@ pub(crate) fn garbage_collect_session_directories( lock_file_to_session_dir.items().filter_map(|(lock_file_name, directory_name)| { debug!("garbage_collect_session_directories() - inspecting: {}", directory_name); - if directory_name.as_str() == current_session_directory_name - && !is_finalized(directory_name) - { - // Skipping our own active directory is important for correctness. - // - // To summarize #147821: we will try to lock directories before deciding they can be - // garbage collected, but the ability of `flock::Lock` to detect a lock held *by the - // same process* varies across file locking APIs. Then, if our own session directory - // has become old enough to be eligible for GC, we are beholden to platform-specific - // details about detecting the our own lock on the session directory. - // - // POSIX `fcntl(F_SETLK)`-style file locks are maintained across a process. On - // systems where this is the mechanism for `flock::Lock`, there is no way to - // discover if an `flock::Lock` has been created in the same process on the same - // file. Attempting to set a lock on the lockfile again will succeed, even if the - // lock was set by another thread, on another file descriptor. Then we would - // garbage collect our own live directory, unable to tell it was locked perhaps by - // this same thread. - // - // It's not clear that `flock::Lock` can be fixed for this in general, and our own - // incremental session directory is the only one which this process may own, so skip - // it here and avoid the problem. We know it's not garbage anyway: we're using it. - // Once finalized, its lock is released. Include it in collection so we keep - // the newest completed session. - return None; - } - let Ok(timestamp) = extract_timestamp_from_session_dir(directory_name) else { debug!( "found session-dir with malformed timestamp: {}", @@ -772,6 +745,31 @@ pub(crate) fn garbage_collect_session_directories( } } } else if is_old_enough_to_be_collected(timestamp) { + if directory_name.as_str() == current_session_directory_name { + // Skipping our own active directory is important for correctness. + // + // To summarize #147821: we will try to lock directories before deciding they can be + // garbage collected, but the ability of `flock::Lock` to detect a lock held *by the + // same process* varies across file locking APIs. Then, if our own session directory + // has become old enough to be eligible for GC, we are beholden to platform-specific + // details about detecting the our own lock on the session directory. + // + // POSIX `fcntl(F_SETLK)`-style file locks are maintained across a process. On + // systems where this is the mechanism for `flock::Lock`, there is no way to + // discover if an `flock::Lock` has been created in the same process on the same + // file. Attempting to set a lock on the lockfile again will succeed, even if the + // lock was set by another thread, on another file descriptor. Then we would + // garbage collect our own live directory, unable to tell it was locked perhaps by + // this same thread. + // + // It's not clear that `flock::Lock` can be fixed for this in general, and our own + // incremental session directory is the only one which this process may own, so skip + // it here and avoid the problem. We know it's not garbage anyway: we're using it. + // Once finalized, its lock is released. Include it in collection so we keep + // the newest completed session. + return None; + } + // When cleaning out "-working" session directories, i.e. // session directories that might still be in use by another // compiler instance, we only look a directories that are