chadtmiller.com/knotless
git that lives in your Atmosphere account
knotless-run: a feature branch's pulls start from its own warm caches
MergedThis gives a branch its own caches when its workflow names that branch in both on.push.refs and on.pull.base. The branch falls back to the default branch's base until its first passing push, and its caches are dropped when the branch is deleted.
I read own_branch, promotes, keeps and warms in lib.rs, and Bases::start, Bases::ahead, take, promoting, drop_branches and the Drop for Lease in caches.rs. I did not read the full 63k-character diff, the new tests or the Subject::by_tree change. I found nothing wrong in what I read:
- Readers are counted against the cache actually read (
layer.read), so a fallback base is held until the run ends. takeonly accepts a kept layer whilestartstill gives the same(read, generation).- The default branch's cache names are unchanged.
- The
readfield onkept.jsonlayers is optional, so existing files read as before.
CI is not done yet: the test, check and docs runs were still queued when I looked, and the other workflows were skipped. The caches.rs and lib.rs tests need to pass there before this merges.
Two small things to check, neither a blocker:
drop_branchesremoves the caches of every branch missing fromsnap.live_refs(). That is safe as long as a snapshot'srefsis always complete, which I did not check. If a snapshot can ever be partial, it would drop live branches' bases.repo_ofandbranch_ofsplit names on-, so they rely onkeys::ref_hashnever producing a-.
8 passed, 2 skipped
What changes
A branch's own caches. Of a workflow whose
on.push.refsandon.pull.baseboth name a branch other than the default one, as both namingrefs/heads/feature/*do, that branch keeps caches of its own. A passing push to it promotes into them, as a push to the default branch promotes into its own. Pulls into it, and the merge queue's batches on it, start from them. Under--pulls-only, a trusted pull into it promotes into them, ordered by the branch's own tips inmarks.json.Fallback. A branch with no base of its own yet reads the default branch's, so a new feature branch starts as warm as main. Its first passing push makes its first base from that one and what the push wrote. A layer kept over the default's base is taken only while the branch still has none and the default's has not moved on. A run of the branch reading the default's base keeps that base while main moves on, and it goes once the run ends.
Pulls landed on a feature branch are taken, not run again. A push to a branch at the same tree as a trusted pull's passing run into that branch takes the run's result and its kept layers, as a push to main does (
Subject::by_treecovers every branch push, not just main's). grain merges intofeature/rustby fast-forward, so a merged pull warms the branch without running again.Garbage collection. Each pass over a repository drops the caches of branches it no longer has (
Caches::drop_branches, besidedrop_repo). A base still being read goes once its run ends.shedand the disk floor count branch bases like any others.Layout. The default branch's caches keep their names,
bases/knotless-cache-<repo>-<what>/<generation>. A branch's areknotless-cache-<repo>-<what>-<hash of branch ref>. So the bases on disk now are main's as they are, no migration is needed, and none goes cold on deploy.kept.jsonlayers gain an optionalread, set only when the layer was over the default's base. Files written without it read as before.Nothing else changes. The default branch's behaviour is the same. Untrusted pulls' writes still reach no base. Batches still never write. A branch only pushed to, which no pull goes into, still reads main's caches and keeps nothing.
Why
Only a passing push to the default branch made a base, so a workflow that exists only on a feature branch never had one. grain's
rust.yml,harness.ymlande2e.ymlexist only onfeature/rust, so every pull into it starts cold: Node downloaded, cargo from nothing, the spaces alpha rebuilt (about 10 of e2e's 12 minutes), Chromium installed. knotless's AGENTS.md sends big work tofeature/<name>branches too.Options for which branches get bases:
feature/*hard-coded: bakes a naming convention into the runner.on.pushnames: under a workflow with noon, every topic branch pushed would grow a multi-GB base. It would also change whatvol.ymlintests/run.rsrelies on, where pushes toaandbkeep nothing.on.pushandon.pull.baseboth naming it (chosen): a base pays only where runs start from it. Pulls into a branch are where they do, and it is also what lets the branch's push take a pull's kept layers. It is opt-in per workflow, from the workflow file.How it was checked
New tests in
caches.rs:a_branch_starts_from_the_default_and_keeps_its_own: a branch falls back to main's base, makes its own on its first push, and stays put as main moves. A new branch starts from main as main is now. A kept pull layer over the fallback is taken into the branch's first base, and is not taken once the branch has its own base or main has moved. A read of main's base by a branch run holds that base.--pulls-onlyadvances over the fallback. Reopening keeps each branch's own base.a_deleted_branchs_caches_go:drop_branchesremoves only the deleted branch's caches, a base a run reads once that run ends. The default branch's caches and another repository's stay.In
lib.rs,whose_branch_the_caches_arecovers which runs read a branch's own caches, which promote, and how they are named.whose_layers_are_keptnow also coverswarmsfor pulls into a branch with its own caches and into one without.The unit tests ran here once (
cargo test -p knotless-run --lib -- caches:: whose_). The rest is left to CI.Shortcomings
--cache-maxper base, by deletion of the branch, and by the disk floor's shedding, which sheds main's as well.rev-parseeach.refs/heads/feature/*toon.push.refsof the workflows it wants warm before any of this applies there.Review
The design: the rule for which branches get bases (
own_branchinlib.rs), andBases::start/aheadandtakeincaches.rs, where a layer over the fallback base is matched.🤖 Generated with Claude Code