imagecache: Re-unpack a layer retired while joining its flight - #1509
Open
igooch wants to merge 1 commit into
Open
imagecache: Re-unpack a layer retired while joining its flight#1509igooch wants to merge 1 commit into
igooch wants to merge 1 commit into
Conversation
The reuse/retire interlock was one-sided. retireLayer treats joining a call already in flight as a veto, because its closure never ran and it therefore knows nothing about the dir. ensureLayer made the same kind of join and drew the opposite conclusion: singleflight.Do returned the other call's nil error, so ensureLayer returned the layer path as if it had verified it. When the flight it joined was a retirement, that path had just been renamed aside and nothing unpacked it back. The pull path then recorded the layer and its final re-verify reported "layer dir vanished during pull", failing an otherwise healthy pull into an RPC retry. The window is a few microseconds wide, so it surfaced only as a rare failure under CI load. Give ensureLayer the guard retireLayer already has: settle for a dir only when we ran the flight ourselves, or when the tree is still on disk once the joined flight finishes. Anything else means we joined a retirement, so re-enter and unpack the layer again, bounded by maxEnsureLayerFlights. Checking the dir rather than always re-entering keeps the dedup intact, so concurrent pulls of one image still collapse onto a single unpack. The pull path keeps its re-verify: it still guards a dir removed by something outside the flight, which the singleflight cannot see.
Collaborator
|
Can you clean up the PR description? It's very difficult to follow. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1076
The bug
The reuse/retire interlock was one-sided.
retireLayertreats joining anin-flight call as a veto — its closure never ran, so it knows nothing about the
dir.
ensureLayermade the same join and concluded the opposite:Doreturnedthe other call's
nilerror, so it returned the layer path as if verified.When the flight it joined was a retirement, that path had just been renamed
aside and nothing unpacked it back.
pullrecorded the layer, and itsre-verify reported
layer dir vanished during pull (evicted?)— failing ahealthy pull into an RPC retry. A few microseconds wide, hence CI-only.
Correcting the diagnosis in #1076
The issue blames an eviction cutoff postdating
ensureLayer's mtime refresh.That can't fire here: the test uses the default
minAge = 2m, socutoff = now - 2mwhile a refreshed dir has mtime ≈now—retireLayeralways vetoes. That window is real only at
minAge ≈ 0.Evidence
Counting joins that leave no
fsdir gives an exact 1:1 match with the failure(34→34, 149→149, 191→191, 1→1). No failure ever occurred without a join, the
rate tracks the flight-window width, and guarding the join drops it to zero
while the joins continue.
A/B on this branch, same harness, 200 iterations per head start:
The fix
Give
ensureLayerthe guardretireLayeralready has: accept a dir only if weran the flight ourselves, or the tree is still on disk once the joined flight
finishes. Otherwise we joined a retirement — re-enter and unpack again, bounded
by
maxEnsureLayerFlights.Checking the dir rather than always re-entering keeps the dedup: unconditional
retry would make N actors pulling one image each spin for their own flight.
No livelock —
retireLayervetoes on the symmetric join.Unchanged on purpose
pull's re-verify andTestPullReverifyFailsCleanlyOnYankedLayerstay. There-verify guards a dir removed outside the flight, which the singleflight
can't see. Fixing the cause didn't require weakening it.
Testing
TestEnsureLayerJoiningRetireFlightRepacksLayerreproduces thisdeterministically by occupying the flight as
retireLayerdoes. Fails onmain, passes here.-race: 70 pass, 0 fail.TestConcurrentEnsureImageAndEvict:500 consecutive
-raceruns clean.make verifyclean exceptmetrics.sh(needs Docker),proto-fmt.sh(needsclang-format),shellcheck.sh— the latter two fail identically onmain,and this touches no metrics, protos, or shell scripts.
Follow-up (separate issue)
TestConcurrentEnsureImageAndEvicttests less than it appears: over 5,000iterations it produced 33 candidates, skipped all as fresh, and removed zero
records — the
EnsureImagehit beats the evictor ~99.3% of the time, so theeviction-wins branch is almost never taken.