Skip to content

Commit 9477718

Browse files
committed
feat(cache): fingerprint tracked getEnvs match sets
Motivation: Bulk env reads are only cache-safe if the cache remembers the entire matched env set. Without match-set fingerprinting, changing a matching env value, adding a new matching name, or removing one could replay stale output. Scope: Make the getEnvs tracked option meaningful, record tracked match sets in IPC reports, store them in the post-run fingerprint, validate changed/added/removed matches during cache lookup, and render env-specific miss messages. This PR does not add the real Vite fixture. Verification: - cargo test -p vite_task_server --test integration - UPDATE_SNAPSHOTS=1 cargo test -p vite_task_bin --test e2e_snapshots fetch_envs_tracks_glob_match_set -- --ignored - cargo test -p vite_task_bin --test e2e_snapshots fetch_envs_tracks_glob_match_set -- --ignored
1 parent 25a3219 commit 9477718

15 files changed

Lines changed: 410 additions & 46 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 `getEnvs` match sets can now participate in task cache fingerprints, so changing, adding, or removing a matching env var invalidates the cache.
34
- **Added** Runner-aware `getEnvs` calls now return env values served by the runner for matching env glob patterns.
45
- **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.
56
- **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))

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vite_task/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ tracing = { workspace = true }
4545
twox-hash = { workspace = true }
4646
materialized_artifact = { workspace = true }
4747
uuid = { workspace = true, features = ["v4"] }
48+
vite_glob = { workspace = true }
4849
vite_path = { workspace = true }
4950
vite_select = { workspace = true }
5051
vite_str = { workspace = true }

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,8 @@ 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) => {
198+
FingerprintMismatch::TrackedEnvChanged(mismatch)
199+
| FingerprintMismatch::TrackedEnvGlobChanged { mismatch, .. } => {
199200
format_env_changed_inline(&[mismatch.name()])
200201
}
201202
};

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,14 +220,22 @@ pub enum FingerprintMismatch {
220220
},
221221
/// A runner-aware tool-tracked env var changed between runs.
222222
TrackedEnvChanged(EnvMismatch),
223+
/// A runner-aware tool-tracked env glob's match-set changed between runs.
224+
TrackedEnvGlobChanged {
225+
pattern: Str,
226+
mismatch: EnvMismatch,
227+
},
223228
}
224229

225230
impl From<crate::session::execute::fingerprint::PostRunMismatch> for FingerprintMismatch {
226231
fn from(mismatch: crate::session::execute::fingerprint::PostRunMismatch) -> Self {
227232
use crate::session::execute::fingerprint::PostRunMismatch;
228233
match mismatch {
229-
PostRunMismatch::InputChanged { kind, path } => Self::InputChanged { kind, path },
230-
PostRunMismatch::TrackedEnvChanged(mismatch) => Self::TrackedEnvChanged(mismatch),
234+
PostRunMismatch::Input { kind, path } => Self::InputChanged { kind, path },
235+
PostRunMismatch::TrackedEnv(mismatch) => Self::TrackedEnvChanged(mismatch),
236+
PostRunMismatch::TrackedEnvGlob { pattern, mismatch } => {
237+
Self::TrackedEnvGlobChanged { pattern, mismatch }
238+
}
231239
}
232240
}
233241
}
@@ -253,7 +261,7 @@ pub fn split_path(path: &str) -> (Option<&str>, &str) {
253261
/// its own cache warm across branch switches, and a cache from a different
254262
/// version is simply ignored (it lives in a directory this build never looks
255263
/// at) rather than aborting the run. Bumping the version starts a fresh cache.
256-
const CACHE_SCHEMA_VERSION: u32 = 14;
264+
const CACHE_SCHEMA_VERSION: u32 = 15;
257265

258266
/// Name of the per-version subdirectory (e.g. `v13`) under the task-cache
259267
/// directory that holds the database and output archives for the current

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

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -101,23 +101,28 @@ pub(super) async fn update_cache(
101101
// names that the user already declared are skipped because their values
102102
// are already part of the spawn fingerprint.
103103
let tracked_envs = reports.map(|r| collect_tracked_envs(r, metadata)).unwrap_or_default();
104+
let tracked_env_globs = reports.map(collect_tracked_env_globs).unwrap_or_default();
104105

105106
// Paths already in globbed_inputs are skipped: the overlap check above
106107
// guarantees no input modification, so the prerun hash is the correct
107108
// post-exec hash.
108109
let empty_path_reads = HashMap::default();
109110
let path_reads = fspy_outcome.as_ref().map_or(&empty_path_reads, |o| &o.path_reads);
110-
let post_run_fingerprint =
111-
match PostRunFingerprint::create(path_reads, workspace_root, &globbed_inputs, tracked_envs)
112-
{
113-
Ok(fingerprint) => fingerprint,
114-
Err(err) => {
115-
return (
116-
CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled),
117-
Some(ExecutionError::PostRunFingerprint(err)),
118-
);
119-
}
120-
};
111+
let post_run_fingerprint = match PostRunFingerprint::create(
112+
path_reads,
113+
workspace_root,
114+
&globbed_inputs,
115+
tracked_envs,
116+
tracked_env_globs,
117+
) {
118+
Ok(fingerprint) => fingerprint,
119+
Err(err) => {
120+
return (
121+
CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled),
122+
Some(ExecutionError::PostRunFingerprint(err)),
123+
);
124+
}
125+
};
121126

