Repository navigation
fix(solver): probe the keys a dependency reports after an edge merge - #47
kmannislands wants to merge 1 commit into
Conversation
An edge learns the keys of a dependency from the updates on its request to that dependency, and only probed the cache with the keys past the length it had already seen. That assumes the dependency's keys only grow. They do within one edge, but when the dependency's edge is merged into another edge the request is redirected, and the next update carries the other edge's keys, which can differ from the keys seen so far at every position. Keys that did not lengthen the list were never probed. EarthBuild hits this whenever it solves part of a graph before the rest of it in the same session: a LET or ARG with $(...), an IF, a FOR, a WAIT block, or the per-target solves of VERSION 0.8. The key of a local source is session-scoped (random:), so an imported registry cache can only match the steps below a local COPY through the content based key of that COPY. The first solve computes that key and loads the COPY from the imported cache. Storing the result links the session-scoped key to the COPY in the local cache. A later solve emits new vertices for the same steps; its COPY finds the local record through the session-scoped key alone and hands its consumer a key only the local cache can resolve, then merges into the completed COPY of the first solve. The consumer receives that edge's key, which the imported cache can resolve, but never probes it, and so re-executes every step that the first solve did not. Probe every key in the update that the consumer has not seen, compared by identity. When the update extends the keys seen so far, which is the common case, the result is the same tail as before. Upstream moby/buildkit has the same length comparison in processDepReq. Fixes EarthBuild/earthbuild#1033. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (2)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe solver now detects dependency cache keys that were not present in the prior key list, including when the list does not grow by appending. A new regression test checks imported-cache lookup after dependency merging. ChangesImported cache lookup
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Bug fix · Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to The change is mergeable after normal checks; no actionable cache-lookup risk was established. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Warning Some tools did not complete. Review the errors below. 🔧 golangci-lint (2.13.2)golangci-lint (2.13.2) skipped for .golangci.yml: configuration requires version: "2". Migrate older configurations using golangci-lint migrate and review the result; see https://golangci-lint.run/docs/product/migration-guide/. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
➖ Are we earthbuild yet?No change in "earthly" occurrences 📈 Overall Progress
Keep up the great work migrating from Earthly to Earthbuild! 🚀 💡 Tips for finding more occurrencesRun locally to see detailed breakdown: ./.github/scripts/count-earthly.shNote that the goal is not to reach 0. |
… merge Moves both buildkit pins to EarthBuild/buildkit#47 (517b9677a), which fixes the imported cache miss below a local COPY after a sub-solve: - go.mod's replace, for the library the CLI compiles in. - buildkitd/Earthfile's BUILDKIT_BASE_IMAGE, for the daemon image, where the fix takes effect. The pin points at the PR branch so CI runs the integration tests against it. Per CONTRIBUTING, #47 should be fast-forwarded onto the fork's main so this SHA stays valid when both merge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes EarthBuild/earthbuild#1033.
Problem
An imported registry cache (
--cache-from, or the import side of--remote-cache) is never used below a localCOPYonce part of that state has been solved on its own first. The triggers are aLET/ARGwith$(...), anIF, aFOR, aWAITblock, or the per-target solves of VERSION 0.8. The import itself succeeds. Every step below that point re-executes on a fresh buildkitd anyway.Two facts set it up:
random:) and is never exported. The registry cache can only match the steps below a localCOPYthrough thatCOPY's content-based key. That's the checksum of the copied files, which the solver computes from the source's result ("slow cache").flowchart LR base["FROM alpine<br/>image key"] src["local source f.txt<br/>random:S<br/>(session-scoped)"] content["content key C<br/>(checksum of f.txt)"] kloc["COPY key K_loc<br/>local cache only"] kreg["COPY key K_reg<br/>registry cache"] run["RUN record<br/>in registry cache"] base --> kloc src --> kloc base --> kreg content --> kreg kreg --> run kloc -. "no link: random:S is never exported" .-x runThe sequence that loses the cache hit:
sequenceDiagram autonumber participant F as earth (frontend) participant A as COPY edge A<br/>(sub-solve) participant B as COPY edge B<br/>(main solve) participant R as RUN edge participant L as local cache participant G as registry cache Note over F,G: Sub-solve for LET V="$(cat f.txt)" F->>A: solve A->>G: query via random:S G-->>A: no match A->>G: query via content key C G-->>A: K_reg A->>L: load the COPY from G, store it locally Note over L: random:S and C both now link to K_loc Note over F,G: Main solve, with new vertices for the same steps F->>B: solve B->>G: query via random:S G-->>B: no match B->>L: query via random:S L-->>B: K_loc B->>R: keys [K_loc] R->>G: probe K_loc G-->>R: no match Note over A,B: B has the same index key as A, so B merges into A A->>R: keys [K_reg] rect rgb(255, 235, 235) Note over R: Before: seen 1 key, update has 1 key, so nothing new. K_reg is never probed and RUN executes end rect rgb(230, 245, 230) Note over R: After: K_reg was not seen, so it is probed R->>G: probe K_reg G-->>R: RUN record, loaded from cache endThe new edge B reports
K_locto its consumer. B then merges into edge A, which finished in the sub-solve. The consumer's request is redirected to A, and its next update carries A's keys,[K_reg]. ButprocessUpdateonly probed the keys past the length it had already seen (len(dep.keys) < len(state.keys)). That assumes a dependency's keys only grow. They do within one edge, but not across a merge. Here both lists have length 1, soK_regwas never probed. ARUN's rootfs input has no slow-cache function, sonoCacheMatchPossiblelatches and the step executes. Every step after it misses too.The issue proposed forcing the content key to be recomputed when a match came only through
random:keys. That isn't needed. The edge that computed the content key is still alive in the same job, and the new edges merge into it. The only gap is that the consumer ignored the merged edge's keys. This keeps the change minimal, as AGENTS.md asks.Change
solver/edge.go:processUpdateprobes every key in a dependency update that it hasn't seen before. Keys are compared by identity (*CacheKey).unseenKeyshelper returns the tail exactly as before when the update extends the keys already seen, which is the usual case, and doesn't allocate.flowchart TD upd["Update from merged edge A<br/>keys seen so far: [K_loc]<br/>state.keys: [K_reg]"] subgraph before ["Before"] b1{"len(seen) less than<br/>len(state.keys)?"} b2["no, 1 vs 1: nothing probed<br/>RUN executes"] b1 --> b2 end subgraph after ["After: unseenKeys"] a1{"does state.keys<br/>extend the seen keys?"} a2["probe the new tail<br/>(the usual case, same as before)"] a3["probe every key not seen,<br/>compared by identity: K_reg<br/>registry hit, RUN loaded from cache"] a1 -- yes --> a2 a1 -- no --> a3 end upd --> b1 upd --> a1Probing the merged edge's keys can't produce a false match:
mergeTorefuses to merge an ignore-cache edge into one that uses the cache. An ignore-cache edge never probes, so it reports no keys.The diff is
solver/edge.go+36/−2 (call site, plus the helper and its doc comment), and a new test.Upstream
moby/buildkit
master(656dabf81, after v0.34.0-rc2) has the same length comparison inprocessDepReq, and the same bug:I found no upstream issue or PR for it. Related upstream work, all separate from this fix:
12d4b6375"solver: advertise committed cache keys after exec" (v0.32.0) is a neighbouring case: a consumer gets zero keys from a completed shared edge.224061dff/e7050b9eafix silently dropped links when importing a remote cache (v0.33.0), another cause of cache misses.None of these are in the fork. This PR is not being sent upstream yet.
Tests
New
TestCacheSourceLookupAfterDepMergeinsolver/scheduler_test.goreplays the scenario with in-memory caches:It checks that nothing in the main solve executes.
Results:
RUNexecutes).golang:1.21(the CI Go version), linux/arm64:go test -mod=vendor -run TestCacheSourceLookupAfterDepMerge -count=20 ./solver/: pass. Also passes 20/20 with-race.go test -mod=vendor -count=3 -skip TestJobsIntegration ./solver/: pass.TestJobsIntegrationneeds the integration sandbox and fails onmainwithmissing requirements.go vet -mod=vendor ./solver/: clean.docker buildx bake lint: pass.-raceon all of./solver/: about 35 tests fail onmainboth with and without this change, with identical failing sets. The causes are pre-existing races in the fork's graph-state debug tracker anddispatchinstrumentation, plus a few upstream races. Those are being handled separately.End to end (EarthBuild
mainCLI with a buildkitd image built from this branch). This is the issue's round trip: export with--max-remote-cachefrom a fresh daemon, wipe it, then rebuild with--cache-fromon another fresh daemon.343d8bfee)RUNs afterLET $(...),IF,FORand the WAIT chain re-execute, in 3/3 runsRUNs*cached*, in 4/4 runs; the controls are unchangedThe companion EarthBuild change adds
test4totests/remote-cache. It runs this round trip earth-in-earth and asserts that the steps afterLET,IF,FORandWAITare cached. It fails against the current pin and passes withBUILDKIT_PROJECTpointed at this branch.Companion PR
EarthBuild/earthbuild#1036 pins both buildkit references to this branch's commit (
517b9677a) and addstests/remote-cache+test4, so the integration tests run against this change. Per EarthBuild's CONTRIBUTING, the two should merge together, with this branch fast-forwarded ontomainto keep the SHA.Prepared with AI assistance (Claude Code).
🤖 Generated with Claude Code
Summary by CodeRabbit