Repository navigation
Description
Written by an AI agent (Claude) on behalf of @jasonkuhrt.
Remote caching is still unreleased (#820), and its API is drafted in #713, so this is a request for the protocol while it can still change.
What happens
In read-write mode, Vite Task uploads an entry only after it executes a task. ExecutionCache::update records the entry locally, then spawns the upload (crates/vt/src/session/cache/mod.rs:521-568 at a3c087a7, the revision Vite+ 1.4.0 pins, called from session/execute/cache_update.rs:190).
A remote hit takes the other path. try_hit_remote fetches and validates it (mod.rs:453-490), and restore extracts its archive and records it locally (mod.rs:583-619). As try_hit's doc puts it, the hit "is recorded locally by [Self::restore], and is never uploaded" (mod.rs:350-353). main at c6c91f9 is the same.
The protocol has each endpoint name its own storage, and an exact match is "the entry stored under the requested key" (crates/vt_remote_cache/src/lib.rs:113). Some servers also share entries between related endpoints. GitHub Actions' cache does this: a run can restore a cache that the default branch created when its own branch has none (GitHub: dependency caching).
A server like that has a gap. When it answers one endpoint's fetch with another endpoint's entry, the requesting endpoint's own entry for that key stays as it was. And because that machine's local cache now hits, no later run there fetches the task or uploads it either.
The case that motivates it
We run our own server for the protocol, with one endpoint per branch. Writes are scoped per branch for the reason GitHub Actions scopes its cache by branch: a pull request's CI can't replace the entries that the default branch, and every branch reading it, use. Several candidates per key (#778) would let branches share one namespace, but not keep a pull request from writing where the default branch reads. A fetch on a branch's endpoint serves the most recently stored entry under the key among that branch's entries and the default branch's, so pull requests replay what the default branch's CI stored.
After a merge, the default branch's endpoint could read the merged branch's entries too, and replay what the merged branch's CI stored. We tried it. One run replayed 146 tasks from the merged branch, and the default branch's endpoint kept its older entry for every one of them. Every branch reads the default branch's entries, so all of them missed those tasks until the default branch next executed them.
So the default branch no longer reads the merged branch. Its CI re-executes work that the merged branch's CI already did, and branches miss those tasks until that run finishes.
The ask
An optional store field on a fetch's exact match, which asks a read-write client to store the entry it was just served:
{ kind: "exact", value: bytes, blob_id: string | null, store: true }
What it means in each case:
- Absent or
false: nothing changes. Today's servers never send it, andFetcheddoesn't deny unknown fields (lib.rs:109-131), so today's clients ignore it. - A
readclient ignores it. - A hit that validates, restores and is recorded locally is stored the way
updatestores an executed task's entry: the samekeyandsecondary_key, and the value as it was recorded locally, with its blob. That value differs from the served bytes only inoutput_archive, the local archive's file name, which every client replaces with its own on download (mod.rs:489). The blob is the archive just downloaded, and the store uses the configured auth. - A hit that fails validation, or whose archive fails to download, is a miss and isn't stored. The task executes, and the usual upload after executing stores it.
- A hit whose outputs fail to restore fails the task, as it does today, and isn't stored.
- A hit that restores but fails to record locally (
mod.rs:607-616) isn't stored either. The next run fetches it again, and the server can ask again. - Timing: the store runs in the background like an upload after executing, so the task's dependents don't wait. The run waits for it before exiting, as it waits for uploads (
crates/vt/src/session/execute/scheduler.rs:302). - A store that fails never fails the task. Its error is reported on the hit's row of the summary.
- A fallback match has no
storefield.
The server decides when to ask, and the client needs no notion of where an entry came from.
Where it goes:
- The field on
Fetched::Exact(lib.rs:114-122). - The flag carried through
remote::resolveandRestore(remote.rs:95-137),try_hit_remoteandCacheHit. - In
ExecutionCache::restore, once a remote hit is recorded locally, the sameprepare_uploadanduploads.spawnthatupdateuses (mod.rs:554-566). - A field for a hit's store error in the summary. Today
set_upload_errorapplies only to a task that ran (crates/vt/src/session/reporter/summary.rs:402-411).
Alternatives considered
- Upload every remote hit. Every hit would send its archive back, which wastes bandwidth for runs that routinely read shared entries. The server would throw most of it away.
- Copy on the server during the fetch. The server would copy an entry before any client had validated it against the requesting endpoint's tree. With one entry per key (The remote cache keeps one entry per task, so branches overwrite each other's results #778), an entry that then fails validation would have replaced one that was valid.
- Copy on the server when a branch merges. Same problem: only the client validates an entry against its inputs. If The remote cache keeps one entry per task, so branches overwrite each other's results #778 lands as a lookup that returns several candidates per key, a server could copy without replacing anything. But that needs The remote cache keeps one entry per task, so branches overwrite each other's results #778's protocol change, and it copies every entry blind. The flag works with today's protocol and stores only entries a client validated.
Not part of this ask: a store by reference, which would name a blob the server already holds instead of sending the archive again. It would make the store cheaper, but it needs a protocol addition of its own, and the flag works without it.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity