perf(create): singleflight same-ref snapshot and cloud-image imports#37
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses a concurrency/performance issue in the Cocoon provider where multiple pods requesting the same cold OCI snapshot or cloud image could redundantly pull/import the same artifact and race on local name registration. It introduces provider-owned singleflight.Groups to deduplicate snapshot pulls by local snapshot name and run-image materialization by ref (including force semantics), using cancel-detached bounded contexts to prevent one caller’s cancellation from aborting shared work.
Changes:
- Add
snapshotPullSFandrunImageSFsingleflight groups to deduplicate snapshot pulls and run-image resolution/imports. - Wrap snapshot pull+inspect and run-image resolution paths in singleflight with a cancel-detached, time-bounded import context (
importDetachTimeout). - Add/adjust concurrency-focused tests with in-memory OCI artifacts and deterministic synchronization.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| provider/cocoon/provider.go | Adds new singleflight.Group fields for snapshot pulls and run-image materialization deduplication. |
| provider/cocoon/create.go | Introduces detached import context timeout and wraps snapshot/image resolution in singleflight to avoid duplicate imports and name races. |
| provider/cocoon/create_test.go | Adds singleflight concurrency tests and supporting fake runtime/registry behavior for deterministic assertions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@CMGS Unfortunately I hit an unexpected error while processing your comment. I've automatically reported this to GitHub. You can ask me to try again later by mentioning me in a new comment. If you want to contact GitHub about this error, please mention the following identifier so they can better serve you: Sorry for the inconvenience! |
d01f00d to
f4696cf
Compare
|
Updated at
Fix: shared work now runs on Two synctest-based regression tests cover it; both were verified to FAIL against the pre-fix shape ( Codex re-reviewed at |
Concurrent pods on one cold OCI artifact each pulled and imported the same bytes, and worse: vk's SnapshotImport removes the target name before importing while cocoon registers it only on completion, so of N concurrent cold clones N-1 failed "snapshot name already in use" and a late rm could yank the name a finished import had just registered out from under a pending clone. Wrap ensureSnapshot's check+pull+inspect and ensureRunImage's whole resolution in provider-owned keyed singleflight, mirroring ensureForkSnapshot. The local-hit fast path stays flight-free; the re-check inside the snapshot flight closes the rm window. Shared work is cancel-detached from callers with an importDetachTimeout backstop so a stalled registry stream cannot block waiters forever. The image key carries force so concurrent force requests share one import while a later force still re-imports; different refs fly concurrently.
f4696cf to
e642a19
Compare
Refs #35.
Problem
Concurrent pods using the same cold OCI artifact independently download and import the same bytes —
ensureSnapshotandensureRunImagehave no synchronization between the local check and the pull/import. The user-visible failure is worse than wasted bandwidth: vk'sSnapshotImportremoves the target name before importing (retry idempotency), while cocoon binds the name only when an import completes. In an N-way cold burst, N-1 pods failsnapshot name already in use, and a late rm can unbind the name a finished import just registered, failing a pending clone with "not found".Note the issue text's corruption framing does not hold against current cocoon: imports extract into a unique data dir and
insertRecordrejects name collisions, and live VMs keep their hardlinked memory files across asnapshot rm. The real harms are the availability failure above plus N× registry bytes.Change
singleflight.Groups beside the existingforkSnapshotSF, same idiom.ensureSnapshot: local-hit fast path stays flight-free; the flight wraps check+pull+inspect keyed on the local name. The re-check inside the flight is load-bearing — it is what closes the rm-vs-registered-import window.ensureRunImage: the whole resolution (manifest fetch, classify, all four destinations including thecocoon image pullfallbacks) runs in a flight keyed on the raw ref, withforcefolded into the key: concurrent force requests share one import, a force caller never adopts a non-force flight's local hit, and a later force re-imports because keys evict on flight return.WithTimeout(p.lifecycleCtx, importDetachTimeout)): one caller's aborted CreatePod can't fail the rest,Closeaborts it (so a stalledcocoonchild can't outlive the provider), and the 30m deadline backstops a registry stream that hangs without either firing.DoChan+selecton their own ctx, so a cancelled CreatePod returns immediately instead of occupying its worker until the flight ends, while the flight keeps running for the remaining waiters.resolveWakeSourceis deliberately not wrapped: it always re-pulls so a crashed prior wake's stale-hibernate-importis never reused, and virtual-kubelet serializes per pod, so there is no fan-out to dedup.SnapshotPullDurationnow observes only real pulls (leader-only) with no line moved;SnapshotPullTotalkeeps its ensure-outcome-per-pod meaning.Tests
Test fakes now mirror cocoon's contract — import names become visible only when
wait()completes — which makes the concurrency assertions deterministic: 16-way cold burst → exactly 1 import + 1 manifest fetch (snapshot and image paths); warm hit → 0 transfers/imports; different refs provably overlap in flight; a cancelled leader doesn't fail the shared import (fake import dies on a cancelled ctx, so a plain ctx pass-through fails this test); shared failure reaches all waiters and a later call retries; force semantics (concurrent-share, warm-skip-then-force-reimport, later-force). The two tests that need "second caller parked in the flight" usetesting/synctest. Fixtures are minimal real streamable artifacts (digest-verified blobs served from memory).Evidence
go test -race -run 'TestEnsureSnapshot|TestEnsureRunImage' -count=20 ./provider/cocoon/ok.make lint0 issues on linux+darwin;go test -race ./...all 8 packages ok;make buildok.execsnoop), CreatePod p95, andname already in usefailures ~15 → 0.Follow-up candidate (pre-existing, out of scope here):
ensureForkSnapshot's detached flight uses a barecontext.WithoutCancelwith no timeout backstop, so it is now the only one of the three provider flights without a deadline. A fix should not just reuseimportDetachTimeout—SnapshotSavescales ~2s/GiB and is a different operation family than a registry-stream import, so it needs its own bound. Tracked for a separate change.