Skip to content

Commit cf5f1ad

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 a25bdcd commit cf5f1ad

13 files changed

Lines changed: 365 additions & 33 deletions

File tree

CHANGELOG.md

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

33
- **Changed** Tracked environment values in task cache fingerprints are now stored only as SHA-256 digests, and env-related cache miss details report names without values.
4+
- **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 ([#450](https://github.com/voidzero-dev/vite-task/pull/450)).
45
- **Added** Runner-aware `getEnvs` calls now return env values served by the runner for matching env glob patterns ([#449](https://github.com/voidzero-dev/vite-task/pull/449)).
56
- **Fixed** Runner-aware `getEnv` reads now affect task cache fingerprints, so changing a tool-served env value invalidates cached output and names the env var in the miss message ([#454](https://github.com/voidzero-dev/vite-task/pull/454)).
67
- **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
@@ -186,7 +186,8 @@ pub fn format_cache_status_inline(cache_status: &CacheStatus) -> Option<Str> {
186186
FingerprintMismatch::InputChanged { kind, path } => {
187187
format_input_change_str(*kind, path.as_str())
188188
}
189-
FingerprintMismatch::TrackedEnvChanged(mismatch) => {
189+
FingerprintMismatch::TrackedEnvChanged(mismatch)
190+
| FingerprintMismatch::TrackedEnvGlobChanged { mismatch, .. } => {
190191
format_env_changed_inline(&[mismatch.name()])
191192
}
192193
};

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,14 +211,22 @@ pub enum FingerprintMismatch {
211211
},
212212
/// A runner-aware tool-tracked env var changed between runs.
213213
TrackedEnvChanged(EnvMismatch),
214+
/// A runner-aware tool-tracked env glob's match-set changed between runs.
215+
TrackedEnvGlobChanged {
216+
pattern: Str,
217+
mismatch: EnvMismatch,
218+
},
214219
}
215220

216221
impl From<crate::session::execute::fingerprint::PostRunMismatch> for FingerprintMismatch {
217222
fn from(mismatch: crate::session::execute::fingerprint::PostRunMismatch) -> Self {
218223
use crate::session::execute::fingerprint::PostRunMismatch;
219224
match mismatch {
220-
PostRunMismatch::InputChanged { kind, path } => Self::InputChanged { kind, path },
221-
PostRunMismatch::TrackedEnvChanged(mismatch) => Self::TrackedEnvChanged(mismatch),
225+
PostRunMismatch::Input { kind, path } => Self::InputChanged { kind, path },
226+
PostRunMismatch::TrackedEnv(mismatch) => Self::TrackedEnvChanged(mismatch),
227+
PostRunMismatch::TrackedEnvGlob { pattern, mismatch } => {
228+
Self::TrackedEnvGlobChanged { pattern, mismatch }
229+
}
222230
}
223231
}
224232
}

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

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ struct TrackingOutcome {
3232
read_write_overlap: Option<RelativePathBuf>,
3333
}
3434

35+
type TrackedEnvValues = BTreeMap<Str, Option<EnvValueHash>>;
36+
type TrackedEnvGlobValues = BTreeMap<Str, BTreeMap<Str, EnvValueHash>>;
37+
3538
/// Decide whether the finished run may be cached, and store it if so.
3639
///
3740
/// Every outcome returns a `(status, error)` pair for the caller's single
@@ -100,8 +103,8 @@ pub(super) async fn update_cache(
100103
// Collect tool-reported tracked envs for the post-run fingerprint. Env
101104
// names that the user already declared are skipped because their values
102105
// are already part of the spawn fingerprint.
103-
let tracked_envs = match collect_tracked_reports(reports, metadata) {
104-
Ok(tracked_envs) => tracked_envs,
106+
let (tracked_envs, tracked_env_globs) = match collect_tracked_reports(reports, metadata) {
107+
Ok(tracked_reports) => tracked_reports,
105108
Err(err) => {
106109
return (
107110
CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled),
@@ -115,17 +118,21 @@ pub(super) async fn update_cache(
115118
// post-exec hash.
116119
let empty_path_reads = HashMap::default();
117120
let path_reads = fspy_outcome.as_ref().map_or(&empty_path_reads, |o| &o.path_reads);
118-
let post_run_fingerprint =
119-
match PostRunFingerprint::create(path_reads, workspace_root, &globbed_inputs, tracked_envs)
120-
{
121-
Ok(fingerprint) => fingerprint,
122-
Err(err) => {
123-
return (
124-
CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled),
125-
Some(ExecutionError::PostRunFingerprint(err)),
126-
);
127-
}
128-
};
121+
let post_run_fingerprint = match PostRunFingerprint::create(
122+
path_reads,
123+
workspace_root,
124+
&globbed_inputs,
125+
tracked_envs,
126+
tracked_env_globs,
127+
) {
128+
Ok(fingerprint) => fingerprint,
129+
Err(err) => {
130+
return (
131+
CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled),
132+
Some(ExecutionError::PostRunFingerprint(err)),
133+
);
134+
}
135+
};
129136

130137
let output_archive = match collect_and_archive_outputs(metadata, workspace_root, cache_dir) {
131138
Ok(archive) => archive,
@@ -183,17 +190,24 @@ fn observe_fspy(
183190
fn collect_tracked_reports(
184191
reports: Option<&Reports>,
185192
metadata: &CacheMetadata,
186-
) -> anyhow::Result<BTreeMap<Str, Option<EnvValueHash>>> {
187-
reports.map(|r| collect_tracked_envs(r, metadata)).transpose().map(Option::unwrap_or_default)
193+
) -> anyhow::Result<(TrackedEnvValues, TrackedEnvGlobValues)> {
194+
reports
195+
.map(|reports| {
196+
let tracked_envs = collect_tracked_envs(reports, metadata)?;
197+
let tracked_env_globs = collect_tracked_env_globs(reports)?;
198+
Ok::<_, anyhow::Error>((tracked_envs, tracked_env_globs))
199+
})
200+
.transpose()
201+
.map(Option::unwrap_or_default)
188202
}
189203

190204
/// Select tool-reported env records to embed in the post-run fingerprint.
191-
/// Only `tracked: true` records are included, and names that the user already
192-
/// declared as fingerprinted are skipped.
205+
/// Names that the user already declared as fingerprinted are skipped because
206+
/// their values are already in the spawn fingerprint.
193207
fn collect_tracked_envs(
194208
reports: &Reports,
195209
metadata: &CacheMetadata,
196-
) -> anyhow::Result<BTreeMap<Str, Option<EnvValueHash>>> {
210+
) -> anyhow::Result<TrackedEnvValues> {
197211
let fingerprinted = &metadata.spawn_fingerprint.env_fingerprints().fingerprinted_envs;
198212
let mut tracked_envs = BTreeMap::new();
199213

@@ -218,6 +232,28 @@ fn collect_tracked_envs(
218232
Ok(tracked_envs)
219233
}
220234

235+
/// Select tool-reported env-glob records to embed in the post-run
236+
/// fingerprint. The full match-set is stored as value hashes.
237+
fn collect_tracked_env_globs(reports: &Reports) -> anyhow::Result<TrackedEnvGlobValues> {
238+
let mut tracked_env_globs = BTreeMap::new();
239+
240+
for (pattern, record) in &reports.env_glob_records {
241+
let mut matches = BTreeMap::new();
242+
for (name, value) in &record.matches {
243+
let name_str = name
244+
.to_str()
245+
.ok_or_else(|| anyhow::anyhow!("tracked env match name is not valid UTF-8"))?;
246+
let value_str = value.to_str().ok_or_else(|| {
247+
anyhow::anyhow!("tracked env match value for {name_str} is not valid UTF-8")
248+
})?;
249+
matches.insert(Str::from(name_str), EnvValueHash::new(value_str));
250+
}
251+
tracked_env_globs.insert(Str::from(pattern.as_ref()), matches);
252+
}
253+
254+
Ok(tracked_env_globs)
255+
}
256+
221257
/// Collect output files matching the configured globs and create a tar.zst
222258
/// archive in the cache directory.
223259
///

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

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

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

5766
/// Fingerprint for a single path (file or directory)
@@ -91,12 +100,14 @@ impl PostRunFingerprint {
91100
/// * `base_dir` - Workspace root for resolving relative paths
92101
/// * `globbed_inputs` - Prerun glob fingerprint; paths here are skipped
93102
/// * `tracked_envs` - Tool-requested env vars (name -> value hash), validated on lookup
103+
/// * `tracked_env_globs` - Tool-requested env globs (pattern -> match-set hashes)
94104
#[tracing::instrument(level = "debug", skip_all, name = "create_post_run_fingerprint")]
95105
pub fn create(
96106
inferred_path_reads: &HashMap<RelativePathBuf, PathRead>,
97107
base_dir: &AbsolutePath,
98108
globbed_inputs: &BTreeMap<RelativePathBuf, u64>,
99109
tracked_envs: BTreeMap<Str, Option<EnvValueHash>>,
110+
tracked_env_globs: BTreeMap<Str, BTreeMap<Str, EnvValueHash>>,
100111
) -> anyhow::Result<Self> {
101112
let inferred_inputs = inferred_path_reads
102113
.par_iter()
@@ -108,7 +119,7 @@ impl PostRunFingerprint {
108119
})
109120
.collect::<anyhow::Result<HashMap<_, _>>>()?;
110121

111-
Ok(Self { inferred_inputs, tracked_envs })
122+
Ok(Self { inferred_inputs, tracked_envs, tracked_env_globs })
112123
}
113124

114125
/// Validates the fingerprint against current filesystem state and env
@@ -144,7 +155,7 @@ impl PostRunFingerprint {
144155
} else {
145156
input_relative_path.clone()
146157
};
147-
Some(Ok(PostRunMismatch::InputChanged { kind, path }))
158+
Some(Ok(PostRunMismatch::Input { kind, path }))
148159
}
149160
},
150161
);
@@ -159,14 +170,78 @@ impl PostRunFingerprint {
159170
if let Some(mismatch) =
160171
EnvMismatch::compare(name, stored_value.as_ref(), current_value.as_ref())
161172
{
162-
return Ok(Some(PostRunMismatch::TrackedEnvChanged(mismatch)));
173+
return Ok(Some(PostRunMismatch::TrackedEnv(mismatch)));
174+
}
175+
}
176+
177+
for (pattern, stored_matches) in &self.tracked_env_globs {
178+
let current_matches = match_env_glob(pattern.as_str(), envs)?;
179+
if let Some(mismatch) = first_env_glob_mismatch(stored_matches, &current_matches) {
180+
return Ok(Some(PostRunMismatch::TrackedEnvGlob {
181+
pattern: pattern.clone(),
182+
mismatch,
183+
}));
163184
}
164185
}
165186

166187
Ok(None)
167188
}
168189
}
169190

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

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)