122127
let output_archive = match collect_and_archive_outputs(metadata, workspace_root, cache_dir) {
123128
Ok(archive) => archive,
@@ -192,6 +197,29 @@ fn collect_tracked_envs(reports: &Reports, metadata: &CacheMetadata) -> BTreeMap
192197
.collect()
193198
}
194199

200+
/// Select tool-reported env-glob records to embed in the post-run
201+
/// fingerprint. Only `tracked: true` records are included, and the full
202+
/// match-set is stored as-is.
203+
fn collect_tracked_env_globs(reports: &Reports) -> BTreeMap<Str, BTreeMap<Str, Str>> {
204+
reports
205+
.env_glob_records
206+
.iter()
207+
.filter(|(_, record)| record.tracked)
208+
.map(|(pattern, record)| {
209+
let matches: BTreeMap<Str, Str> = record
210+
.matches
211+
.iter()
212+
.filter_map(|(name, value)| {
213+
let name_str = name.to_str()?;
214+
let value_str = value.to_str()?;
215+
Some((Str::from(name_str), Str::from(value_str)))
216+
})
217+
.collect();
218+
(Str::from(pattern.as_ref()), matches)
219+
})
220+
.collect()
221+
}
222+
195223
/// Collect output files matching the configured globs and create a tar.zst
196224
/// archive in the cache directory.
197225
///

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

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,24 @@ pub struct PostRunFingerprint {
4242
/// `None` if unset. Validated at cache lookup against the same plan env
4343
/// context that served the original request.
4444
pub tracked_envs: BTreeMap<Str, Option<Str>>,
45+
46+
/// Glob-pattern env queries (`getEnvs`) made with `tracked: true`.
47+
/// Outer key is the glob pattern, inner map is the match-set at execution
48+
/// time (name -> value). Validated at cache lookup by re-matching against
49+
/// the current env context and comparing the resulting set.
50+
pub tracked_env_globs: BTreeMap<Str, BTreeMap<Str, Str>>,
4551
}
4652

4753
/// A mismatch between the stored post-run fingerprint and the current state.
4854
#[derive(Debug, Clone)]
4955
pub enum PostRunMismatch {
5056
/// An inferred input file or directory changed.
51-
InputChanged { kind: InputChangeKind, path: RelativePathBuf },
57+
Input { kind: InputChangeKind, path: RelativePathBuf },
5258
/// A tool-tracked env var changed value, appeared, or disappeared.
53-
TrackedEnvChanged(EnvMismatch),
59+
TrackedEnv(EnvMismatch),
60+
/// A tool-tracked env glob's match-set changed between runs. Carries the
61+
/// first differing entry in env-name order.
62+
TrackedEnvGlob { pattern: Str, mismatch: EnvMismatch },
5463
}
5564

5665
/// Fingerprint for a single path (file or directory)
@@ -90,12 +99,14 @@ impl PostRunFingerprint {
9099
/// * `base_dir` - Workspace root for resolving relative paths
91100
/// * `globbed_inputs` - Prerun glob fingerprint; paths here are skipped
92101
/// * `tracked_envs` - Tool-requested env vars (name -> value), validated on lookup
102+
/// * `tracked_env_globs` - Tool-requested env globs (pattern -> matches)
93103
#[tracing::instrument(level = "debug", skip_all, name = "create_post_run_fingerprint")]
94104
pub fn create(
95105
inferred_path_reads: &HashMap<RelativePathBuf, PathRead>,
96106
base_dir: &AbsolutePath,
97107
globbed_inputs: &BTreeMap<RelativePathBuf, u64>,
98108
tracked_envs: BTreeMap<Str, Option<Str>>,
109+
tracked_env_globs: BTreeMap<Str, BTreeMap<Str, Str>>,
99110
) -> anyhow::Result<Self> {
100111
let inferred_inputs = inferred_path_reads
101112
.par_iter()
@@ -107,7 +118,7 @@ impl PostRunFingerprint {
107118
})
108119
.collect::<anyhow::Result<HashMap<_, _>>>()?;
109120

110-
Ok(Self { inferred_inputs, tracked_envs })
121+
Ok(Self { inferred_inputs, tracked_envs, tracked_env_globs })
111122
}
112123

