Skip to content
Open
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
13 changes: 8 additions & 5 deletions crates/vt/docs/task-cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,11 @@ The cached execution result:

```rust
pub struct CacheEntryValue {
pub post_run_fingerprint: PostRunFingerprint,
pub input_fingerprints: InputFingerprints,
pub tracked_env_fingerprints: TrackedEnvFingerprints,
pub std_outputs: Arc<[StdOutput]>,
pub duration: Duration,
pub globbed_inputs: BTreeMap<RelativePathBuf, u64>,
pub output_archive: Option<Str>,
}
```

Expand Down Expand Up @@ -357,10 +358,11 @@ Cache entries are serialized using `bincode` for efficient storage.
│ 3. Create CacheEntryValue │
│ ──────────────────────────── │
│ CacheEntryValue { │
│ post_run_fingerprint, │
│ input_fingerprints, │
│ tracked_env_fingerprints, │
│ std_outputs, │
│ duration, │
globbed_inputs, │
output_archive, │
│ } │
│ │ │
│ ▼ │
Expand Down Expand Up @@ -571,7 +573,8 @@ crates/vt/src/session/
│ └── display.rs # Cache status display formatting
├── execute/
│ ├── mod.rs # execute_spawn, SpawnOutcome
│ ├── fingerprint.rs # PostRunFingerprint, PathFingerprint, DirEntryKind
│ ├── fingerprint.rs # InputFingerprints, PathFingerprint, InputChange
│ ├── post_run.rs # TrackedEnvFingerprints (tracked env validation)
│ └── spawn.rs # spawn_with_tracking, fspy integration
└── reporter/
└── mod.rs # Reporter traits for cache hit/miss display
Expand Down
2 changes: 1 addition & 1 deletion crates/vt/src/session/cache/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ mod tests {
fn inline_tracked_env_mismatch_preserves_kind() {
let added = CacheStatus::Miss(CacheMiss::FingerprintMismatch(
FingerprintMismatch::TrackedEnvQueryChanged {
query: crate::session::execute::fingerprint::TrackedEnvQuery::Glob(Str::from(
query: crate::session::execute::post_run::TrackedEnvQuery::Glob(Str::from(
"PROBE_*",
)),
mismatch: EnvMismatch::Added { name: Str::from("PROBE_C") },
Expand Down
114 changes: 29 additions & 85 deletions crates/vt/src/session/cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ use wincode::{
io::{Reader, Writer},
};

pub use super::execute::fingerprint::InputChangeKind;
use super::execute::{
fingerprint::{PostRunFingerprint, TrackedEnvQuery},
fingerprint::{InputChange, InputFingerprints},
pipe::StdOutput,
post_run::{PostRunMismatch, TrackedEnvFingerprints, TrackedEnvQuery},
};

const TASK_CACHE_PREALLOCATION_SIZE_LIMIT: usize = 256 * 1024 * 1024;
Expand Down Expand Up @@ -118,19 +120,19 @@ unsafe impl<'de, C: ConfigCore> SchemaRead<'de, C> for DurationSchema {

/// Cached execution result for a task.
///
/// Contains the post-run fingerprint (from fspy), captured outputs,
/// execution duration, and explicit input file hashes.
/// Contains the run's input fingerprints and tracked env state, captured
/// outputs, and execution duration.
#[derive(Debug, SchemaWrite, SchemaRead, Serialize)]
pub struct CacheEntryValue {
pub post_run_fingerprint: PostRunFingerprint,
/// Fingerprints of everything the cached run read. Checked against the
/// filesystem at lookup to decide whether the entry is still valid.
pub input_fingerprints: InputFingerprints,
/// Env vars and bulk env queries observed by runner-aware tools during
/// the run. Checked against the current env context at lookup.
pub tracked_env_fingerprints: TrackedEnvFingerprints,
pub std_outputs: Arc<[StdOutput]>,
#[wincode(with = "DurationSchema")]
pub duration: Duration,
/// Hashes of explicit input files computed from positive globs.
/// Files matching negative globs are already filtered out.
/// Path is relative to workspace root, value is `xxHash3_64` of file content.
/// Stored in the value (not the key) so changes can be detected and reported.
pub globbed_inputs: BTreeMap<RelativePathBuf, u64>,
/// Filename of the output archive (e.g. `{uuid}.tar.zst`) stored alongside
/// `cache.db` in the cache directory. `None` if no output files were produced.
pub output_archive: Option<Str>,
Expand All @@ -151,16 +153,6 @@ pub enum CacheMiss {
FingerprintMismatch(FingerprintMismatch),
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum InputChangeKind {
/// File content changed but path is the same
ContentModified,
/// New file or folder added
Added,
/// Existing file or folder removed
Removed,
}

/// A single env var difference between a stored fingerprint and the current
/// environment.
///
Expand Down Expand Up @@ -240,11 +232,15 @@ pub enum FingerprintMismatch {
},
}

impl From<crate::session::execute::fingerprint::PostRunMismatch> for FingerprintMismatch {
fn from(mismatch: crate::session::execute::fingerprint::PostRunMismatch) -> Self {
use crate::session::execute::fingerprint::PostRunMismatch;
impl From<InputChange> for FingerprintMismatch {
fn from(change: InputChange) -> Self {
Self::InputChanged { kind: change.kind, path: change.path }
}
}

impl From<PostRunMismatch> for FingerprintMismatch {
fn from(mismatch: PostRunMismatch) -> Self {
match mismatch {
PostRunMismatch::Input { kind, path } => Self::InputChanged { kind, path },
PostRunMismatch::TrackedEnv(mismatch) => Self::TrackedEnvChanged(mismatch),
PostRunMismatch::TrackedEnvQuery { query, mismatch } => {
Self::TrackedEnvQueryChanged { query, mismatch }
Expand Down Expand Up @@ -274,7 +270,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 = 18;
const CACHE_SCHEMA_VERSION: u32 = 19;

/// 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 Expand Up @@ -334,17 +330,19 @@ impl ExecutionCache {

// Try to find the cache entry by key (spawn fingerprint + input config)
if let Some(cache_value) = self.get_by_cache_key(&cache_key).await? {
// Validate explicit globbed inputs against the stored values
if let Some(mismatch) =
detect_globbed_input_change(&cache_value.globbed_inputs, globbed_inputs)
// Validate the stored input fingerprints against the filesystem:
// the listed-inputs snapshot against the fresh one, then each
// discovered input against the disk.
if let Some(change) =
cache_value.input_fingerprints.find_change(globbed_inputs, workspace_root)?
{
return Ok(Err(CacheMiss::FingerprintMismatch(mismatch)));
return Ok(Err(CacheMiss::FingerprintMismatch(change.into())));
}

// Validate post-run fingerprint (inferred inputs + tracked envs)
// Validate the tracked env state against the current env context.
if let Some(mismatch) = cache_value
.post_run_fingerprint
.validate(workspace_root, &cache_metadata.unfiltered_envs)?
.tracked_env_fingerprints
.validate_envs(&cache_metadata.unfiltered_envs)?
{
return Ok(Err(CacheMiss::FingerprintMismatch(mismatch.into())));
}
Expand Down Expand Up @@ -419,60 +417,6 @@ impl ExecutionCache {
}
}

/// Compare stored and current globbed inputs, returning the first changed path.
/// Both maps are `BTreeMap` so we iterate them in sorted lockstep.
fn detect_globbed_input_change(
stored: &BTreeMap<RelativePathBuf, u64>,
current: &BTreeMap<RelativePathBuf, u64>,
) -> Option<FingerprintMismatch> {
let mut stored_iter = stored.iter();
let mut current_iter = current.iter();
let mut s = stored_iter.next();
let mut c = current_iter.next();

loop {
match (s, c) {
(None, None) => return None,
(Some((sp, _)), None) => {
return Some(FingerprintMismatch::InputChanged {
kind: InputChangeKind::Removed,
path: sp.clone(),
});
}
(None, Some((cp, _))) => {
return Some(FingerprintMismatch::InputChanged {
kind: InputChangeKind::Added,
path: cp.clone(),
});
}
(Some((sp, sh)), Some((cp, ch))) => match sp.cmp(cp) {
std::cmp::Ordering::Equal => {
if sh != ch {
return Some(FingerprintMismatch::InputChanged {
kind: InputChangeKind::ContentModified,
path: sp.clone(),
});
}
s = stored_iter.next();
c = current_iter.next();
}
std::cmp::Ordering::Less => {
return Some(FingerprintMismatch::InputChanged {
kind: InputChangeKind::Removed,
path: sp.clone(),
});
}
std::cmp::Ordering::Greater => {
return Some(FingerprintMismatch::InputChanged {
kind: InputChangeKind::Added,
path: cp.clone(),
});
}
},
}
}
}

// Basic database operations
impl ExecutionCache {
#[expect(
Expand Down
23 changes: 9 additions & 14 deletions crates/vt/src/session/execute/cache_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ use vt_str::Str;

use super::{
CacheState,
fingerprint::{PathRead, PostRunFingerprint, TrackedEnvQuery},
fingerprint::{InputFingerprints, PathRead, fingerprint_discovered},
glob,
post_run::{TrackedEnvFingerprints, TrackedEnvQuery},
spawn::ChildOutcome,
};
use crate::{
Expand Down Expand Up @@ -132,19 +133,13 @@ pub(super) async fn update_cache(
}
};

// Paths already in globbed_inputs are skipped: the overlap check above
// guarantees no input modification, so the prerun hash is the correct
// post-exec hash.
// Fingerprint the discovered inputs (traced reads not already in the
// globbed snapshot: the overlap check above guarantees no input
// modification, so the prerun hash is the correct post-exec hash).
let empty_path_reads = HashMap::default();
let path_reads = fspy_outcome.as_ref().map_or(&empty_path_reads, |o| &o.path_reads);
let post_run_fingerprint = match PostRunFingerprint::create(
path_reads,
workspace_root,
&globbed_inputs,
tracked_envs,
tracked_env_queries,
) {
Ok(fingerprint) => fingerprint,
let discovered = match fingerprint_discovered(path_reads, workspace_root, &globbed_inputs) {
Ok(discovered) => discovered,
Err(err) => {
return (
CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled),
Expand All @@ -169,10 +164,10 @@ pub(super) async fn update_cache(
};

let new_cache_value = CacheEntryValue {
post_run_fingerprint,
input_fingerprints: InputFingerprints::new(globbed_inputs, discovered),
tracked_env_fingerprints: TrackedEnvFingerprints { tracked_envs, tracked_env_queries },
std_outputs: std_outputs.into(),
duration,
globbed_inputs,
output_archive,
};
match cache.update(metadata, new_cache_value, cache_dir).await {
Expand Down
Loading
Loading