From 7e04e5ab8b72d601c1664ed057a52e7e0710fcb9 Mon Sep 17 00:00:00 2001 From: Thiago Riemma Carbonera Date: Mon, 13 Jul 2026 18:41:18 -0300 Subject: [PATCH] Resolving merge conflicts --- docs/notes/2.34.x.md | 2 ++ src/rust/fs/store/src/immutable_inputs.rs | 18 ++++++++++- src/rust/fs/store/src/local_tests.rs | 38 ++++++++++++++++++++++- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/docs/notes/2.34.x.md b/docs/notes/2.34.x.md index a7bf7120561..ce485f2a839 100644 --- a/docs/notes/2.34.x.md +++ b/docs/notes/2.34.x.md @@ -20,6 +20,8 @@ Fixed a bug where `SingleSourceField` could not be hydrated for generated target Fixed an issue where `pants --changed-since` would unnecessarily invalidate all targets in a `BUILD` file when only whitespace or comment lines were modified. +Fixed a bug where `pantsd` would crash on macOS (during Docker builds) because it retained cached paths to temporary directories that had been deleted by the OS. + ### Goals ### Backends diff --git a/src/rust/fs/store/src/immutable_inputs.rs b/src/rust/fs/store/src/immutable_inputs.rs index 94368c9bd06..f46a764c382 100644 --- a/src/rust/fs/store/src/immutable_inputs.rs +++ b/src/rust/fs/store/src/immutable_inputs.rs @@ -62,7 +62,23 @@ impl ImmutableInputs { directory_digest: DirectoryDigest, ) -> Result { let digest = directory_digest.as_digest(); - let cell = self.0.contents.lock().entry(digest).or_default().clone(); + // we must verify the memoized path still exists on disk. + // If it was deleted, we invalidate the cache and force re-materialization. + let cell = { + let mut contents = self.0.contents.lock(); + let cell = contents.entry(digest).or_default().clone(); + if let Some(path) = cell.get() { + if path.exists() { + cell + } else { + let new_cell = Arc::new(OnceCell::new()); + contents.insert(digest, new_cell.clone()); + new_cell + } + } else { + cell + } + }; // We (might) need to initialize the value. // diff --git a/src/rust/fs/store/src/local_tests.rs b/src/rust/fs/store/src/local_tests.rs index 565d8037a1e..a2510da34e0 100644 --- a/src/rust/fs/store/src/local_tests.rs +++ b/src/rust/fs/store/src/local_tests.rs @@ -1,7 +1,7 @@ // Copyright 2022 Pants project contributors (see CONTRIBUTORS.md). // Licensed under the Apache License, Version 2.0 (see LICENSE). use crate::local::ByteStore; -use crate::{EntryType, LocalOptions, ShrinkBehavior}; +use crate::{EntryType, ImmutableInputs, LocalOptions, ShrinkBehavior, Store}; use std::collections::HashSet; use std::io::Write; @@ -9,6 +9,7 @@ use std::path::Path; use std::time::Duration; use bytes::{BufMut, Bytes, BytesMut}; +use fs::DirectoryDigest; use hashing::{Digest, Fingerprint}; use tempfile::{NamedTempFile, TempDir}; use testutil::data::{TestData, TestDirectory}; @@ -807,3 +808,38 @@ fn get_directory_size(path: &Path) -> usize { } len } + +#[tokio::test] +async fn recovers_from_deleted_directory_issue_23411() { + let executor = task_executor::Executor::new(); + let store_dir = TempDir::new().unwrap(); + let store = Store::local_only(executor, store_dir.path()).unwrap(); + + let base_dir = TempDir::new().unwrap(); + let immutable_inputs = ImmutableInputs::new(store.clone(), base_dir.path()).unwrap(); + + let digest = DirectoryDigest::from_persisted_digest(TestDirectory::empty().digest()); + + let path1 = immutable_inputs.path_for_dir(digest.clone()).await.unwrap(); + assert!( + path1.exists(), + "The directory should have been created the first time" + ); + + std::fs::remove_dir_all(&path1).unwrap(); + assert!( + !path1.exists(), + "The directory was physically deleted by the 'reaper'" + ); + + let path2 = immutable_inputs.path_for_dir(digest).await.unwrap(); + + assert!( + path2.exists(), + "The code must have detected the absence and recreated the directory on the disk" + ); + assert_ne!( + path1, path2, + "The returned path will be different because a new random chroot has been generated" + ); +}