You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: byte budget for buffered fallbacks + ECR commit-gate
Two structural fixes for high-concurrency ECR / large-layer workloads
surfaced by the perf/profile-harness critical review:
Byte budget for buffered uploads
GHCR/GAR/ACR fallbacks buffer the entire blob in memory; the prior
count-only cap let 64 concurrent 1 GB layers (CUDA/ML images) pin
~64 GB resident. New buffered_blob_bytes (default 512 MB) caps
cross-call bytes via a tokio Semaphore; each fallback acquires
min(known_size, budget) permits before buffer_stream via a new
acquire_buffered_upload_permits helper.
ECR consistency gate for manifest commit
ECR's manifest validator lags blob PUT-201 by hundreds of ms at
high concurrency, surfacing as BLOB_UPLOAD_UNKNOWN on manifest PUT
and exhausting the retry budget. New
BatchBlobChecker::wait_for_blobs_available polls
BatchCheckLayerAvailability with exp backoff (200ms -> 5s cap)
before manifest PUT, gated on RetryConfig::manifest_commit_wait
(30s prod, 0 in fast_retry). Wired into push_manifests AND
discover_and_sync_artifacts.
Artifact pipeline parity
discover_and_sync_artifacts now uses batch-check pre-population +
BLOB_CONCURRENCY-capped parallel blob processing, matching the main
image flow, with the same manifest_commit_wait gate before artifact
manifest PUT.
Diagnostic CLI flags removed
--force-http1 and --no-h2-adaptive-window dropped from production
CLI. The #[doc(hidden)] pub builder methods are retained for the
perf-profile test harness and documented in client.rs +
ocync-distribution/CLAUDE.md so future audits don't flag them as
dead.
Idiomatic cleanups
- Provider dispatch in blob_push_stream: if-chain to exhaustive match
(new ProviderKind variants fail to compile until handled)
- fastrand replaces hand-rolled RandomState jitter (retry.rs)
- should_retry_transport keeps is_request/is_body/is_decode only;
is_connect/is_timeout are subsets of is_request on the async-hyper
path. New test pins the timeout-vs-is_request equivalence.
- Option<&Rc<dyn>> -> Option<&dyn> for pass-through trait params
- HashSet collect dedup replaces filter-and-insert pattern
- buffer_stream doc-vs-code drift fixed
- std::cmp::min nesting -> chained .min() across 5 sites
1399 tests passing; cargo deny / fmt / clippy -D warnings clean.
Copy file name to clipboardExpand all lines: crates/ocync-distribution/CLAUDE.md
+12Lines changed: 12 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -96,6 +96,18 @@ Provider names:
96
96
97
97
`build_registry_client` (`src/cli/mod.rs`) calls `ocync_distribution::install_crypto_provider()` at the top -- production main does this too, but the dispatch entry point is also reached from tests that bypass main, so the install must be idempotent there.
98
98
99
+
## Perf-harness test hooks on `RegistryClientBuilder`
100
+
101
+
Three `#[doc(hidden)] pub` builder methods exist on `RegistryClientBuilder` solely for in-repo perf and A/B harnesses (primarily `crates/ocync-sync/tests/perf_profile.rs`):
102
+
103
+
-`allow_invalid_certs(bool)` -- terminates TLS at a `testcontainers``registry:2` with a self-signed cert.
104
+
-`force_http1(bool)` -- compares HTTP/2 vs HTTP/1.1 throughput.
These are **intentionally retained**. They look unused from a production-code grep because production code never reaches them (no CLI flag, no env var, no config setting). Removing them as "dead code" deletes load-bearing diagnostic infrastructure: any future perf investigation that needs to reproduce the HTTP/2 stall investigation (or any successor) would have to re-introduce the same plumbing from scratch.
108
+
109
+
If you're tempted to delete them, search `tests/perf_profile.rs` first -- it references all three by name.
.expect("first upload POST must fire within 5s (task 1 likely stuck)");
1612
+
1613
+
let client_2 = Arc::clone(&client);
1614
+
let blob_b_clone = blob_b.clone();
1615
+
let digest_b_clone = digest_b.clone();
1616
+
let h2 = tokio::spawn(asyncmove{
1617
+
client_2
1618
+
.blob_push_stream(
1619
+
&repo_b,
1620
+
&digest_b_clone,
1621
+
Some(blob_b_clone.len()asu64),
1622
+
data_stream(&blob_b_clone,65536),
1623
+
)
1624
+
.await
1625
+
});
1626
+
1627
+
let r1 = h1.await.unwrap();
1628
+
let r2 = h2.await.unwrap();
1629
+
assert!(r1.is_ok(),"first upload should succeed: {:?}", r1.err());
1630
+
assert!(r2.is_ok(),"second upload should succeed: {:?}", r2.err());
1631
+
1632
+
let times = post_times.lock().unwrap();
1633
+
assert_eq!(times.len(),2,"both POSTs must have fired");
1634
+
let gap = times[1].duration_since(times[0]);
1635
+
// Task 2 cannot POST until task 1 releases the byte permit,
1636
+
// which happens when task 1's `blob_push_stream` returns
1637
+
// (after PATCH-delay + fast PUT). The lower bound is
1638
+
// patch_delay minus a generous CI margin (100 ms) since we
1639
+
// already eliminated the head-start race above.
1640
+
let lower_bound = patch_delay.saturating_sub(Duration::from_millis(100));
1641
+
assert!(
1642
+
gap >= lower_bound,
1643
+
"second POST started after only {gap:?}; expected the byte budget to queue it for at least ~{lower_bound:?} behind the first (patch_delay = {patch_delay:?})"
0 commit comments