113124
/// Validates the fingerprint against current filesystem state and env
@@ -143,7 +154,7 @@ impl PostRunFingerprint {
143154
} else {
144155
input_relative_path.clone()
145156
};
146-
Some(Ok(PostRunMismatch::InputChanged { kind, path }))
157+
Some(Ok(PostRunMismatch::Input { kind, path }))
147158
}
148159
},
149160
);
@@ -157,14 +168,88 @@ impl PostRunFingerprint {
157168
if let Some(mismatch) =
158169
EnvMismatch::compare(name, stored_value.as_ref(), current_value.as_ref())
159170
{
160-
return Ok(Some(PostRunMismatch::TrackedEnvChanged(mismatch)));
171+
return Ok(Some(PostRunMismatch::TrackedEnv(mismatch)));
172+
}
173+
}
174+
175+
for (pattern, stored_matches) in &self.tracked_env_globs {
176+
let current_matches = match_env_glob(pattern.as_str(), envs)?;
177+
if let Some(mismatch) = first_env_glob_mismatch(stored_matches, &current_matches) {
178+
return Ok(Some(PostRunMismatch::TrackedEnvGlob {
179+
pattern: pattern.clone(),
180+
mismatch,
181+
}));
161182
}
162183
}
163184

164185
Ok(None)
165186
}
166187
}
167188

189+
/// Build the current match-set for `pattern` by enumerating the given env
190+
/// snapshot and keeping UTF-8 names whose representation matches the glob.
191+
fn match_env_glob(
192+
pattern: &str,
193+
envs: &FxHashMap<Arc<OsStr>, Arc<OsStr>>,
194+
) -> anyhow::Result<BTreeMap<Str, Str>> {
195+
let glob = vite_glob::env::EnvGlob::new(pattern)?;
196+
Ok(envs
197+
.iter()
198+
.filter_map(|(name, value)| {
199+
let name_str = name.to_str()?;
200+
let value_str = value.to_str()?;
201+
if glob.is_match(name_str) {
202+
Some((Str::from(name_str), Str::from(value_str)))
203+
} else {
204+
None
205+
}
206+
})
207+
.collect())
208+
}
209+
210+
/// Find the first deterministic difference between stored and current env
211+
/// glob match-sets.
212+
fn first_env_glob_mismatch(
213+
stored: &BTreeMap<Str, Str>,
214+
current: &BTreeMap<Str, Str>,
215+
) -> Option<EnvMismatch> {
216+
let mut stored_iter = stored.iter();
217+
let mut current_iter = current.iter();
218+
let mut s = stored_iter.next();
219+
let mut c = current_iter.next();
220+
221+
loop {
222+
match (s, c) {
223+
(None, None) => return None,
224+
(Some((name, value)), None) => {
225+
return Some(EnvMismatch::Removed { name: name.clone(), value: value.clone() });
226+
}
227+
(None, Some((name, value))) => {
228+
return Some(EnvMismatch::Added { name: name.clone(), value: value.clone() });
229+
}
230+
(Some((sn, sv)), Some((cn, cv))) => match sn.cmp(cn) {
231+
std::cmp::Ordering::Equal => {
232+
if sv != cv {
233+
return Some(EnvMismatch::Changed {
234+
name: sn.clone(),
235+
old_value: sv.clone(),
236+
new_value: cv.clone(),
237+
});
238+
}
239+
s = stored_iter.next();
240+
c = current_iter.next();
241+
}
242+
std::cmp::Ordering::Less => {
243+
return Some(EnvMismatch::Removed { name: sn.clone(), value: sv.clone() });
244+
}
245+
std::cmp::Ordering::Greater => {
246+
return Some(EnvMismatch::Added { name: cn.clone(), value: cv.clone() });
247+
}
248+
},
249+
}
250+
}
251+
}
252+
168253
/// Determine the kind of change between two differing path fingerprints.
169254
/// Caller guarantees `stored != current`.
170255
///

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ pub enum SavedCacheMissReason {
137137
InputChanged { kind: InputChangeKind, path: Str },
138138
/// A runner-aware tool reported a tracked env var that changed between runs.
139139
TrackedEnvChanged(EnvMismatch),
140+
/// A runner-aware tool reported a tracked env glob whose match-set changed
141+
/// between runs. Carries the first differing entry.
142+
TrackedEnvGlobChanged { pattern: Str, mismatch: EnvMismatch },
140143
}
141144

142145
/// An execution error, serializable for persistence.
@@ -283,6 +286,12 @@ impl SavedCacheMissReason {
283286
FingerprintMismatch::TrackedEnvChanged(mismatch) => {
284287
Self::TrackedEnvChanged(mismatch.clone())
285288
}
289+
FingerprintMismatch::TrackedEnvGlobChanged { pattern, mismatch } => {
290+
Self::TrackedEnvGlobChanged {
291+
pattern: pattern.clone(),
292+
mismatch: mismatch.clone(),
293+
}
294+
}
286295
},
287296
}
288297
}
@@ -572,7 +581,8 @@ impl TaskResult {
572581
let desc = format_input_change_str(*kind, path.as_str());
573582
vite_str::format!("→ Cache miss: {desc}")
574583
}
575-
SavedCacheMissReason::TrackedEnvChanged(mismatch) => {
584+
SavedCacheMissReason::TrackedEnvChanged(mismatch)
585+
| SavedCacheMissReason::TrackedEnvGlobChanged { mismatch, .. } => {
576586
vite_str::format!("→ Cache miss: {mismatch}")
577587
}
578588
},

0 commit comments

Comments
 (0)