Cache Closure¶
The cache must hold one invariant. Every output delivered from the cache must come with its complete runtime closure in the cache. Counters per shared build carry the invariant through the graph. All start conditions read the counter. A self-heal can repair whatever a failed build proved wrong.
flowchart LR
C[CommitNar] -- seed --> W[missing_runtime_deps]
R[retire_outputs] -- reverse spread --> W
W -- "complete + terminal success" --> F[fetchable]
F --> U["blocking_deps of waiting builds"]
U -- 0 --> Q[Queued]
Complete Closure¶
Runtime dependencies are living in derivation_dependency next to build dependencies (EdgeKind::Runtime = 1, Both = 2). The predicates are in gradient-db/src/graph/predicates.rs.
| Term | Definition |
|---|---|
Present (present_predicate) |
Output rows exist for the shared build, each with a cached_path row carrying a set file_hash |
missing_runtime_deps |
Runtime dependencies of the shared build without a complete closure |
Complete closure (shared_build_complete_predicate) |
Present and missing_runtime_deps = 0 |
fetchable (fetchable_predicate) |
Terminal success and complete closure. An upstream copy does not count |
.drv present (drv_present_predicate) |
The build's own .drv NAR with a backed cached_path row. Presence only, not a complete closure |
- Build start condition:
blocking_deps = 0and.drvpresent.blocking_depsis the count of direct dependencies without a setfetchableflag. Thefetchableflags of the dependencies already cover their own closures, and the condition is free of recursion. - Passthrough start condition: shared builds available in a cache can start without either term. Their jobs carry no
required_paths(gradient-scheduler/src/loops/build.rs). - Push: jobs upload only their own outputs. Everything below them already had a complete closure before assignment. The invariant can hold without the worker pushing a closure.
NAR Commit¶
commit (gradient-graph/src/nar.rs) must run inside the graph writer's transaction for every CommitNar. A pooled handle is not accepted.
- Upsert the
cached_pathrow underFOR NO KEY UPDATE. Readwas_backedunder that lock. No later statement can recover this endpoint. - Overwrite
referenceswith the line the worker reported, in order.references_for_hashcan rebuild the narinfoReferences:line and the signature fingerprint verbatim from that line. - Insert a runtime dependency for each path in
references(add-only), then updatewantedfor the producers. seed_runtime_deps(gradient-db/src/graph/runtime_can_start.rs): an absolute recount of the producers. Paths without an earlier NAR becomefreshly_present. Re-pushes of a backed path get only a recount.- Spread the newly complete closures to the shared builds needing them at runtime.
ripple_shared_builds_completecan count down their missing dependencies level by level in one call ofripple_missing_runtime_deps, a SQL function. Thenbecame_fetchablemust setfetchableand move theblocking_depscounter. - Apply
ripple_shared_builds_incomplete, thenlost_fetchability, to shared builds left incomplete by a new dependency on a missing path. -
Write a
cached_path_signaturerow per target cache. Sign each row with the cache's key. The row is unsigned without a key or when all producing tasks keep the path private. Mark matchingderivation_outputrows cached. -
Transitions only: the spread may start only from rows with a state change reported by the seed. Spreading from a state would drive a counter below zero. A negative counter can never read
= 0again. - Deadlocks: the graph writer can retry a transaction failing with
40P01,40001or25P02up toGRAPH_TX_ATTEMPTS(3) times (gradient-graph/src/writer.rs).
Retire¶
retire_outputs (gradient-db/src/graph/runtime_can_start.rs) is the one path deleting cached_path rows.
- Lock the hashes
FOR NO KEY UPDATEin one hash-ordered statement, with the producers' advisory keys exclusive. - Read the producers with a complete closure (
complete_among) before the delete can destroy that endpoint. - Delete the rows, cascading to
cached_path_signature. Clearis_cachedon the outputs. ripple_shared_builds_incompletefrom the shared builds that had a complete closure.- Apply
lost_fetchabilityover producers and every build left incomplete. Reset only the producers of deleted paths toCreated.
| Caller | Trigger |
|---|---|
demote_cached_output (gradient-db/src/caches/demotion.rs) |
Self-heal, operator invalidation, a cache dropping its last claim, a NAR missing from storage |
GcRequest::Paths (gradient-graph/src/gc.rs) |
Stale-path eviction and zombie purge |
- One cache's claim:
Demotion::CacheClaimcan drop only thecached_path_signaturerow of that cache. The retire can follow only after the last signing cache is gone. - Graph writer only: all callers run inside the graph writer. The GC scans take place on the pool and hand the deletes to the graph writer.
- Lock order:
cached_pathrows (by hash), thenderivation_buildrows (byderivation), with advisory keys ahead of rows.demote_cached_outputmust write the cache availability flag (cache_available) before the retire. The function must take both lock passes first. Details are in Shared Builds.
Consistency Check¶
graph_consistency_report (gradient-db/src/graph/consistency.rs) is the only backstop for a lost move.
recount_missing_runtime_deps: an absolute, table-wide recount ofmissing_runtime_deps(also the column's backfill).repair_fetchableandrepair_can_start: rewritefetchableandblocking_depsover the start-condition scope (pending shared builds and the rows they wait on).recount_walk_completenessandrecount_wantedrecount the walk bit and the needs-build mark table-wide.settle_skippedmust then settle the queue against that mark.
Self-Heal¶
A build failing with InputsUnavailable must name its missing paths. repair_missing_inputs (gradient-graph/src/self_heal.rs) can handle each case in the table.
| Case | Action |
|---|---|
| Reported missing, object gone | demote_cached_output: retire the row, delete the object, clear cache_available. Builds needing the path are blocking again until the producer can push again |
| Reported missing, object present | Kept. Storage is the judge, and a worker's outdated view of the cache deletes nothing. The failed build retries |
Producer without a build_job (orphan) |
Also unwalk_parents_of: the parents keep their cached outputs, and the next evaluation walks them again and names the orphan |
No producer (.drv or source) |
Purged only when the object is really gone. The failed derivation is unwalked, so the next evaluation walks it again and pushes its sources |
- Corrupt NAR: the worker must check every fetched input against its
nar_sizeandnar_hash(verify_nar,gradient-worker/src/proto/nar_daemon_import.rs). A mismatch is aCorruptCachedNar, classifiedInputsUnavailablein prefetch and Substitute alike. The same self-heal can rebuild the producer with consistent metadata. - Requeue:
requeue_failed_shared_buildscan thaw demoted producers in a terminal failure at once. - Circuit breaker: the build will fail without the self-heal after
build.inputsUnavailableMaxLoops(GRADIENT_BUILD_INPUTS_UNAVAILABLE_MAX_LOOPS, default 3) priorInputsUnavailableattempts (inputs_unavailable_attempt_count). - Failure text: all failures store the worker's error, capped, on
build_attempt.failure_message.
Garbage Collection¶
The cache-maintenance pass (gradient-cache/src/schedule.rs) can start every gc.intervalSecs (3600 s). The keep-set is the closure of every entry_point and build_job derivation over derivation_dependency (reachable_derivations_cte), not the rows with a build_job of their own.
| Pass | Reclaimed | Bound |
|---|---|---|
| Evaluation GC | Evaluations beyond the task's keep_evaluations newest terminal ones |
Waiting during an active evaluation of the task, unless wedged longer than gc.wedgedEvalHours (24) |
| Derivation GC | derivation rows outside the keep-set, their attempt logs |
Created before gc.orphanDerivationHours (24) |
| Stale-path eviction | cached_path rows outside live_cached_paths_cte, then their objects |
Last fetch (or commit) older than max(gc.narTtlHours, gc.narUploadGraceHours) (336 h) |
| Zombie purge | Confirmed rows with the object missing from storage | Storage probe per row. A probe error will preserve the row |
| Orphan NAR files | Objects without any referencing row | Older than gc.narUploadGraceHours (24 h) |
- Re-check: the graph writer must re-check each chunk against roots created since the scan (
scanned_at). The writer can delete only rows that stayed dead. Object removal is limited to rows the graph writer reported as retired. - Live paths: outputs of the keep-set's runtime closure, plus the
.drvNAR andinputSrcsof every reachable derivation. - Fetch time:
cached_path_signature.last_fetched_atandfetch_countmove on every NAR download (gradient-web/src/endpoints/caches/nar.rs).
Access¶
| Endpoint | Rule |
|---|---|
GET /builds/{build}, /log, /graph, /closure, /downloads |
Public project, a member of the build's project, or a member of any project with a build_job for the same derivation (BuildAccessContext::load, gradient-web/src/endpoints/builds/mod.rs) |
GET /builds/{build}/download/{filename} |
The same rule, or a download token for the derivation |
Narinfo, NAR, ls and serve on /cache/{cache} |
A signed cached_path_signature row for that cache and a file_hash on the path (signed_in_cache, gradient-db/src/cached_paths.rs) |
GET /cache/{cache}/log/{drv} |
Own log only when the cache can hand out an output of the derivation by the same rule (derivation_signed_in_cache). Otherwise the upstream caches (caches/build_log.rs) |