|
| 1 | +1. The algorithm is a post-order concurrent DAG walk |
| 2 | +Children are always fully committed before their parent is pushed. This is not just an ordering preference — it is a correctness invariant. A registry will reject a manifest whose layers do not yet exist. The semaphore-release trick (region.end() before recursing into children, region.start() before copying self) is what makes this safe at any concurrency level, including 1. |
| 3 | +2. Deduplication is a first-class primitive, not an afterthought |
| 4 | +StatusTracker is the first gate every node passes through. Without it, shared blobs referenced by multiple manifests in an index would be fetched and pushed N times concurrently and race on dst.push(). The tracker collapses N workers into 1 doer + N-1 waiters. |
| 5 | +3. The cache proxy decouples resolution from traversal |
| 6 | +CacheProxy absorbs manifest and index bytes on first fetch (during _resolve_root / successors). When _process_node later copies the root, it reads from memory instead of re-hitting the network. The stop_caching flag exists because map_root may re-fetch different content (platform selection) that should not pollute the cache. |
| 7 | +4. Hooks are the extension seam, not subclassing |
| 8 | +pre_copy, post_copy, on_copy_skipped, on_mounted are injected callables. The tagging mechanism (ensuring dst_ref ends up pointing at the root) is implemented entirely through these hooks, not through a special code path in the traversal. SkipNode raised from pre_copy short-circuits _do_copy_node — this is how ReferencePusher atomically pushes + tags the root in a single PUT. |
| 9 | +5. Two destination capabilities drive two tagging strategies |
| 10 | +- Plain Target (e.g., disk layout, in-memory): tag is a separate call; the hook fires after the copy |
| 11 | +- ReferencePusher (e.g., OCI registry): tag is embedded in the push URL; the hook fires before the copy and skips the normal push.. |
| 12 | +6. Mount is an opportunistic cross-repo blob copy |
| 13 | +_mount_or_copy_node only applies to non-manifest blobs when dst is a Mounter. It posts a server-side copy hint; the registry may honor it (201 → done) or refuse (202 → fall back to full upload). The fallback is unified with normal upload via the get_content closure, so mount and copy share the same post/pre-copy hook lifecycle. |
| 14 | +7. Foreign layer filtering is a pre-traversal filter, not a skip hook |
| 15 | +remove_foreign_layers() strips non-distributable layers from the successors list. These layers are never fetched, never pushed, and never dedup-checked. If you need to transfer them, you must override find_successors. |
| 16 | + |
| 17 | +``` |
| 18 | +### COPY Overview |
| 19 | +
|
| 20 | +ACTION copy(src, src_ref, dst, dst_ref, opts): |
| 21 | + state ← ValidatingInputs |
| 22 | + opts ← opts.clone() or default Options -- mutation safety |
| 23 | +
|
| 24 | + if dst_ref == "": dst_ref ← src_ref |
| 25 | +
|
| 26 | + -- ValidatingInputs |
| 27 | + assert src ≠ null else raise CopyError(SOURCE, "Resolve") |
| 28 | + assert dst ≠ null else raise CopyError(DEST, "Resolve") |
| 29 | + state ← CreatingProxy |
| 30 | +
|
| 31 | + -- CreatingProxy |
| 32 | + proxy ← CacheProxy(base=src, max_bytes=opts.max_metadata_bytes) |
| 33 | + state ← ResolvingRoot |
| 34 | +
|
| 35 | + -- ResolvingRoot |
| 36 | + root ← resolve_root(src, src_ref, proxy) -- see below |
| 37 | + state ← MappingRoot |
| 38 | +
|
| 39 | + -- MappingRoot |
| 40 | + if opts.map_root ≠ null: |
| 41 | + proxy.stop_caching ← true |
| 42 | + root ← opts.map_root(proxy, root) |
| 43 | + proxy.stop_caching ← false |
| 44 | + state ← InstallingHooks |
| 45 | +
|
| 46 | + -- InstallingHooks |
| 47 | + install_tagging_hooks(dst, dst_ref, proxy, root, opts) -- see below |
| 48 | + state ← RunningGraph |
| 49 | +
|
| 50 | + -- RunningGraph |
| 51 | + copy_graph(src=src, dst=dst, root=root, proxy=proxy, opts=opts) |
| 52 | + state ← Done(root) |
| 53 | + return root |
| 54 | +``` |
| 55 | + |
| 56 | +``` |
| 57 | +### Concurrency |
| 58 | +
|
| 59 | +───────────────────────────────────────────────────────────────────── |
| 60 | +WORKER LAUNCHER (with semaphore gating) |
| 61 | +───────────────────────────────────────────────────────────────────── |
| 62 | +
|
| 63 | +ACTION launch_workers(sem, fn, descriptors, cancel): |
| 64 | + for each desc in descriptors: |
| 65 | + thread ← new daemon Thread: |
| 66 | + if cancel.is_set(): return |
| 67 | + region ← LimitedRegion(sem) |
| 68 | + region.start() -- acquire slot (blocks if full) |
| 69 | + if cancel.is_set(): |
| 70 | + region.end(); return |
| 71 | + try: |
| 72 | + fn(region, desc) |
| 73 | + catch e: |
| 74 | + first_error ← e -- capture first error only |
| 75 | + cancel.set() -- signal all workers to stop |
| 76 | + finally: |
| 77 | + region.end() -- always release slot |
| 78 | + thread.start() |
| 79 | +
|
| 80 | +
|
| 81 | +───────────────────────────────────────────────────────────────────── |
| 82 | +COPY NODE |
| 83 | +───────────────────────────────────────────────────────────────────── |
| 84 | +
|
| 85 | +Per-node: Claimed → CheckingExistence |
| 86 | + → [Skipped | FindingSuccessors] |
| 87 | + → [YieldingSlot → WaitingForChildren → ReacquiringSlot]? |
| 88 | + → Copying → NodeDone |
| 89 | +
|
| 90 | +Single copy: PreCopy → [Skipped_ByHook | Transferring → PostCopy → NodeCopied] |
| 91 | +
|
| 92 | +States: Fetching → Buffering → VerifyingDigest → VerifyingSize |
| 93 | + → Pushing → Transferred |
| 94 | +
|
| 95 | +``` |
0 commit comments