use super::*;
use crate::daemon_core::depgraph::scanner::scan_recursive;
use crate::daemon_core::depgraph::search_paths::IncludeSearchPaths;
use crate::daemon_core::protocol::{ExecCachePolicy, ExecOutputStreams};
use dashmap::mapref::entry::Entry;
static EXEC_STAGE_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
pub(super) struct ExecStagedPlan {
outputs: Vec<(NormalizedPath, NormalizedPath)>,
rewritten_args: Vec<String>,
root: PathBuf,
}
impl ExecStagedPlan {
fn build(
staging_dir: &Path,
args: &[String],
output_files: &[NormalizedPath],
cwd: &Path,
) -> StagedPlanOutcome<Self> {
if !exec_staging_enabled() {
return StagedPlanOutcome::Unsupported(StagedPlanReason::LaneDisabled);
}
if output_files.is_empty() {
return StagedPlanOutcome::Unsupported(StagedPlanReason::NoDeclaredOutputs);
}
let root = staging_dir.join(format!(
".exec-{}-{}",
std::process::id(),
EXEC_STAGE_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
));
if let Err(source) = std::fs::create_dir_all(&root) {
return StagedPlanOutcome::Error(StagedPlanError {
reason: StagedPlanReason::StagingDirectoryCreate,
source,
});
}
let result = (|| {
let mut outputs = Vec::with_capacity(output_files.len());
let mut rewritten_args = args.to_vec();
for declared in output_files {
let requested: NormalizedPath = absolutize(declared.as_path(), cwd).into();
let Some(filename) = requested.file_name() else {
return StagedPlanOutcome::Unsupported(StagedPlanReason::OutputMissingFilename);
};
let staged: NormalizedPath = root.join(filename).into();
if outputs
.iter()
.any(|(_, existing): &(NormalizedPath, NormalizedPath)| existing == &staged)
{
return StagedPlanOutcome::Unsupported(StagedPlanReason::OutputNameCollision);
}
let requested_text = requested.to_string_lossy();
let declared_text = declared.to_string_lossy();
let mut replaced = false;
for arg in &mut rewritten_args {
if arg == requested_text.as_ref() || arg == declared_text.as_ref() {
*arg = staged.to_string_lossy().into_owned();
replaced = true;
}
}
if !replaced {
return StagedPlanOutcome::Unsupported(StagedPlanReason::OutputNotInArguments);
}
outputs.push((requested, staged));
}
StagedPlanOutcome::Enabled(Self {
outputs,
rewritten_args,
root: root.clone(),
})
})();
if !matches!(result, StagedPlanOutcome::Enabled(_)) {
let _ = std::fs::remove_dir_all(&root);
}
result
}
fn staged_paths(&self) -> Vec<NormalizedPath> {
self.outputs
.iter()
.map(|(_, staged)| staged.clone())
.collect()
}
pub(super) fn materialize(&self) -> std::io::Result<StagedMaterializationStats> {
let mut observed = StagedMaterializationStats::default();
for (fault_index, (requested, staged)) in self.outputs.iter().enumerate() {
#[cfg(not(test))]
let _ = fault_index;
#[cfg(test)]
{
inject_staged_fault(
requested.as_path(),
StagedFaultPoint::MaterializeOutput(fault_index),
)
.map_err(|error| materialization_error(error, observed))?;
}
if let Some(parent) = requested.parent() {
std::fs::create_dir_all(parent)
.map_err(|error| materialization_error(error, observed))?;
}
let output = crate::daemon_core::daemon::server::persist::materialize_independent_with_stats(
staged.as_path(),
requested.as_path(),
)
.map_err(|error| materialization_error(error, observed))?;
observed.add(output);
}
self.cleanup()
.map_err(|error| materialization_error(error, observed))?;
Ok(observed)
}
pub(super) fn output_count(&self) -> usize {
self.outputs.len()
}
fn cleanup(&self) -> std::io::Result<()> {
std::fs::remove_dir_all(&self.root).or_else(|error| {
if error.kind() == std::io::ErrorKind::NotFound {
Ok(())
} else {
Err(error)
}
})
}
}
impl Drop for ExecStagedPlan {
fn drop(&mut self) {
let _ = self.cleanup();
}
}
fn exec_staging_enabled() -> bool {
crate::daemon_core::daemon::server::persist::staged_exec_lane_enabled()
}
const EXEC_KEY_DOMAIN: &[u8] = b"zccache-exec-key-v2";
const EXEC_STREAM_CAP_BYTES: usize = 16 * 1024 * 1024;
const EXEC_COALESCE_WAIT_ENV: &str = "ZCCACHE_EXEC_COALESCE_WAIT_MS";
const EXEC_COALESCE_WAIT_DEFAULT_MS: u64 = 60_000;
#[allow(clippy::too_many_arguments)]
pub(super) async fn handle_generic_tool_exec(
state: &Arc<SharedState>,
tool: &Path,
args: &[String],
cwd: &Path,
env: Vec<(String, String)>,
input_files: &[NormalizedPath],
input_extra: Arc<Vec<u8>>,
output_streams: ExecOutputStreams,
output_files: &[NormalizedPath],
tool_hash_override: Option<[u8; 32]>,
cache_policy: ExecCachePolicy,
cwd_in_key: bool,
include_scan_files: &[NormalizedPath],
include_dirs: &[NormalizedPath],
system_include_dirs: &[NormalizedPath],
iquote_dirs: &[NormalizedPath],
depfile: Option<&Path>,
non_deterministic: bool,
key_args_filter: &[String],
) -> Response {
let key_args = match apply_key_args_filter(args, key_args_filter) {
Ok(v) => v,
Err(e) => {
return Response::Error {
message: format!("invalid key-args-filter regex: {e}"),
};
}
};
let tool_id_hash = match tool_hash_override {
Some(bytes) => ContentHash::from_bytes(bytes),
None => match hash_file_via_cache(state, tool) {
Some(h) => h,
None => {
return Response::Error {
message: format!("cannot hash tool {}", tool.display()),
};
}
},
};
let mut input_pairs: Vec<(String, ContentHash)> = Vec::with_capacity(input_files.len());
for input in input_files {
let abs: PathBuf = absolutize(input.as_path(), cwd);
let hash = match hash_file_via_cache(state, &abs) {
Some(h) => h,
None => {
return Response::Error {
message: format!("cannot hash input file {}", abs.display()),
};
}
};
input_pairs.push((normalize_for_key(&abs), hash));
}
input_pairs.sort_by(|a, b| a.0.cmp(&b.0));
let scan_pairs = match run_include_scan(
state,
cwd,
include_scan_files,
include_dirs,
system_include_dirs,
iquote_dirs,
) {
Ok(v) => v,
Err(e) => return Response::Error { message: e },
};
let primary_key = compose_primary_key(
&tool_id_hash,
&key_args,
&env,
cwd,
cwd_in_key,
&input_pairs,
&scan_pairs,
output_files,
&input_extra,
);
let primary_hex = primary_key.to_hex();
let depfile_path = depfile.map(|p| absolutize(p, cwd));
let stored_dep_pairs = if depfile_path.is_some() {
load_depfile_sidecar(state, &primary_hex, cwd)
} else {
None
};
let full_key = match &stored_dep_pairs {
Some(deps) => compose_full_key(&primary_key, deps),
None => primary_key,
};
let full_hex = full_key.to_hex();
let bypass = non_deterministic || matches!(cache_policy, ExecCachePolicy::Bypass);
let lookup_allowed = !bypass
&& matches!(
cache_policy,
ExecCachePolicy::Normal | ExecCachePolicy::ReadOnly
);
let store_allowed = !bypass && matches!(cache_policy, ExecCachePolicy::Normal);
if lookup_allowed {
if let Some(resp) =
try_exec_cache_hit(state, &full_hex, cwd, output_files, output_streams).await
{
return resp;
}
}
let coalesce_guard = if lookup_allowed {
Some(acquire_in_flight(state, &full_hex).await)
} else {
None
};
if let Some(InFlight::WokenByPeer) = coalesce_guard.as_ref().map(|g| g.outcome()) {
if let Some(resp) =
try_exec_cache_hit(state, &full_hex, cwd, output_files, output_streams).await
{
return resp;
}
}
use crate::daemon_core::daemon::staged_stats::{StagedCounter, StagedTiming};
let planning_started = std::time::Instant::now();
state.profiler.staged.count(StagedCounter::PlanAttempted);
let staged_plan_result = ExecStagedPlan::build(state.staging.path(), args, output_files, cwd);
state.profiler.staged.timing(
StagedTiming::Planning,
planning_started.elapsed().as_nanos() as u64,
);
let staged_plan = match staged_plan_result {
StagedPlanOutcome::Enabled(plan) => {
state.profiler.staged.count(StagedCounter::PlanEnabled);
Some(plan)
}
StagedPlanOutcome::Unsupported(reason) => {
state.profiler.staged.count(StagedCounter::PlanUnsupported);
state.profiler.staged.failure(reason.failure());
None
}
StagedPlanOutcome::Error(error) => {
state.profiler.staged.count(StagedCounter::PlanError);
state.profiler.staged.failure(error.reason.failure());
tracing::warn!(
reason = error.reason.id(),
error = %error.source,
"exact-exec staging plan failed; using legacy path"
);
None
}
};
let compiler_args = staged_plan
.as_ref()
.map_or(args, |plan| plan.rewritten_args.as_slice());
let execution_outputs = staged_plan
.as_ref()
.map_or_else(|| output_files.to_vec(), ExecStagedPlan::staged_paths);
for path in &execution_outputs {
let path = path.as_path();
if let Err(error) = break_output_hardlink_before_compile(path) {
tracing::warn!(
event = "exec_output_detach_failed",
path = %path.display(),
error = %error,
"failed to detach generic-tool output before execution"
);
return Response::Error {
message: format!("failed to detach output {}: {error}", path.display()),
};
}
}
let compiler_started = std::time::Instant::now();
let output = match spawn_tool(tool, compiler_args, cwd, &env).await {
Ok(o) => o,
Err(e) => {
return Response::Error {
message: format!("failed to run {}: {e}", tool.display()),
};
}
};
if staged_plan.is_some() {
state.profiler.staged.count(StagedCounter::CompilerStaged);
state.profiler.staged.timing(
StagedTiming::Compiler,
compiler_started.elapsed().as_nanos() as u64,
);
}
let exit_code = output.status.code().unwrap_or(1);
let stdout = Arc::new(if output_streams.stdout {
output.stdout
} else {
Vec::new()
});
let stderr = Arc::new(if output_streams.stderr {
output.stderr
} else {
Vec::new()
});
if staged_plan.is_some() && exit_code != 0 {
return Response::GenericToolExecResult {
exit_code,
stdout,
stderr,
output_files: Vec::new(),
cached: false,
cache_key_hex: full_hex,
};
}
let (captured_outputs, cache_outputs, all_captured) =
snapshot_output_files(output_files, &execution_outputs, cwd);
let mut final_full_hex = full_hex.clone();
let mut depfile_ok = true;
if let Some(df_path) = depfile_path.as_deref() {
if exit_code != 0 {
depfile_ok = false;
} else {
match harvest_depfile(state, df_path, cwd, &input_pairs) {
Ok(dep_pairs) => {
persist_depfile_sidecar(state, &primary_hex, &dep_pairs);
let new_full = compose_full_key(&primary_key, &dep_pairs);
final_full_hex = new_full.to_hex();
}
Err(e) => {
tracing::warn!(
depfile = %df_path.display(),
err = %e,
"depfile parse failed; skipping cache store for this run"
);
depfile_ok = false;
}
}
}
}
let too_large = stdout.len() > EXEC_STREAM_CAP_BYTES || stderr.len() > EXEC_STREAM_CAP_BYTES;
let cacheable_exit = true; let mut staged_publication_failure = None;
let mut staged_cached_artifact = None;
if store_allowed && all_captured && !too_large && depfile_ok && cacheable_exit {
let artifact = ArtifactData {
outputs: cache_outputs,
stdout: Arc::clone(&stdout),
stderr: Arc::clone(&stderr),
exit_code,
};
let staged_sources = staged_plan.as_ref().map(ExecStagedPlan::staged_paths);
match store_exec_artifact(state, final_full_hex.clone(), artifact, staged_sources).await {
Ok(pending) => staged_cached_artifact = pending,
Err(reason) => staged_publication_failure = Some(reason),
}
} else if too_large {
tracing::warn!(
stdout_len = stdout.len(),
stderr_len = stderr.len(),
cap = EXEC_STREAM_CAP_BYTES,
"exec output exceeded cache cap; not storing this run"
);
} else if non_deterministic {
tracing::debug!(
key = %final_full_hex,
"exec marked non-deterministic; not storing"
);
}
if let Some(plan) = staged_plan.as_ref() {
if !all_captured {
return Response::Error {
message: "successful generic tool omitted a staged output".to_string(),
};
}
let salvage_reason = staged_publication_failure.map(StagedPublishFailure::id);
if let Err(error) = materialize_exec_plan_observed(state, plan, salvage_reason) {
return Response::Error {
message: format!("failed to materialize generic tool outputs: {error}"),
};
}
if let Some(cached) = staged_cached_artifact {
state.artifacts.insert(final_full_hex.clone(), cached);
}
}
drop(coalesce_guard);
Response::GenericToolExecResult {
exit_code,
stdout,
stderr,
output_files: captured_outputs,
cached: false,
cache_key_hex: final_full_hex,
}
}
#[allow(clippy::too_many_arguments)]
#[path = "handle_exec_key.rs"]
mod key;
use key::*;
enum InFlight {
Owner,
WokenByPeer,
}
struct InFlightGuardExec {
state: Arc<SharedState>,
key: String,
outcome: InFlight,
}
impl InFlightGuardExec {
fn outcome(&self) -> InFlight {
match self.outcome {
InFlight::Owner => InFlight::Owner,
InFlight::WokenByPeer => InFlight::WokenByPeer,
}
}
}
impl Drop for InFlightGuardExec {
fn drop(&mut self) {
if matches!(self.outcome, InFlight::Owner) {
if let Some((_, notify)) = self.state.in_flight_exec.remove(&self.key) {
notify.notify_waiters();
}
}
}
}
async fn acquire_in_flight(state: &Arc<SharedState>, key_hex: &str) -> InFlightGuardExec {
let key = key_hex.to_string();
let notify_arc = {
match state.in_flight_exec.entry(key.clone()) {
Entry::Occupied(o) => Arc::clone(o.get()),
Entry::Vacant(v) => {
v.insert(Arc::new(Notify::new()));
return InFlightGuardExec {
state: Arc::clone(state),
key,
outcome: InFlight::Owner,
};
}
}
};
let budget = in_flight_wait_budget();
if let CoalesceOutcome::TimedOut =
coalesce_wait(&state.in_flight_exec, &key, notify_arc, budget).await
{
tracing::warn!(
event = "in_flight_exec_wait_timeout",
key = %key,
budget_ms = budget.as_millis() as u64,
"waited past the coalesce budget for the in-flight exec owner of this key; \
the owner may be wedged — running our own copy instead of hanging (issue #971)"
);
crate::daemon_core::core::lifecycle::write_event(
"in_flight_exec_wait_timeout",
serde_json::json!({
"key": key,
"budget_ms": budget.as_millis() as u64,
"reason": "in-flight exec owner did not finish within the coalesce budget; running own copy",
}),
);
}
InFlightGuardExec {
state: Arc::clone(state),
key,
outcome: InFlight::WokenByPeer,
}
}
#[derive(Debug, PartialEq, Eq)]
enum CoalesceOutcome {
Woken,
SlotResolved,
TimedOut,
}
async fn coalesce_wait(
in_flight: &dashmap::DashMap<String, Arc<Notify>>,
key: &str,
notify_arc: Arc<Notify>,
budget: std::time::Duration,
) -> CoalesceOutcome {
let notified = notify_arc.notified();
tokio::pin!(notified);
notified.as_mut().enable();
let still_ours = in_flight
.get(key)
.is_some_and(|cur| Arc::ptr_eq(cur.value(), ¬ify_arc));
if !still_ours {
return CoalesceOutcome::SlotResolved;
}
tokio::select! {
() = notified.as_mut() => CoalesceOutcome::Woken,
() = tokio::time::sleep(budget) => CoalesceOutcome::TimedOut,
}
}
fn in_flight_wait_budget() -> std::time::Duration {
std::env::var(EXEC_COALESCE_WAIT_ENV)
.ok()
.and_then(|v| v.trim().parse::<u64>().ok())
.map(std::time::Duration::from_millis)
.unwrap_or(std::time::Duration::from_millis(
EXEC_COALESCE_WAIT_DEFAULT_MS,
))
}
async fn spawn_tool(
tool: &Path,
args: &[String],
cwd: &Path,
env: &[(String, String)],
) -> std::io::Result<std::process::Output> {
let mut cmd = tokio::process::Command::new(tool);
cmd.args(args).current_dir(cwd);
cmd.env_clear();
for (k, v) in env {
cmd.env(k, v);
}
crate::daemon_core::daemon::process::tokio_command_output_with_priority(&mut cmd, CompilePriority::Normal)
.await
}
fn snapshot_output_files(
output_files: &[NormalizedPath],
actual_paths: &[NormalizedPath],
cwd: &Path,
) -> (Vec<ArtifactOutput>, Vec<ArtifactOutput>, bool) {
let mut captured_outputs: Vec<ArtifactOutput> = Vec::with_capacity(output_files.len());
let mut cache_outputs: Vec<ArtifactOutput> = Vec::with_capacity(output_files.len());
let mut all_captured = true;
for (declared, actual) in output_files.iter().zip(actual_paths) {
let abs: PathBuf = absolutize(actual.as_path(), cwd);
match std::fs::read(&abs) {
Ok(bytes) => {
let payload = ArtifactPayload::Bytes(Arc::new(bytes));
captured_outputs.push(ArtifactOutput {
name: declared.to_string_lossy().into_owned(),
payload: payload.clone(),
});
cache_outputs.push(ArtifactOutput {
name: declared.to_string_lossy().into_owned(),
payload,
});
}
Err(e) => {
all_captured = false;
tracing::warn!(
path = %abs.display(),
err = %e,
"declared output file missing after exec; not caching this output"
);
}
}
}
(captured_outputs, cache_outputs, all_captured)
}
async fn try_exec_cache_hit(
state: &Arc<SharedState>,
key_hex: &str,
cwd: &Path,
output_files: &[NormalizedPath],
output_streams: ExecOutputStreams,
) -> Option<Response> {
let mut entry = lookup_artifact_with_disk_fallback(state, key_hex)?;
entry.last_used = std::time::Instant::now();
let exit_code = entry.meta.exit_code;
let stdout_full = entry.stdout.clone();
let stderr_full = entry.stderr.clone();
let names = Arc::clone(&entry.meta.output_names);
let payloads_loaded = ensure_payloads(&mut entry, &state.artifact_dir, key_hex).is_some();
if !payloads_loaded {
return None;
}
let payloads = Arc::clone(entry.payloads.as_ref()?);
drop(entry);
let mut paired: Vec<(NormalizedPath, &CachedPayload)> = Vec::with_capacity(output_files.len());
for declared in output_files {
let declared_name = declared.to_string_lossy().into_owned();
let idx = names.iter().position(|n| n == &declared_name)?;
let payload = payloads.get(idx)?;
let abs: NormalizedPath = if declared.as_path().is_absolute() {
declared.clone()
} else {
cwd.join(declared.as_path()).into()
};
paired.push((abs, payload));
}
let targets: Vec<(NormalizedPath, NormalizedPath)> = paired
.iter()
.enumerate()
.map(|(i, (abs, _))| {
let cache_file = state.artifact_dir.join(format!("{key_hex}_{i}"));
(abs.clone(), cache_file)
})
.collect();
let payloads_for_write: Vec<CachedPayload> =
paired.into_iter().map(|(_, p)| p.clone()).collect();
let has_staged_payload = payloads_for_write.iter().any(|payload| {
matches!(payload, CachedPayload::File(path) if is_staged_artifact_path(path.as_path()))
});
let materialize_started = std::time::Instant::now();
let observed = write_payloads_par_observed(&targets, &payloads_for_write);
if has_staged_payload {
if !record_staged_hit_materialization(state, targets.len(), materialize_started, observed) {
return None;
}
} else if observed.is_none() {
return None;
}
let response_stdout = if output_streams.stdout {
stdout_full
} else {
Arc::new(Vec::new())
};
let response_stderr = if output_streams.stderr {
stderr_full
} else {
Arc::new(Vec::new())
};
let mut response_outputs: Vec<ArtifactOutput> = Vec::with_capacity(targets.len());
for ((abs, _), payload) in targets.iter().zip(payloads_for_write.iter()) {
let bytes: Arc<Vec<u8>> = match payload {
CachedPayload::Bytes(b) => Arc::clone(b),
CachedPayload::File(p) => match std::fs::read(p.as_path()) {
Ok(b) => Arc::new(b),
Err(_) => Arc::new(Vec::new()),
},
};
response_outputs.push(ArtifactOutput {
name: abs.to_string_lossy().into_owned(),
payload: ArtifactPayload::Bytes(bytes),
});
}
Some(Response::GenericToolExecResult {
exit_code,
stdout: response_stdout,
stderr: response_stderr,
output_files: response_outputs,
cached: true,
cache_key_hex: key_hex.to_string(),
})
}
async fn store_exec_artifact(
state: &Arc<SharedState>,
key_hex: String,
artifact: ArtifactData,
staged_sources: Option<Vec<NormalizedPath>>,
) -> Result<Option<CachedArtifact>, StagedPublishFailure> {
let cached = CachedArtifact::from_artifact_data(&artifact);
if let Some(sources) = staged_sources {
publish_artifact_paths_observed(state, &key_hex, cached.meta.clone(), &sources)?;
return Ok(Some(cached));
}
{
let artifact_dir = state.artifact_dir.clone();
let key_for_persist = key_hex.clone();
let payloads: Vec<Arc<Vec<u8>>> = artifact
.outputs
.iter()
.filter_map(|o| o.payload.as_bytes().cloned())
.collect();
let persist_meta = cached.meta.clone();
if payloads.is_empty() {
state.artifact_store.insert(&key_hex, &persist_meta);
}
let state_ref = Arc::clone(state);
let sem = Arc::clone(&state.persist_semaphore);
tokio::spawn(async move {
#[expect(
clippy::expect_used,
reason = "persist_semaphore is owned by ServerState for the daemon's lifetime; AcquireError here would be a logic bug (semaphore explicitly closed), not a runtime condition"
)]
let _permit = sem
.acquire()
.await
.expect("persist_semaphore is owned by ServerState and never closed");
let written = tokio::task::spawn_blocking(move || {
let _ = persist_artifact_payloads(&artifact_dir, &key_for_persist, &payloads);
(key_for_persist, persist_meta)
})
.await;
if let Ok((kh, meta)) = written {
let _ = state_ref
.index_writer_tx
.send(IndexWriterCommand::Insert(kh, meta));
}
});
}
state.artifacts.insert(key_hex, cached);
Ok(None)
}
fn absolutize(p: &Path, cwd: &Path) -> PathBuf {
if p.is_absolute() {
p.to_path_buf()
} else {
cwd.join(p)
}
}
fn absolutize_norm(p: &NormalizedPath, cwd: &Path) -> NormalizedPath {
let ap = absolutize(p.as_path(), cwd);
NormalizedPath::from(ap.as_path())
}
fn normalize_for_key(path: &Path) -> String {
let s = path.to_string_lossy().into_owned();
s.replace('\\', "/")
}
#[cfg(test)]
#[path = "handle_exec_coalesce_tests.rs"]
mod coalesce_tests;
#[cfg(test)]
#[path = "handle_exec_tests.rs"]
mod tests;