Skip to content

gantry acr artifact streaming - #836

Draft
Vaibhav Patel (vpatelsj) wants to merge 51 commits into
mainfrom
copilot/gantry-acr-artifact-streaming
Draft

Vaibhav Patel (vpatelsj) wants to merge 51 commits into
mainfrom
copilot/gantry-acr-artifact-streaming

Conversation

@vpatelsj

Copy link
Copy Markdown
Contributor

No description provided.

@vpatelsj Vaibhav Patel (vpatelsj) changed the title Copilot/gantry acr artifact streaming gantry acr artifact streaming Sep 24, 2026
@vpatelsj
Vaibhav Patel (vpatelsj) force-pushed the copilot/gantry-acr-artifact-streaming branch from 3d5f410 to 92842e1 Compare September 25, 2026 12:06
Six agent knobs were duplicated across the config struct, defaults, env
vars, CLI flags, validation, chart values, JSON schema, and the rendered
ConfigMap. Four of them were tuning values an operator cannot evaluate
from the outside, so keep only the feature gate and the signed-origin
host allowlist and move the rest into internal/gantry/streaming beside
the behavior they govern.

NewOriginClient no longer takes tuning parameters and cannot fail, and
streaming.Options now falls back to defaults instead of rejecting a zero
value, which removes an error path from agent startup.

Also stop writing logConfig.logLevel on the host. The configurator owns
p2pConfig only; the test now pins that unrelated OverlayBD settings are
left exactly as the host had them.
The live streaming e2e sent sig=redacted, so the plan's secret-free
logs contract passed without testing anything. Use a sentinel SAS value
and assert it is absent from every Gantry pod's logs and metrics after
the request has demonstrably transited the agent.
Add a stale-provider e2e: remove the blob from the provider's containerd
while its DHT record survives, then assert the next range fails over to
the signed origin inside one request and the peer serves nothing more.
libp2p has no protocol-level withdraw, so this is the real production
shape rather than a simulated error.

Make the test origin reject any request whose URI did not arrive
byte-for-byte and give it the ACR data-path form, doubled slash and
percent-encoded query included. Raw signed-URL preservation through the
real listener was previously only covered by a unit test with a
hand-set RequestURI.

Factor the shared cluster setup into a fixture so the two scenarios do
not duplicate it.

Document the operations surface the plan required and was missing:
metrics reference, four alert rules separating Gantry, ACR, peer-reuse
and allowlist failures, an incident table, and a compatibility matrix
naming who owns each component.
ACR exposes the Artifact Streaming manifest under a different digest that
control-plane tag metadata never reports. Resolve it through the node-local
AKS helper instead, which returns Docker-Content-Digest.
Selectable() ignored lease expiry, so dead holders stayed rankable and kept
SelectableCount() at the full holder count. That satisfied the early-return in
attemptClaim, so reclaimableChairs never ran and the stale set was
self-sustaining. Rank and the counts now treat a chair as usable only when it
is within the abandon threshold, measured against an injectable store clock.
The blob serve cap exists to preserve bandwidth per large-layer stream.
Artifact Streaming index reads inherited it accidentally: a few KiB on the
container-start path queued behind 1 GiB transfers. Small bounded ranges now
take a separate semaphore, and only ranges with both bounds present qualify so
an unbounded transfer cannot claim the lane. Range fetches also get their own
h2 transport, since sharing one connection with bulk streams put them behind
the same flow-control window and idle-ping fate.
PrefetchManifestChildren only dispatched PleasePull to ranked chairs, so a
node that served a manifest never fetched the layers itself. Under Artifact
Streaming containerd never requests the blobs either, so the per-node Resolve
path never fired and coverage stalled at a few replicas per layer instead of
one per node. OnManifestServed now seeds every child into the local content
store, preferring peers and falling back to origin.

Readiness no longer gates on chair availability: a node with no selectable
chair can still serve and seed, and gating kept it out of rotation.
Every node seeds every layer, so an origin fallback turns one cold image
into nodes x layers concurrent origin pulls. At 1000 nodes that was up to
4000 concurrent 1 GiB pulls through the node-local ACR helper, which
starved the OverlayBD range reads that gate container start: 174 GB left
origin and not one layer ever committed.

Seeding is now peer-only by default and polls the swarm for longer instead
of giving up after ten minutes. Bootstrapping a cold image stays the chair
prefetch path's job, which pulls from a small designated set of nodes.
Also expose gantry_layer_seed_total through the benchmark scrape allowlist,
which silently dropped it before.
The containerd content store is all-or-nothing, so a peer could not serve
any part of a 1 GiB layer until it held the whole thing. Under Artifact
Streaming nothing is ever complete during a cold start, which left every
node reading OverlayBD indexes straight from origin: 1000 nodes x up to 32
concurrent origin reads each, against a registry that answers 429.

Ranges fetched from a peer or origin are now kept in a disk-backed cache
and served back out of the transfer endpoint even when the blob itself is
absent, and the node announces the digest so the swarm can find it. Only
reads up to MaxCachedRangeBytes are admitted, so bulk layer traffic cannot
evict the small index reads that actually gate container start.
The advertiser proves a digest is serveable by opening it in the content
store and reconciles announcements against that same inventory, so a node
holding only cached ranges was invisible: Notify refused to announce it and
reconcile would have withdrawn it anyway. Peers could never discover a
range source, which left the range cache purely local and the origin
stampede intact.
Measured on a node after a successful run: the range cache held 40 layers,
41 GB apparent but only 52 MB allocated. Probing a 1.08 GB layer shows the
only bytes a container touches are the head and the last ~1 MB - 0.13% of
the image. Everything in between is never read at mount time.

Fetching those on demand makes the first CreateContainer fail, and the pod
then waits out kubelet backoff, which is what put a ~130s floor under
startup with a min of 130s and a max of 178s - a fixed cost, not
contention. Serving a manifest now warms both regions for every layer,
peer-first and jittered so a cold image does not put the whole fleet on
origin at once.
p90 startup was 15s but p95 was 44s, which is the shape of a minority of
nodes starting before their index warm finished. Halve the spread and
double the per-node concurrency; origin only carried 1.72 GB across the
fleet, so there is ample headroom.
Measured at 1000 nodes. Concurrency 16 with a 500ms spread against
concurrency 8 with 2s: p90 42s vs 15s, max 80s vs 50s, job window 103s vs
70s, p50 unchanged at 11s vs 10s. Warming harder just makes nodes contend,
so restore the settings that produced the better distribution.

One sample per configuration, so treat the p50 difference as noise; the p90
and window gaps are large enough to act on.
@vpatelsj
Vaibhav Patel (vpatelsj) force-pushed the copilot/gantry-acr-artifact-streaming branch from acbf8fb to 7e68855 Compare October 16, 2026 10:07

This branch has not been deployed

No deployments
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.

1 participant