Skip to content

solver: backport moby/buildkit#4887 to fix lost edges after a merged session ends - #29

Open
c-p-b wants to merge 5 commits into
EarthBuild:mainfrom
c-p-b:backport-4887
Open

c-p-b wants to merge 5 commits into
EarthBuild:mainfrom
c-p-b:backport-4887

Conversation

@c-p-b

@c-p-b c-p-b commented Oct 1, 2026

Copy link
Copy Markdown

Backports moby/buildkit#4887 (and #4347, which it builds on) to the current fork, and adds a real-daemon reproduction of the failure it fixes. This is an alternative to waiting for the upstream merge in #22 / EarthBuild/earthbuild#442, which already includes both. If the upgrade is close, close this and keep only the reproduction.

The failure

failed to get edge: inconsistent graph state (moby/buildkit#2303, #22). On a shared builder it arises without any cancellation:

  1. Session A builds a target that is a cache hit, so its shared steps only compute cache keys.
  2. Session B builds a different target on the same shared steps. GC has pruned their results, so they have to run. B's edges merge into A's.
  3. A ends normally. Its Discard deletes the states B's merged edges still use, and B fails looking one up.

#4887 makes setEdge add the merge source's jobs to the target state and its ancestors, so step 3 no longer deletes them.

Changes

  • 15bdffcb7 cherry-picks solver: fix printing progress messages after merged edges moby/buildkit#4347: setEdge takes the target state. This is the signature #4887 extends. The conflict was only in its context: the fork has hasOwner there.
  • 3be4af221 and 8a81118a7 cherry-pick #4887's two solver commits, with its test TestStaleEdgeMerge. Its third commit only changes debug logging and is left out.
  • b1191ea90 imports bklog in solver/jobs.go, which addJobs needs.
  • 980efc9b3 adds hack/lostedge-repro, a driver that runs the sequence above against any buildkitd image: ./hack/lostedge-repro/run.sh <image> -keep 0.

Testing

go test ./solver/ passes except TestJobsIntegration, which fails identically on main on a machine without the integration sandbox.

The reproduction, 24 B builds per run on linux/arm64:

buildkitd -keep 3 -keep 0
earthbuild/buildkitd:v0.8.18-b0385986 (51fe8fb9) 15 of 24 lost 18 of 24 lost
the same, on a 32-vCPU EKS builder 9 of 24 lost 12 of 24 lost
this branch at b1191ea90 (the last commit only adds hack/) 0 of 24 0 of 24
#22 head (2c7e20713) 0 of 24 0 of 24

Not covered

#4887 leaves a narrower race, where a Discard runs between the edge index lookup and setEdge. It still fails the solver unit test from #22 on stock moby/buildkit v0.33.0. #28 fixes it on top of #22. This reproduction does not reach it.

tonistiigi and others added 5 commits October 1, 2026 05:21
When different LLB vertexes (eg. parallel requests referencing
local sources from different sessions) generate same cache keys
during solve they are merged together into a single operation.

Currently, when this happened the progress for the vertex that
was dropped got lost. This fixes this case by adding the
progressWriter of the redirected vertex as a target to the
source one.

This should also work with multiple levels of merged edges,
just multiple nested multiwriters as well.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
(cherry picked from commit e1da8b7)
Before this, it was possible for an edge merge to happen to a target
edge/state that is no longer in the actives map, e.g.
1. Job A solves some edges
2. Job B solves some edges that get merged with job A's edges
3. Job A is discarded
4. Job C solves some edges that get merged with job B's edges, which
   recursively end up merging to job A's, which no longer exist in the
   actives map.

While this doesn't always result in an error, given the right state and
order of operations this can result in `getState` or `getEdge` being
called on the "stale" edges that are inactive and an error. E.g. if a
stale dep transitions from desired state `cache-fast` to `cache-slow`,
the slow cache logic will call `getState` on it and return a `compute
cache` error.

I also *suspect* this is the same root cause behind `inconsistent graph
state` errors still seen occasionally, but have not repro'd that error
locally so can't be 100% sure yet.

The fix here updates `state.setEdge` to register all the jobs in the
source edge's state with the target edge's state. This works out
because:
1. edges that are the target of a merge will not have their state
   removed from the actives map if only the original job creating them
   is discarded
1. those edges are still removed eventually, but only when all jobs
   referencing them (now including jobs referencing them via edge
   merges) are discarded

Signed-off-by: Erik Sipsma <erik@sipsma.dev>
(cherry picked from commit 1b3daaa)
Signed-off-by: Erik Sipsma <erik@sipsma.dev>
(cherry picked from commit ffdbd86)
addJobs (moby/buildkit#4887) logs through bklog, which this fork's jobs.go
did not import yet.
… failure

Drives a buildkitd into "failed to get state for index", the slow-cache
lookup of the same deleted state behind "inconsistent graph state", with no
cancellation: a cache-hit session ends while a second session, whose edges
merged into the first one's, still needs them to run. It fails 15 to 18 of
24 builds on earthbuild/buildkitd:v0.8.18-b0385986 and none on this branch.
See hack/lostedge-repro/README.md.
Copilot AI balanced review requested due to automatic review settings October 1, 2026 12:30
@coderabbitai

coderabbitai Bot commented Oct 1, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 4806b936-c559-45bc-b998-f88cb34ffcd1

  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgithub.laiyagushi.com/​containerd/​containerd@​v1.7.137573100100100
Addedgithub.laiyagushi.com/​moby/​buildkit@​v0.13.27577100100100
Addedgithub.laiyagushi.com/​tonistiigi/​fsutil@​v0.0.0-20240424095704-91a3fc46842c97100100100100

View full report

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The reproduction can report success without establishing or executing the required race conditions.

Review effort: Balanced
Findings: 2 Medium severity · 1 Low severity

Open (3)
What changed in this PR

Backports BuildKit’s stale edge-merge fix and adds a daemon-level reproduction.

Changes:

  • Propagates jobs through merged solver states.
  • Adds solver regression coverage.
  • Adds a real-daemon reproduction utility.
File Description
solver/​jobs.go Implements merged-state job propagation.
solver/​scheduler_test.go Tests edge merging and job lifetimes.
util/​progress/​multiwriter.go Adds an interface assertion.
hack/​lostedge-repro/​main.go Implements the reproduction driver.
hack/​lostedge-repro/​run.sh Runs the driver against a containerized daemon.
hack/​lostedge-repro/​README.md Documents reproduction usage and results.
hack/​lostedge-repro/​go.mod Defines reproduction dependencies.
hack/​lostedge-repro/​.gitignore Excludes generated artifacts.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +149 to +152
if *depth > 10 || *keep >= *depth {
fmt.Fprintln(os.Stderr, "need -depth <= 10 and -keep < -depth")
os.Exit(2)
}
Comment on lines +186 to +189
pruned, err := prune(ctx, c)
if err != nil {
fmt.Printf("iter %d: prune failed: %v\n", it, err)
}
Comment thread solver/scheduler_test.go
require.NotContains(t, s.actives[depV1.Digest()].jobs, j0)
require.Contains(t, s.actives[depV1.Digest()].jobs, j1)

// discard j0, verify that v0 is still active and it's state contains j1 since j1's
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants