Skip to content

Commit 136a98b

Browse files
committed
feat(cache): fingerprint tracked getEnv reads
Motivation: Once tools can read env values from the runner, cached tasks must remember tracked getEnv reads or a later run can replay stale output after the env changes. Scope: Make the getEnv tracked option meaningful, record served single-env values in IPC reports, store tracked getEnv values in the post-run fingerprint, validate them during cache lookup, and render env-specific cache miss messages. This PR intentionally does not implement getEnvs or env glob match-set tracking. Verification: - cargo test -p vite_task_server --test integration - UPDATE_SNAPSHOTS=1 cargo test -p vite_task_bin --test e2e_snapshots fetch_env_tracked_invalidates_on_change -- --ignored - UPDATE_SNAPSHOTS=1 cargo test -p vite_task_bin --test e2e_snapshots fetch_env_tracks_with_explicit_inputs -- --ignored - cargo test -p vite_task_bin --test e2e_snapshots fetch_env_tracked_invalidates_on_change -- --ignored - cargo test -p vite_task_bin --test e2e_snapshots fetch_env_tracks_with_explicit_inputs -- --ignored
1 parent 7bf34a6 commit 136a98b

17 files changed

Lines changed: 336 additions & 86 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Changelog
22

3+
- **Added** Runner-aware `getEnv` reads can now participate in task cache fingerprints, so changing a tool-served env value invalidates the cache and names the env var in the miss message.
34
- **Added** Runner-aware tools can now opt the current task run out of caching through the new IPC channel; Vite dev server integration uses this automatically ([#441](https://github.com/voidzero-dev/vite-task/pull/441))
45
- **Fixed** Prefix environment assignments like `PATH=... command` now affect executable lookup during task planning, so tools provided only by the prefixed `PATH` can be resolved correctly ([#440](https://github.com/voidzero-dev/vite-task/pull/440))
56
- **Changed** Cache misses caused by a tracked env var now name the env var inline, for example `cache miss: env 'NODE_ENV' changed`, instead of the generic `envs changed` message ([#438](https://github.com/voidzero-dev/vite-task/pull/438))

crates/vite_task/src/session/cache/display.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,9 @@ pub fn format_cache_status_inline(cache_status: &CacheStatus) -> Option<Str> {
195195
FingerprintMismatch::InputChanged { kind, path } => {
196196
format_input_change_str(*kind, path.as_str())
197197
}
198+
FingerprintMismatch::TrackedEnvChanged(mismatch) => {
199+
format_env_changed_inline(&[mismatch.name()])
200+
}
198201
};
199202
Some(vite_str::format!("○ cache miss: {reason}, executing"))
200203
}

crates/vite_task/src/session/cache/mod.rs

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
pub mod archive;
44
pub mod display;
55

6-
use std::{collections::BTreeMap, fmt::Display, fs::File, io::Write, sync::Arc, time::Duration};
6+
use std::{
7+
collections::BTreeMap, ffi::OsStr, fmt::Display, fs::File, io::Write, sync::Arc, time::Duration,
8+
};
79

810
// Re-export display functions for convenience
911
pub use display::format_cache_status_inline;
@@ -12,6 +14,7 @@ pub use display::{
1214
format_spawn_change,
1315
};
1416
use rusqlite::{Connection, OptionalExtension as _};
17+
use rustc_hash::FxHashMap;
1518
use serde::{Deserialize, Serialize};
1619
use tokio::sync::Mutex;
1720
use vite_path::{AbsolutePath, RelativePathBuf};
@@ -165,6 +168,23 @@ impl EnvMismatch {
165168
}
166169
}
167170
}
171+
172+
/// Compare a stored env value against the current one, returning the
173+
/// mismatch if they differ. `None` on either side means the env is unset
174+
/// there; two unset or two equal values are not a mismatch.
175+
#[must_use]
176+
pub fn compare(name: &Str, stored: Option<&Str>, current: Option<&Str>) -> Option<Self> {
177+
match (stored, current) {
178+
(None, Some(value)) => Some(Self::Added { name: name.clone(), value: value.clone() }),
179+
(Some(value), None) => Some(Self::Removed { name: name.clone(), value: value.clone() }),
180+
(Some(old_value), Some(new_value)) if old_value != new_value => Some(Self::Changed {
181+
name: name.clone(),
182+
old_value: old_value.clone(),
183+
new_value: new_value.clone(),
184+
}),
185+
_ => None,
186+
}
187+
}
168188
}
169189

170190
impl Display for EnvMismatch {
@@ -198,23 +218,16 @@ pub enum FingerprintMismatch {
198218
kind: InputChangeKind,
199219
path: RelativePathBuf,
200220
},
221+
/// A runner-aware tool-tracked env var changed between runs.
222+
TrackedEnvChanged(EnvMismatch),
201223
}
202224

203-
impl Display for FingerprintMismatch {
204-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205-
match self {
206-
Self::SpawnFingerprint { old, new } => {
207-
write!(f, "Spawn fingerprint changed: old={old:?}, new={new:?}")
208-
}
209-
Self::InputConfig => {
210-
write!(f, "input configuration changed")
211-
}
212-
Self::OutputConfig => {
213-
write!(f, "output configuration changed")
214-
}
215-
Self::InputChanged { kind, path } => {
216-
write!(f, "{}", display::format_input_change_str(*kind, path.as_str()))
217-
}
225+
impl From<crate::session::execute::fingerprint::PostRunMismatch> for FingerprintMismatch {
226+
fn from(mismatch: crate::session::execute::fingerprint::PostRunMismatch) -> Self {
227+
use crate::session::execute::fingerprint::PostRunMismatch;
228+
match mismatch {
229+
PostRunMismatch::InputChanged { kind, path } => Self::InputChanged { kind, path },
230+
PostRunMismatch::TrackedEnvChanged(mismatch) => Self::TrackedEnvChanged(mismatch),
218231
}
219232
}
220233
}
@@ -240,7 +253,7 @@ pub fn split_path(path: &str) -> (Option<&str>, &str) {
240253
/// its own cache warm across branch switches, and a cache from a different
241254
/// version is simply ignored (it lives in a directory this build never looks
242255
/// at) rather than aborting the run. Bumping the version starts a fresh cache.
243-
const CACHE_SCHEMA_VERSION: u32 = 13;
256+
const CACHE_SCHEMA_VERSION: u32 = 14;
244257

245258
/// Name of the per-version subdirectory (e.g. `v13`) under the task-cache
246259
/// directory that holds the database and output archives for the current
@@ -292,6 +305,7 @@ impl ExecutionCache {
292305
cache_metadata: &CacheMetadata,
293306
globbed_inputs: &BTreeMap<RelativePathBuf, u64>,
294307
workspace_root: &AbsolutePath,
308+
envs: &FxHashMap<Arc<OsStr>, Arc<OsStr>>,
295309
) -> anyhow::Result<Result<CacheEntryValue, CacheMiss>> {
296310
let spawn_fingerprint = &cache_metadata.spawn_fingerprint;
297311
let execution_cache_key = &cache_metadata.execution_cache_key;
@@ -307,11 +321,11 @@ impl ExecutionCache {
307321
return Ok(Err(CacheMiss::FingerprintMismatch(mismatch)));
308322
}
309323

310-
// Validate post-run fingerprint (inferred inputs from fspy)
311-
if let Some((kind, path)) = cache_value.post_run_fingerprint.validate(workspace_root)? {
312-
return Ok(Err(CacheMiss::FingerprintMismatch(
313-
FingerprintMismatch::InputChanged { kind, path },
314-
)));
324+
// Validate post-run fingerprint (inferred inputs + tracked envs)
325+
if let Some(mismatch) =
326+
cache_value.post_run_fingerprint.validate(workspace_root, envs)?
327+
{
328+
return Ok(Err(CacheMiss::FingerprintMismatch(mismatch.into())));
315329
}
316330
// Associate the execution key to the cache entry key if not already,
317331
// so that next time we can find it and report what changed

crates/vite_task/src/session/execute/cache_update.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Post-run cache update: decide whether a finished spawn may be cached and,
22
//! if so, store its fingerprint, captured output, and output archive.
33
4-
use std::{sync::Arc, time::Duration};
4+
use std::{collections::BTreeMap, sync::Arc, time::Duration};
55

66
use vite_path::{AbsolutePath, RelativePathBuf};
77
use vite_str::Str;
@@ -97,13 +97,19 @@ pub(super) async fn update_cache(
9797
return (CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::FspyUnsupported), None);
9898
}
9999

100+
// Collect tool-reported tracked envs for the post-run fingerprint. Env
101+
// names that the user already declared are skipped because their values
102+
// are already part of the spawn fingerprint.
103+
let tracked_envs = reports.map(|r| collect_tracked_envs(r, metadata)).unwrap_or_default();
104+
100105
// Paths already in globbed_inputs are skipped: the overlap check above
101106
// guarantees no input modification, so the prerun hash is the correct
102107
// post-exec hash.
103108
let empty_path_reads = HashMap::default();
104109
let path_reads = fspy_outcome.as_ref().map_or(&empty_path_reads, |o| &o.path_reads);
105110
let post_run_fingerprint =
106-
match PostRunFingerprint::create(path_reads, workspace_root, &globbed_inputs) {
111+
match PostRunFingerprint::create(path_reads, workspace_root, &globbed_inputs, tracked_envs)
112+
{
107113
Ok(fingerprint) => fingerprint,
108114
Err(err) => {
109115
return (
@@ -166,6 +172,26 @@ fn observe_fspy(
166172
}
167173
}
168174

175+
/// Select tool-reported env records to embed in the post-run fingerprint.
176+
/// Only `tracked: true` records are included, and names that the user already
177+
/// declared as fingerprinted are skipped.
178+
fn collect_tracked_envs(reports: &Reports, metadata: &CacheMetadata) -> BTreeMap<Str, Option<Str>> {
179+
let fingerprinted = &metadata.spawn_fingerprint.env_fingerprints().fingerprinted_envs;
180+
reports
181+
.env_records
182+
.iter()
183+
.filter(|(_, record)| record.tracked)
184+
.filter_map(|(name, record)| {
185+
let name_str = name.to_str()?;
186+
if fingerprinted.contains_key(name_str) {
187+
return None;
188+
}
189+
let value = record.value.as_ref().and_then(|value| value.to_str().map(Str::from));
190+
Some((Str::from(name_str), value))
191+
})
192+
.collect()
193+
}
194+
169195
/// Collect output files matching the configured globs and create a tar.zst
170196
/// archive in the cache directory.
171197
///

crates/vite_task/src/session/execute/fingerprint.rs

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,23 @@
55
66
use std::{
77
collections::BTreeMap,
8+
ffi::OsStr,
89
fs::File,
910
io::{self, BufRead},
1011
sync::Arc,
1112
};
1213

1314
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
15+
use rustc_hash::FxHashMap;
1416
use serde::{Deserialize, Serialize};
1517
use vite_path::{AbsolutePath, RelativePathBuf};
1618
use vite_str::Str;
1719
use wincode::{SchemaRead, SchemaWrite};
1820

19-
use crate::{collections::HashMap, session::cache::InputChangeKind};
21+
use crate::{
22+
collections::HashMap,
23+
session::cache::{EnvMismatch, InputChangeKind},
24+
};
2025

2126
/// Path read access info
2227
#[derive(Debug, Clone, Copy)]
@@ -31,6 +36,21 @@ pub struct PostRunFingerprint {
3136
/// Paths inferred from fspy during execution with their content fingerprints.
3237
/// Only populated when `input_config.includes_auto` is true.
3338
pub inferred_inputs: HashMap<RelativePathBuf, PathFingerprint>,
39+
40+
/// Env vars observed via runner-aware IPC `getEnv` with `tracked: true`.
41+
/// Key is the env name; value is the env value at execution time, or
42+
/// `None` if unset. Validated at cache lookup against the same plan env
43+
/// context that served the original request.
44+
pub tracked_envs: BTreeMap<Str, Option<Str>>,
45+
}
46+
47+
/// A mismatch between the stored post-run fingerprint and the current state.
48+
#[derive(Debug, Clone)]
49+
pub enum PostRunMismatch {
50+
/// An inferred input file or directory changed.
51+
InputChanged { kind: InputChangeKind, path: RelativePathBuf },
52+
/// A tool-tracked env var changed value, appeared, or disappeared.
53+
TrackedEnvChanged(EnvMismatch),
3454
}
3555

3656
/// Fingerprint for a single path (file or directory)
@@ -69,11 +89,13 @@ impl PostRunFingerprint {
6989
/// * `inferred_path_reads` - Map of paths that were read during execution (from fspy)
7090
/// * `base_dir` - Workspace root for resolving relative paths
7191
/// * `globbed_inputs` - Prerun glob fingerprint; paths here are skipped
92+
/// * `tracked_envs` - Tool-requested env vars (name -> value), validated on lookup
7293
#[tracing::instrument(level = "debug", skip_all, name = "create_post_run_fingerprint")]
7394
pub fn create(
7495
inferred_path_reads: &HashMap<RelativePathBuf, PathRead>,
7596
base_dir: &AbsolutePath,
7697
globbed_inputs: &BTreeMap<RelativePathBuf, u64>,
98+
tracked_envs: BTreeMap<Str, Option<Str>>,
7799
) -> anyhow::Result<Self> {
78100
let inferred_inputs = inferred_path_reads
79101
.par_iter()
@@ -85,16 +107,17 @@ impl PostRunFingerprint {
85107
})
86108
.collect::<anyhow::Result<HashMap<_, _>>>()?;
87109

88-
Ok(Self { inferred_inputs })
110+
Ok(Self { inferred_inputs, tracked_envs })
89111
}
90112

91-
/// Validates the fingerprint against current filesystem state.
92-
/// Returns `Some((kind, path))` if an input changed, `None` if all valid.
113+
/// Validates the fingerprint against current filesystem state and env
114+
/// context. Returns `Some(mismatch)` if anything changed, `None` if all valid.
93115
#[tracing::instrument(level = "debug", skip_all, name = "validate_post_run_fingerprint")]
94116
pub fn validate(
95117
&self,
96118
base_dir: &AbsolutePath,
97-
) -> anyhow::Result<Option<(InputChangeKind, RelativePathBuf)>> {
119+
envs: &FxHashMap<Arc<OsStr>, Arc<OsStr>>,
120+
) -> anyhow::Result<Option<PostRunMismatch>> {
98121
let input_mismatch = self.inferred_inputs.par_iter().find_map_any(
99122
|(input_relative_path, path_fingerprint)| {
100123
let input_full_path = Arc::<AbsolutePath>::from(base_dir.join(input_relative_path));
@@ -120,11 +143,25 @@ impl PostRunFingerprint {
120143
} else {
121144
input_relative_path.clone()
122145
};
123-
Some(Ok((kind, path)))
146+
Some(Ok(PostRunMismatch::InputChanged { kind, path }))
124147
}
125148
},
126149
);
127-
input_mismatch.transpose()
150+
if let Some(result) = input_mismatch {
151+
return result.map(Some);
152+
}
153+
154+
for (name, stored_value) in &self.tracked_envs {
155+
let current_value =
156+
envs.get(OsStr::new(name.as_str())).and_then(|value| value.to_str().map(Str::from));
157+
if let Some(mismatch) =
158+
EnvMismatch::compare(name, stored_value.as_ref(), current_value.as_ref())
159+
{
160+
return Ok(Some(PostRunMismatch::TrackedEnvChanged(mismatch)));
161+
}
162+
}
163+
164+
Ok(None)
128165
}
129166
}
130167

crates/vite_task/src/session/execute/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,10 @@ async fn lookup_cache(
531531
Report::failed(ExecutionError::Cache { kind: CacheErrorKind::Lookup, source: err })
532532
})?;
533533

534-
match cache.try_hit(cache_metadata, &globbed_inputs, workspace_root).await {
534+
match cache
535+
.try_hit(cache_metadata, &globbed_inputs, workspace_root, &cache_metadata.unfiltered_envs)
536+
.await
537+
{
535538
Ok(Ok(cached)) => Ok(CacheLookup::Hit(cached)),
536539
Ok(Err(miss)) => Ok(CacheLookup::Miss { miss, globbed_inputs }),
537540
Err(err) => {

crates/vite_task/src/session/reporter/summary.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use vite_str::Str;
1717
use super::{CACHE_MISS_STYLE, COMMAND_STYLE, ColorizeExt};
1818
use crate::session::{
1919
cache::{
20-
CacheMiss, FingerprintMismatch, InputChangeKind, SpawnFingerprintChange,
20+
CacheMiss, EnvMismatch, FingerprintMismatch, InputChangeKind, SpawnFingerprintChange,
2121
detect_spawn_fingerprint_changes, format_input_change_str, format_spawn_change,
2222
},
2323
event::{
@@ -135,6 +135,8 @@ pub enum SavedCacheMissReason {
135135
ConfigChanged,
136136
/// An input file or folder changed.
137137
InputChanged { kind: InputChangeKind, path: Str },
138+
/// A runner-aware tool reported a tracked env var that changed between runs.
139+
TrackedEnvChanged(EnvMismatch),
138140
}
139141

140142
/// An execution error, serializable for persistence.
@@ -278,6 +280,9 @@ impl SavedCacheMissReason {
278280
FingerprintMismatch::InputChanged { kind, path } => {
279281
Self::InputChanged { kind: *kind, path: Str::from(path.as_str()) }
280282
}
283+
FingerprintMismatch::TrackedEnvChanged(mismatch) => {
284+
Self::TrackedEnvChanged(mismatch.clone())
285+
}
281286
},
282287
}
283288
}
@@ -567,6 +572,9 @@ impl TaskResult {
567572
let desc = format_input_change_str(*kind, path.as_str());
568573
vite_str::format!("→ Cache miss: {desc}")
569574
}
575+
SavedCacheMissReason::TrackedEnvChanged(mismatch) => {
576+
vite_str::format!("→ Cache miss: {mismatch}")
577+
}
570578
},
571579
},
572580
}

0 commit comments

Comments
 (0)