From 531c09c1e09a02bdfc6cfe1b73dbb037da891a29 Mon Sep 17 00:00:00 2001 From: Craigory Coppola Date: Mon, 23 Mar 2026 17:38:59 -0400 Subject: [PATCH 1/2] fix(core): runtime inputs shouldn't be cached at task_hasher layer and filesets should be in the hash_plans layer --- .../src/native/tasks/hashers/hash_runtime.rs | 8 +- packages/nx/src/native/tasks/task_hasher.rs | 86 +++++++++++++------ 2 files changed, 64 insertions(+), 30 deletions(-) diff --git a/packages/nx/src/native/tasks/hashers/hash_runtime.rs b/packages/nx/src/native/tasks/hashers/hash_runtime.rs index 0bd0e840b75..faa9a6331e6 100644 --- a/packages/nx/src/native/tasks/hashers/hash_runtime.rs +++ b/packages/nx/src/native/tasks/hashers/hash_runtime.rs @@ -2,14 +2,13 @@ use crate::native::hasher::hash; use crate::native::utils::command::create_shell_command; use dashmap::DashMap; use std::collections::HashMap; -use std::sync::Arc; use tracing::trace; pub fn hash_runtime( workspace_root: &str, command: &str, env: &HashMap, - cache: Arc>, + cache: &DashMap, ) -> anyhow::Result { let cache_key = runtime_cache_key(command, env); @@ -51,16 +50,15 @@ mod tests { use super::*; use dashmap::DashMap; use std::collections::HashMap; - use std::sync::Arc; #[test] fn test_hash_runtime() { let workspace_root = if cfg!(windows) { "C:\\" } else { "/tmp" }; let command = "echo runtime"; let env: HashMap = HashMap::new(); - let cache = Arc::new(DashMap::new()); + let cache = DashMap::new(); - let result = hash_runtime(workspace_root, command, &env, Arc::clone(&cache)).unwrap(); + let result = hash_runtime(workspace_root, command, &env, &cache).unwrap(); assert_eq!(result, "10571312846059850300"); } diff --git a/packages/nx/src/native/tasks/task_hasher.rs b/packages/nx/src/native/tasks/task_hasher.rs index f1caa4feb04..52c698042b0 100644 --- a/packages/nx/src/native/tasks/task_hasher.rs +++ b/packages/nx/src/native/tasks/task_hasher.rs @@ -139,6 +139,14 @@ pub struct HasherOptions { pub selectively_hash_ts_config: bool, } +/// Cached result for project/workspace file hashing. +/// Stores both the hash value and the matched file paths (for input collection). +#[derive(Clone)] +struct CachedFileSetHash { + hash: String, + files: Vec, +} + #[napi] pub struct TaskHasher { workspace_root: String, @@ -150,7 +158,6 @@ pub struct TaskHasher { root_tsconfig_path: Option, options: Option, external_cache: Arc>, - runtime_cache: Arc>, } #[napi] impl TaskHasher { @@ -181,7 +188,6 @@ impl TaskHasher { root_tsconfig_path, options, external_cache: Arc::new(DashMap::new()), - runtime_cache: Arc::new(DashMap::new()), } } @@ -193,10 +199,13 @@ impl TaskHasher { cwd: String, collect_task_inputs: Option, ) -> anyhow::Result> { - // Create a fresh task output cache for this invocation + // Create fresh caches for this invocation. // This ensures no stale caches across multiple CLI commands when the daemon holds - // the TaskHasher instance + // the TaskHasher instance. let task_output_cache = DashMap::new(); + let runtime_cache: DashMap = DashMap::new(); + let project_file_set_cache: DashMap = DashMap::new(); + let workspace_file_set_cache: DashMap = DashMap::new(); let should_collect_inputs = collect_task_inputs.unwrap_or(false); let function_start = std::time::Instant::now(); @@ -252,6 +261,9 @@ impl TaskHasher { sorted_externals: &sorted_externals, selectively_hash_tsconfig, task_output_cache: &task_output_cache, + runtime_cache: &runtime_cache, + project_file_set_cache: &project_file_set_cache, + workspace_file_set_cache: &workspace_file_set_cache, cwd: cwd_path, collect_inputs: should_collect_inputs, }, @@ -326,6 +338,9 @@ impl TaskHasher { sorted_externals, selectively_hash_tsconfig, task_output_cache, + runtime_cache, + project_file_set_cache, + workspace_file_set_cache, cwd, collect_inputs, }: HashInstructionArgs, @@ -335,29 +350,36 @@ impl TaskHasher { let empty = HashInputsBuilder::default(); let (hash, inputs) = match instruction { HashInstruction::WorkspaceFileSet(workspace_file_set) => { - let result = hash_workspace_files_with_inputs( - workspace_file_set, - &self.all_workspace_files, - )?; + let cache_key = instruction.to_string(); + // Check cache first; clone and drop the Ref before any insert + let cached_entry = if let Some(entry) = workspace_file_set_cache.get(&cache_key) { + entry.clone() + } else { + let result = hash_workspace_files_with_inputs( + workspace_file_set, + &self.all_workspace_files, + )?; + let entry = CachedFileSetHash { + hash: result.hash, + files: result.files, + }; + workspace_file_set_cache.insert(cache_key, entry.clone()); + entry + }; trace!(parent: &span, "hash_workspace_files: {:?}", now.elapsed()); let inputs = if collect_inputs { HashInputsBuilder { - files: result.files.into_iter().collect(), + files: cached_entry.files.into_iter().collect(), ..Default::default() } } else { - drop(result.files); empty }; - (result.hash, inputs) + (cached_entry.hash, inputs) } HashInstruction::Runtime(runtime) => { - let hashed_runtime = hash_runtime( - &self.workspace_root, - runtime, - js_env, - Arc::clone(&self.runtime_cache), - )?; + let hashed_runtime = + hash_runtime(&self.workspace_root, runtime, js_env, runtime_cache)?; trace!(parent: &span, "hash_runtime: {:?}", now.elapsed()); let inputs = if collect_inputs { instruction.into() @@ -383,22 +405,33 @@ impl TaskHasher { (hashed_cwd, empty) } HashInstruction::ProjectFileSet(project_name, file_sets) => { - let result = hash_project_files_with_inputs( - project_name, - file_sets, - &self.project_file_map, - )?; + let cache_key = instruction.to_string(); + // Check cache first; clone and drop the Ref before any insert + let cached_entry = if let Some(entry) = project_file_set_cache.get(&cache_key) { + entry.clone() + } else { + let result = hash_project_files_with_inputs( + project_name, + file_sets, + &self.project_file_map, + )?; + let entry = CachedFileSetHash { + hash: result.hash, + files: result.files, + }; + project_file_set_cache.insert(cache_key, entry.clone()); + entry + }; trace!(parent: &span, "hash_project_files: {:?}", now.elapsed()); let inputs = if collect_inputs { HashInputsBuilder { - files: result.files.into_iter().collect(), + files: cached_entry.files.into_iter().collect(), ..Default::default() } } else { - drop(result.files); empty }; - (result.hash, inputs) + (cached_entry.hash, inputs) } HashInstruction::ProjectConfiguration(project_name) => { let hashed_project_config = @@ -514,6 +547,9 @@ struct HashInstructionArgs<'a> { sorted_externals: &'a [&'a String], selectively_hash_tsconfig: bool, task_output_cache: &'a DashMap, + runtime_cache: &'a DashMap, + project_file_set_cache: &'a DashMap, + workspace_file_set_cache: &'a DashMap, cwd: &'a std::path::Path, collect_inputs: bool, } From b56220146539fc6ef8c8f4e4b5a75a6d4d9a75c1 Mon Sep 17 00:00:00 2001 From: "nx-cloud[bot]" <71083854+nx-cloud[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:28:39 +0000 Subject: [PATCH 2/2] fix(core): runtime inputs shouldn't be cached at task_hasher layer and filesets should be in the hash_plans layer [Self-Healing CI Rerun]