Skip to content

Commit 2492bd2

Browse files
committed
fix: store PathBuf with weak pointers to handle cache clearing (#879)
fixes of #872 The solution is not optimal, I spent the whole day trying to understand synchronization but couldn't figure it out, so resorting to this really bad implementation. The good news is that I came up with a simple failing test for future improvements.
1 parent 9b82017 commit 2492bd2

File tree

6 files changed

+84
-25
lines changed

6 files changed

+84
-25
lines changed

src/cache/cache_impl.rs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ impl<Fs: FileSystem> Cache<Fs> {
5959
let is_node_modules = path.file_name().as_ref().is_some_and(|&name| name == "node_modules");
6060
let inside_node_modules =
6161
is_node_modules || parent.as_ref().is_some_and(|parent| parent.inside_node_modules);
62-
let parent_weak = parent.as_ref().map(|p| Arc::downgrade(&p.0));
62+
let parent_weak = parent.as_ref().map(|p| (Arc::downgrade(&p.0), p.to_path_buf()));
6363
let cached_path = CachedPath(Arc::new(CachedPathImpl::new(
6464
hash,
6565
path.to_path_buf().into_boxed_path(),
@@ -138,7 +138,7 @@ impl<Fs: FileSystem> Cache<Fs> {
138138
let mut path = path.clone();
139139
// Go up directories when the querying path is not a directory
140140
while !self.is_dir(&path, ctx) {
141-
if let Some(cv) = path.parent() {
141+
if let Some(cv) = path.parent(self) {
142142
path = cv;
143143
} else {
144144
break;
@@ -168,7 +168,7 @@ impl<Fs: FileSystem> Cache<Fs> {
168168
if let Some(deps) = &mut ctx.missing_dependencies {
169169
deps.push(package_json_path);
170170
}
171-
return path.parent().map_or(Ok(None), |parent| {
171+
return path.parent(self).map_or(Ok(None), |parent| {
172172
self.find_package_json_impl(&parent, options, ctx)
173173
});
174174
};
@@ -311,18 +311,25 @@ impl<Fs: FileSystem> Cache<Fs> {
311311
visited: &mut StdHashSet<u64, BuildHasherDefault<IdentityHasher>>,
312312
) -> Result<CachedPath, ResolveError> {
313313
// Check cache first - if this path was already canonicalized, return the cached result
314-
if let Some(weak) = path.canonicalized.get() {
315-
return weak.upgrade().map(CachedPath).ok_or_else(|| {
316-
io::Error::new(io::ErrorKind::NotFound, "Cached path no longer exists").into()
317-
});
314+
if let Some((weak, path_buf)) = path.canonicalized.get() {
315+
return weak
316+
.upgrade()
317+
.map(CachedPath)
318+
.or_else(|| {
319+
// Weak pointer upgrade failed - recreate from stored PathBuf
320+
Some(self.value(path_buf))
321+
})
322+
.ok_or_else(|| {
323+
io::Error::new(io::ErrorKind::NotFound, "Cached path no longer exists").into()
324+
});
318325
}
319326

320327
// Check for circular symlink by tracking visited paths in the current canonicalization chain
321328
if !visited.insert(path.hash) {
322329
return Err(io::Error::new(io::ErrorKind::NotFound, "Circular symlink").into());
323330
}
324331

325-
let res = path.parent().map_or_else(
332+
let res = path.parent(self).map_or_else(
326333
|| Ok(path.normalize_root(self)),
327334
|parent| {
328335
self.canonicalize_with_visited(&parent, visited).and_then(|parent_canonical| {
@@ -336,7 +343,7 @@ impl<Fs: FileSystem> Cache<Fs> {
336343
&self.value(&link.normalize()),
337344
visited,
338345
);
339-
} else if let Some(dir) = normalized.parent() {
346+
} else if let Some(dir) = normalized.parent(self) {
340347
// Symlink is relative `../../foo.js`, use the path directory
341348
// to resolve this symlink.
342349
return self.canonicalize_with_visited(
@@ -358,7 +365,7 @@ impl<Fs: FileSystem> Cache<Fs> {
358365

359366
// Cache the result before removing from visited set
360367
// This ensures parent canonicalization results are cached and reused
361-
let _ = path.canonicalized.set(Arc::downgrade(&res.0));
368+
let _ = path.canonicalized.set((Arc::downgrade(&res.0), res.to_path_buf()));
362369

363370
// Remove from visited set when unwinding the recursion
364371
visited.remove(&path.hash);

src/cache/cached_path.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ pub struct CachedPath(pub Arc<CachedPathImpl>);
2121
pub struct CachedPathImpl {
2222
pub hash: u64,
2323
pub path: Box<Path>,
24-
pub parent: Option<Weak<CachedPathImpl>>,
24+
pub parent: Option<(Weak<CachedPathImpl>, PathBuf)>,
2525
pub is_node_modules: bool,
2626
pub inside_node_modules: bool,
2727
pub meta: OnceLock<Option<(/* is_file */ bool, /* is_dir */ bool)>>, // None means not found.
28-
pub canonicalized: OnceLock<Weak<CachedPathImpl>>,
29-
pub node_modules: OnceLock<Option<Weak<CachedPathImpl>>>,
28+
pub canonicalized: OnceLock<(Weak<CachedPathImpl>, PathBuf)>,
29+
pub node_modules: OnceLock<Option<(Weak<CachedPathImpl>, PathBuf)>>,
3030
pub package_json: OnceLock<Option<PackageJsonIndex>>,
3131
/// `tsconfig.json` found at path.
3232
pub tsconfig: OnceLock<Option<Arc<TsConfig>>>,
@@ -40,7 +40,7 @@ impl CachedPathImpl {
4040
path: Box<Path>,
4141
is_node_modules: bool,
4242
inside_node_modules: bool,
43-
parent: Option<Weak<Self>>,
43+
parent: Option<(Weak<Self>, PathBuf)>,
4444
) -> Self {
4545
Self {
4646
hash,
@@ -75,8 +75,14 @@ impl CachedPath {
7575
self.path.to_path_buf()
7676
}
7777

78-
pub(crate) fn parent(&self) -> Option<Self> {
79-
self.0.parent.as_ref().and_then(|weak| weak.upgrade().map(CachedPath))
78+
pub(crate) fn parent<Fs: FileSystem>(&self, cache: &Cache<Fs>) -> Option<Self> {
79+
self.0.parent.as_ref().and_then(|(weak, path_buf)| {
80+
weak.upgrade().map(CachedPath).or_else(|| {
81+
// Weak pointer upgrade failed - parent was cleared from cache
82+
// Recreate it from the stored PathBuf
83+
Some(cache.value(path_buf))
84+
})
85+
})
8086
}
8187

8288
pub(crate) fn is_node_modules(&self) -> bool {
@@ -104,10 +110,16 @@ impl CachedPath {
104110
) -> Option<Self> {
105111
self.node_modules
106112
.get_or_init(|| {
107-
self.module_directory("node_modules", cache, ctx).map(|cp| Arc::downgrade(&cp.0))
113+
self.module_directory("node_modules", cache, ctx)
114+
.map(|cp| (Arc::downgrade(&cp.0), cp.to_path_buf()))
108115
})
109116
.as_ref()
110-
.and_then(|weak| weak.upgrade().map(CachedPath))
117+
.and_then(|(weak, path_buf)| {
118+
weak.upgrade().map(CachedPath).or_else(|| {
119+
// Weak pointer upgrade failed - recreate from stored PathBuf
120+
Some(cache.value(path_buf))
121+
})
122+
})
111123
}
112124

113125
pub(crate) fn add_extension<Fs: FileSystem>(&self, ext: &str, cache: &Cache<Fs>) -> Self {

src/lib.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
309309
// Go up directories when the querying path is not a directory
310310
let mut cp = cached_path.clone();
311311
if !self.cache.is_dir(&cp, ctx)
312-
&& let Some(cv) = cp.parent()
312+
&& let Some(cv) = cp.parent(&self.cache)
313313
{
314314
cp = cv;
315315
}
@@ -319,7 +319,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
319319
break;
320320
}
321321
// Skip /node_modules/@scope/package.json
322-
if let Some(parent) = p.parent()
322+
if let Some(parent) = p.parent(&self.cache)
323323
&& parent.is_node_modules()
324324
&& let Some(filename) = p.path().file_name()
325325
&& filename.as_encoded_bytes().starts_with(b"@")
@@ -329,7 +329,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
329329
if let Some(package_json) = self.cache.get_package_json(&p, &self.options, ctx)? {
330330
last = Some(package_json);
331331
}
332-
cp = p.parent();
332+
cp = p.parent(&self.cache);
333333
}
334334
Ok(last)
335335
} else {
@@ -876,7 +876,8 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
876876
// 1. let DIRS = NODE_MODULES_PATHS(START)
877877
// 2. for each DIR in DIRS:
878878
for module_name in &self.options.modules {
879-
for cached_path in std::iter::successors(Some(cached_path.clone()), CachedPath::parent)
879+
for cached_path in
880+
std::iter::successors(Some(cached_path.clone()), |cp| cp.parent(&self.cache))
880881
{
881882
// Skip if /path/to/node_modules does not exist
882883
if !self.cache.is_dir(&cached_path, ctx) {
@@ -913,7 +914,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
913914
// Skip if the directory lead to the scope package does not exist
914915
// i.e. `foo/node_modules/@scope` is not a directory for `foo/node_modules/@scope/package`
915916
if package_name.starts_with('@')
916-
&& let Some(path) = cached_path.parent().as_ref()
917+
&& let Some(path) = cached_path.parent(&self.cache).as_ref()
917918
&& !self.cache.is_dir(path, ctx)
918919
{
919920
continue;
@@ -1426,7 +1427,8 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
14261427

14271428
// 11. While parentURL is not the file system root,
14281429
for module_name in &self.options.modules {
1429-
for cached_path in std::iter::successors(Some(cached_path.clone()), CachedPath::parent)
1430+
for cached_path in
1431+
std::iter::successors(Some(cached_path.clone()), |cp| cp.parent(&self.cache))
14301432
{
14311433
// 1. Let packageURL be the URL resolution of "node_modules/" concatenated with packageSpecifier, relative to parentURL.
14321434
let Some(cached_path) = self.get_module_directory(&cached_path, module_name, ctx)

src/tests/clear_cache.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use std::{env, fs};
2+
3+
use rayon::prelude::*;
4+
5+
use crate::Resolver;
6+
7+
#[test]
8+
fn test_parallel_resolve_with_clear_cache() {
9+
let target_dir = env::current_dir().unwrap().join("./target");
10+
let test_dir = target_dir.join("test_clear_cache");
11+
let node_modules = test_dir.join("node_modules");
12+
13+
let _ = fs::remove_dir_all(&test_dir);
14+
fs::create_dir_all(&node_modules).unwrap();
15+
16+
let packages: Vec<String> = (1..=100).map(|i| format!("package_{i}")).collect();
17+
for package in &packages {
18+
let package_dir = node_modules.join(package);
19+
fs::create_dir_all(&package_dir).unwrap();
20+
let package_json = format!(r#"{{"name": "{package}", "main": "index.js"}}"#);
21+
fs::write(package_dir.join("package.json"), package_json).unwrap();
22+
fs::write(package_dir.join("index.js"), "").unwrap();
23+
}
24+
25+
let resolver = Resolver::default();
26+
for _ in 1..100 {
27+
packages.par_iter().enumerate().for_each(|(i, package)| {
28+
if i % 10 == 0 && i > 0 {
29+
resolver.clear_cache();
30+
}
31+
let result = resolver.resolve(&test_dir, package);
32+
assert!(result.is_ok(), "Failed to resolve {package}: {result:?}");
33+
});
34+
}
35+
36+
let _ = fs::remove_dir_all(&test_dir);
37+
}

src/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod alias;
22
mod browser_field;
33
mod builtins;
4+
mod clear_cache;
45
mod dependencies;
56
mod exports_field;
67
mod extension_alias;

src/tsconfig_resolver.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
120120
})? {
121121
return Ok(Some(Arc::clone(tsconfig)));
122122
}
123-
cache_value = cv.parent();
123+
cache_value = cv.parent(&self.cache);
124124
}
125125
Ok(None)
126126
}

0 commit comments

Comments
 (0)