Skip to content

remotecache: fix silently dropped cache link - #7053

Open
tonistiigi wants to merge 2 commits into
moby:masterfrom
tonistiigi:cache-link-fix
Open

remotecache: fix silently dropped cache link#7053
tonistiigi wants to merge 2 commits into
moby:masterfrom
tonistiigi:cache-link-fix

Conversation

@tonistiigi

Copy link
Copy Markdown
Member

closes and replaces #7048

mispencer and others added 2 commits August 17, 2026 12:23
addItemToStorage (import) and marshalItem (export) both build their output
by recursively walking an item's own dependencies, memoizing each item's
storage entry / record slot so a shared dependency is only resolved once.
Both allocated that memoized entry only *after* the recursive walk
finished, using a sentinel ("" / -1) to mark an item as already in
progress in the meantime.

When an item is reachable through more than one path - which happens in
practice once two unrelated ops coincidentally produce byte-identical
content, since their cache records can end up several levels deep in each
other's dependency chains despite having distinct provenance - a second
path can revisit an item while the first path is still in the middle of
resolving it. Before this fix, that reentrant call found only the
in-progress sentinel, got back nil/-1, and its caller silently skipped
registering the link - with no error, no warning. Which links survived
depended on Go's randomized per-process map iteration order (cc.leaves(),
and the multi-candidate alternatives at a single input slot), so
reconstructing the exact same chain from the exact same bytes could
non-deterministically drop a link on some runs and not others.

Both an item's final id (computed by computeIDs before addItemToStorage
runs) and its record's array index (known as soon as its slot in
state.records is reserved) are available up front, before any recursion.
Allocate and register the entry immediately instead of after, so a
reentrant call gets back the same, real (if not yet fully populated)
entry and can append its link to it successfully - nothing is silently
dropped anymore, regardless of traversal order.

This also makes addItemToStorage's separate `visited` map and the
k.byItem "" in-progress sentinel (and its "invalid loop" error branch)
redundant: k.byItem/k.byID alone now correctly memoize both same-call and
cross-call revisits, so `visited` was removed.

Verified with cache/remotecache/v1/cachestorage_test.go using a real
`--cache-to type=local,mode=max` export captured from a repro build
(testdata/cyclic-merge-chain.json): reparsing it into a fresh
NewCacheKeyStorage dropped the affected link on ~60-80% of iterations
against the pre-fix implementation (closely matching the failure rate
reported in the field) and 0% after this change. Also verified end-to-end
against real buildkitd builds (docker-container driver, both
type=registry and type=local cache backends, fresh builder per
iteration): 0 spurious cache misses across 95+ iterations post-fix versus
a 60-100% failure rate before it, including a wider variant with more
parallel merge points and a byte-for-byte content comparison between a
fresh build and a cache-imported one.

Signed-off-by: Spencer G. Jones <spencer.jones2@tylertech.com>
The dropped link comes from a stale nil entry in the per-leaf visited map
when a later traversal reuses an item already stored in the global memo.

Remove the redundant traversal state and keep post-order registration.
Restore marshalItem because export does not share this bug.

Replace the captured fixture with an integration test that repeatedly
imports byte-identical merge branches after pruning local results.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants