Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions crates/vite_task/src/session/cache/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ pub fn format_cache_status_inline(cache_status: &CacheStatus) -> Option<Str> {
format_input_change_str(*kind, path.as_str())
}
FingerprintMismatch::TrackedEnvChanged(mismatch)
| FingerprintMismatch::TrackedEnvGlobChanged { mismatch, .. } => {
| FingerprintMismatch::TrackedEnvQueryChanged { mismatch, .. } => {
vite_str::format!("{mismatch}")
}
};
Expand Down Expand Up @@ -249,8 +249,10 @@ mod tests {
#[test]
fn inline_tracked_env_mismatch_preserves_kind() {
let added = CacheStatus::Miss(CacheMiss::FingerprintMismatch(
FingerprintMismatch::TrackedEnvGlobChanged {
pattern: Str::from("PROBE_*"),
FingerprintMismatch::TrackedEnvQueryChanged {
query: crate::session::execute::fingerprint::TrackedEnvQuery::Glob(Str::from(
"PROBE_*",
)),
mismatch: EnvMismatch::Added { name: Str::from("PROBE_C") },
},
));
Expand Down
17 changes: 10 additions & 7 deletions crates/vite_task/src/session/cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ use wincode::{
io::{Reader, Writer},
};

use super::execute::{fingerprint::PostRunFingerprint, pipe::StdOutput};
use super::execute::{
fingerprint::{PostRunFingerprint, TrackedEnvQuery},
pipe::StdOutput,
};

/// Cache lookup key identifying a task's execution configuration.
///
Expand Down Expand Up @@ -211,9 +214,9 @@ pub enum FingerprintMismatch {
},
/// A runner-aware tool-tracked env var changed between runs.
TrackedEnvChanged(EnvMismatch),
/// A runner-aware tool-tracked env glob's match-set changed between runs.
TrackedEnvGlobChanged {
pattern: Str,
/// A runner-aware tool-tracked bulk env query's match-set changed between runs.
TrackedEnvQueryChanged {
query: TrackedEnvQuery,
mismatch: EnvMismatch,
},
}
Expand All @@ -224,8 +227,8 @@ impl From<crate::session::execute::fingerprint::PostRunMismatch> for Fingerprint
match mismatch {
PostRunMismatch::Input { kind, path } => Self::InputChanged { kind, path },
PostRunMismatch::TrackedEnv(mismatch) => Self::TrackedEnvChanged(mismatch),
PostRunMismatch::TrackedEnvGlob { pattern, mismatch } => {
Self::TrackedEnvGlobChanged { pattern, mismatch }
PostRunMismatch::TrackedEnvQuery { query, mismatch } => {
Self::TrackedEnvQueryChanged { query, mismatch }
}
}
}
Expand All @@ -252,7 +255,7 @@ pub fn split_path(path: &str) -> (Option<&str>, &str) {
/// its own cache warm across branch switches, and a cache from a different
/// version is simply ignored (it lives in a directory this build never looks
/// at) rather than aborting the run. Bumping the version starts a fresh cache.
const CACHE_SCHEMA_VERSION: u32 = 16;
const CACHE_SCHEMA_VERSION: u32 = 17;

/// Name of the per-version subdirectory (e.g. `v14`) under the task-cache
/// directory that holds the database and output archives for the current
Expand Down
31 changes: 18 additions & 13 deletions crates/vite_task/src/session/execute/cache_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use vite_task_server::Reports;

use super::{
CacheState,
fingerprint::{PathRead, PostRunFingerprint},
fingerprint::{PathRead, PostRunFingerprint, TrackedEnvQuery},
glob,
spawn::ChildOutcome,
};
Expand All @@ -37,7 +37,7 @@ struct TrackingOutcome {
}

type TrackedEnvValues = BTreeMap<Str, Option<EnvValueHash>>;
type TrackedEnvGlobValues = BTreeMap<Str, BTreeMap<Str, EnvValueHash>>;
type TrackedEnvQueryValues = BTreeMap<TrackedEnvQuery, BTreeMap<Str, EnvValueHash>>;

/// Decide whether the finished run may be cached, and store it if so.
///
Expand Down Expand Up @@ -122,7 +122,7 @@ pub(super) async fn update_cache(
// Collect tool-reported tracked envs for the post-run fingerprint. Env
// names that the user already declared are skipped because their values
// are already part of the spawn fingerprint.
let (tracked_envs, tracked_env_globs) = match collect_tracked_reports(reports, metadata) {
let (tracked_envs, tracked_env_queries) = match collect_tracked_reports(reports, metadata) {
Ok(tracked_reports) => tracked_reports,
Err(err) => {
return (
Expand All @@ -142,7 +142,7 @@ pub(super) async fn update_cache(
workspace_root,
&globbed_inputs,
tracked_envs,
tracked_env_globs,
tracked_env_queries,
) {
Ok(fingerprint) => fingerprint,
Err(err) => {
Expand Down Expand Up @@ -262,12 +262,12 @@ fn observe_fspy(
fn collect_tracked_reports(
reports: Option<&Reports>,
metadata: &CacheMetadata,
) -> anyhow::Result<(TrackedEnvValues, TrackedEnvGlobValues)> {
) -> anyhow::Result<(TrackedEnvValues, TrackedEnvQueryValues)> {
reports
.map(|reports| {
let tracked_envs = collect_tracked_envs(reports, metadata)?;
let tracked_env_globs = collect_tracked_env_globs(reports)?;
Ok::<_, anyhow::Error>((tracked_envs, tracked_env_globs))
let tracked_env_queries = collect_tracked_env_queries(reports)?;
Ok::<_, anyhow::Error>((tracked_envs, tracked_env_queries))
})
.transpose()
.map(Option::unwrap_or_default)
Expand Down Expand Up @@ -332,12 +332,12 @@ fn collect_tracked_envs(
Ok(tracked_envs)
}

/// Select tool-reported env-glob records to embed in the post-run
/// Select tool-reported bulk env query records to embed in the post-run
/// fingerprint. The full match-set is stored as value hashes.
fn collect_tracked_env_globs(reports: &Reports) -> anyhow::Result<TrackedEnvGlobValues> {
let mut tracked_env_globs = BTreeMap::new();
fn collect_tracked_env_queries(reports: &Reports) -> anyhow::Result<TrackedEnvQueryValues> {
let mut tracked_env_queries = BTreeMap::new();

for (pattern, record) in &reports.tracked_get_envs {
for (query, record) in &reports.tracked_get_envs {
let mut matches = BTreeMap::new();
for (name, value) in &record.matches {
let name_str = name
Expand All @@ -348,10 +348,15 @@ fn collect_tracked_env_globs(reports: &Reports) -> anyhow::Result<TrackedEnvGlob
})?;
matches.insert(Str::from(name_str), EnvValueHash::new(value_str));
}
tracked_env_globs.insert(Str::from(pattern.as_ref()), matches);
let query = match query {
vite_task_server::EnvQuery::Glob(pattern) => {
TrackedEnvQuery::Glob(Str::from(pattern.as_ref()))
}
};
tracked_env_queries.insert(query, matches);
}

Ok(tracked_env_globs)
Ok(tracked_env_queries)
}

/// Collect output files and create a tar.zst archive in the cache directory.
Expand Down
100 changes: 58 additions & 42 deletions crates/vite_task/src/session/execute/fingerprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ use crate::{
session::cache::{EnvMismatch, InputChangeKind},
};

#[derive(
SchemaWrite, SchemaRead, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
)]
pub enum TrackedEnvQuery {
Glob(Str),
}

/// Path read access info
#[derive(Debug, Clone, Copy)]
pub struct PathRead {
Expand All @@ -44,18 +51,18 @@ pub struct PostRunFingerprint {
/// context that served the original request.
pub tracked_envs: BTreeMap<Str, Option<EnvValueHash>>,

/// Glob-pattern env queries (`getEnvs`) made with `tracked: true`.
/// Outer key is the glob pattern, inner map is the match-set at execution
/// time (name -> value hash). Validated at cache lookup by re-matching
/// against the current env context and comparing the resulting set.
/// Bulk env queries (`getEnvs`) made with `tracked: true`.
/// Outer key is the query, inner map is the match-set at execution time
/// (name -> value hash). Validated at cache lookup by re-matching against
/// the current env context and comparing the resulting set.
///
/// Non-UTF-8 env names are never matched, saved, or treated as errors:
/// they are not returned to the client, so their existence cannot affect
/// task behavior. Values are stricter. A matched env must have a UTF-8
/// value; the JS client errors when querying a matched non-UTF-8 value,
/// and cache-hit validation treats a currently matched non-UTF-8 value as
/// a changed mismatch so stale cached output is not replayed.
pub tracked_env_globs: BTreeMap<Str, BTreeMap<Str, EnvValueHash>>,
pub tracked_env_queries: BTreeMap<TrackedEnvQuery, BTreeMap<Str, EnvValueHash>>,
}

/// A mismatch between the stored post-run fingerprint and the current state.
Expand All @@ -65,9 +72,9 @@ pub enum PostRunMismatch {
Input { kind: InputChangeKind, path: RelativePathBuf },
/// A tool-tracked env var changed value, appeared, or disappeared.
TrackedEnv(EnvMismatch),
/// A tool-tracked env glob's match-set changed between runs. Carries the
/// first differing entry in env-name order.
TrackedEnvGlob { pattern: Str, mismatch: EnvMismatch },
/// A tool-tracked bulk env query's match-set changed between runs. Carries
/// the first differing entry in env-name order.
TrackedEnvQuery { query: TrackedEnvQuery, mismatch: EnvMismatch },
}

/// Fingerprint for a single path (file or directory)
Expand Down Expand Up @@ -107,14 +114,14 @@ impl PostRunFingerprint {
/// * `base_dir` - Workspace root for resolving relative paths
/// * `globbed_inputs` - Prerun glob fingerprint; paths here are skipped
/// * `tracked_envs` - Tool-requested env vars (name -> value hash), validated on lookup
/// * `tracked_env_globs` - Tool-requested env globs (pattern -> match-set hashes)
/// * `tracked_env_queries` - Tool-requested bulk env queries (query -> match-set hashes)
#[tracing::instrument(level = "debug", skip_all, name = "create_post_run_fingerprint")]
pub fn create(
inferred_path_reads: &HashMap<RelativePathBuf, PathRead>,
base_dir: &AbsolutePath,
globbed_inputs: &BTreeMap<RelativePathBuf, u64>,
tracked_envs: BTreeMap<Str, Option<EnvValueHash>>,
tracked_env_globs: BTreeMap<Str, BTreeMap<Str, EnvValueHash>>,
tracked_env_queries: BTreeMap<TrackedEnvQuery, BTreeMap<Str, EnvValueHash>>,
) -> anyhow::Result<Self> {
let inferred_inputs = inferred_path_reads
.par_iter()
Expand All @@ -126,7 +133,7 @@ impl PostRunFingerprint {
})
.collect::<anyhow::Result<HashMap<_, _>>>()?;

Ok(Self { inferred_inputs, tracked_envs, tracked_env_globs })
Ok(Self { inferred_inputs, tracked_envs, tracked_env_queries })
}

/// Validates the fingerprint against current filesystem state and the
Expand Down Expand Up @@ -194,19 +201,19 @@ impl PostRunFingerprint {
}
}

for (pattern, stored_matches) in &self.tracked_env_globs {
let current_matches = match match_env_glob(pattern.as_str(), unfiltered_envs)? {
EnvGlobValidation::Matches(matches) => matches,
EnvGlobValidation::NonUtf8Value(mismatch) => {
return Ok(Some(PostRunMismatch::TrackedEnvGlob {
pattern: pattern.clone(),
for (query, stored_matches) in &self.tracked_env_queries {
let current_matches = match match_env_query(query, unfiltered_envs)? {
EnvQueryValidation::Matches(matches) => matches,
EnvQueryValidation::NonUtf8Value(mismatch) => {
return Ok(Some(PostRunMismatch::TrackedEnvQuery {
query: query.clone(),
mismatch,
}));
}
};
if let Some(mismatch) = first_env_glob_mismatch(stored_matches, &current_matches) {
return Ok(Some(PostRunMismatch::TrackedEnvGlob {
pattern: pattern.clone(),
return Ok(Some(PostRunMismatch::TrackedEnvQuery {
query: query.clone(),
mismatch,
}));
}
Expand All @@ -216,34 +223,41 @@ impl PostRunFingerprint {
}
}

/// Build the current match-set for `pattern` by enumerating the given env
/// snapshot and keeping UTF-8 names whose representation matches the glob. If
/// a matching env has a non-UTF-8 value, return a changed mismatch so the stale
/// cache entry is not replayed.
fn match_env_glob(
pattern: &str,
/// Build the current match-set for `query` by enumerating the given env
/// snapshot and keeping matching UTF-8 names. If a matching env has a non-UTF-8
/// value, return a changed mismatch so the stale cache entry is not replayed.
fn match_env_query(
query: &TrackedEnvQuery,
envs: &FxHashMap<Arc<OsStr>, Arc<OsStr>>,
) -> anyhow::Result<EnvQueryValidation> {
let TrackedEnvQuery::Glob(pattern) = query;
let glob = vite_glob::env::EnvGlob::new(pattern.as_str())?;
Ok(collect_matching_envs(envs, |name| glob.is_match(name)))
}

fn collect_matching_envs(
envs: &FxHashMap<Arc<OsStr>, Arc<OsStr>>,
) -> anyhow::Result<EnvGlobValidation> {
let glob = vite_glob::env::EnvGlob::new(pattern)?;
is_match: impl Fn(&str) -> bool,
) -> EnvQueryValidation {
let mut matches = BTreeMap::new();
for (name, value) in envs {
let Some(name_str) = name.to_str() else {
continue;
};
if !glob.is_match(name_str) {
if !is_match(name_str) {
continue;
}
let Some(value_str) = value.to_str() else {
return Ok(EnvGlobValidation::NonUtf8Value(EnvMismatch::Changed {
return EnvQueryValidation::NonUtf8Value(EnvMismatch::Changed {
name: Str::from(name_str),
}));
});
};
matches.insert(Str::from(name_str), EnvValueHash::new(value_str));
}
Ok(EnvGlobValidation::Matches(matches))
EnvQueryValidation::Matches(matches)
}

enum EnvGlobValidation {
enum EnvQueryValidation {
Matches(BTreeMap<Str, EnvValueHash>),
NonUtf8Value(EnvMismatch),
}
Expand Down Expand Up @@ -516,9 +530,10 @@ mod tests {

#[test]
fn validate_reports_current_non_utf8_tracked_env_glob_value_as_changed() {
let mut tracked_env_globs = BTreeMap::new();
tracked_env_globs.insert(Str::from("PROBE_*"), BTreeMap::new());
let fingerprint = PostRunFingerprint { tracked_env_globs, ..PostRunFingerprint::default() };
let mut tracked_env_queries = BTreeMap::new();
tracked_env_queries.insert(TrackedEnvQuery::Glob(Str::from("PROBE_*")), BTreeMap::new());
let fingerprint =
PostRunFingerprint { tracked_env_queries, ..PostRunFingerprint::default() };

let mut unfiltered_envs = FxHashMap::default();
unfiltered_envs.insert(
Expand All @@ -531,22 +546,23 @@ mod tests {
fingerprint.validate(&workspace_root, &unfiltered_envs).expect("validation succeeds");

match mismatch {
Some(PostRunMismatch::TrackedEnvGlob {
pattern,
Some(PostRunMismatch::TrackedEnvQuery {
query,
mismatch: EnvMismatch::Changed { name },
}) => {
assert_eq!(pattern.as_str(), "PROBE_*");
assert_eq!(query, TrackedEnvQuery::Glob(Str::from("PROBE_*")));
assert_eq!(name.as_str(), "PROBE_BAD");
}
other => panic!("expected changed tracked env glob mismatch, got {other:?}"),
other => panic!("expected changed tracked env query mismatch, got {other:?}"),
}
}

#[test]
fn validate_ignores_non_utf8_tracked_env_glob_names() {
let mut tracked_env_globs = BTreeMap::new();
tracked_env_globs.insert(Str::from("PROBE_*"), BTreeMap::new());
let fingerprint = PostRunFingerprint { tracked_env_globs, ..PostRunFingerprint::default() };
let mut tracked_env_queries = BTreeMap::new();
tracked_env_queries.insert(TrackedEnvQuery::Glob(Str::from("PROBE_*")), BTreeMap::new());
let fingerprint =
PostRunFingerprint { tracked_env_queries, ..PostRunFingerprint::default() };

let mut unfiltered_envs = FxHashMap::default();
unfiltered_envs.insert(
Expand Down
Loading
Loading