Skip to content

Commit 02401f2

Browse files
Make sbt Docker e2e jar fetch survive Central 403/404s (#1291)
* Retry sbt Docker e2e jar fetch on Central 404s agent_sbt_versions_patch_in_place failed in 0.05s on PR run 37948228245 when Maven Central answered 404 for the pinned commons-text-1.9.jar. fetch_from_central retried only transport errors, 429 and 5xx, so a single stale CDN negative entry failed the cell before docker run. Every URL the helper fetches is a pinned, released artifact Central never deletes, so a 404 is the same CDN blip sbt-warm-seed.sh already retries; treat it the same way. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CfAQ1cTuWfH3kKiCi6pPR4 * Retry sbt e2e Central fetch on 403 too; send UA On this PR's own run (37954180751) the sbt 1.9.9 agent leg failed both tests in 0.07s with 403 Forbidden for the pinned commons-text-1.9.jar, while the other seven agent legs fetched the same jar fine. Like the 404, it is one runner's CDN edge rejecting a public, released artifact, so retry it the same way. Also identify the client with a User-Agent: reqwest sends none by default. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CfAQ1cTuWfH3kKiCi6pPR4 * Fall back to Google's Central mirror in sbt e2e coverage-docker (sbt) on this PR (run 37957095405) got 403 Forbidden from repo1.maven.org on all five tries in both agent tests, about 15s of backoff each, so retrying the same edge cannot ride it out. Alternate attempts between Central and Google's official Central mirror and check the jar against Central's published sha1, now pinned, instead of fetching the .sha1 from the same origin. Whichever mirror answers, the test patches the exact released bytes. Verified locally: a scratch test fetching through jars() passes, and with the Central URL broken (Central answers 403) the second attempt gets the jar from the mirror and the sha1 matches. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CfAQ1cTuWfH3kKiCi6pPR4 * Drop the custom User-Agent from the sbt e2e fetch With the User-Agent from d63015a, every Linux agent, mill and scala-cli leg of run 37957095379 and coverage-docker (sbt) got 403 from repo1.maven.org on all five tries. On the commit before it, without the header, seven of eight agent legs fetched the jar fine. The header is the likeliest trigger, so go back to reqwest's default request; the Google mirror fallback still covers a 403 from Central. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CfAQ1cTuWfH3kKiCi6pPR4 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1ecb42c commit 02401f2

1 file changed

Lines changed: 47 additions & 25 deletions

File tree

‎crates/socket-patch-cli/tests/docker_e2e_sbt.rs‎

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -63,29 +63,37 @@ const ORG: &str = "test-org";
6363
const PURL: &str = "pkg:maven/org.apache.commons/commons-text@1.9";
6464
const UUID: &str = "5b7a0000-0000-4000-8000-0000000000a1";
6565
const JAR: &str = "commons-text-1.9.jar";
66-
const CENTRAL_JAR: &str =
67-
"https://repo1.maven.org/maven2/org/apache/commons/commons-text/1.9/commons-text-1.9.jar";
66+
/// The jar's path under a Maven Central layout, served by every mirror in
67+
/// [`CENTRAL_MIRRORS`].
68+
const CENTRAL_JAR_PATH: &str = "org/apache/commons/commons-text/1.9/commons-text-1.9.jar";
69+
/// Central's published `.sha1` for [`CENTRAL_JAR_PATH`]. Pinned so the jar
70+
/// may come from either mirror and still be the exact released bytes.
71+
const CENTRAL_JAR_SHA1: &str = "ba6ac8c2807490944a0a27f6f8e68fb5ed2e80e2";
72+
/// Maven Central itself, then Google's official Central mirror. CI saw
73+
/// Central's CDN answer one runner 403 for every retry while other runners
74+
/// fetched the same jar fine, so a second origin is what makes the fetch
75+
/// survive an edge that has turned the runner away.
76+
const CENTRAL_MIRRORS: [&str; 2] = [
77+
"https://repo1.maven.org/maven2",
78+
"https://maven-central.storage-download.googleapis.com/maven2",
79+
];
6880
/// The marker member the patched jar adds.
6981
const MARKER: &str = "SOCKET_PATCHED.txt";
7082

7183
fn hex_of<D: Digest>(bytes: &[u8]) -> String {
7284
hex::encode(D::digest(bytes))
7385
}
7486

75-
/// The pristine jar from Maven Central (checked against Central's `.sha1`)
76-
/// and the patched one: every member copied raw, plus [`MARKER`].
87+
/// The pristine jar from Maven Central (checked against the pinned
88+
/// [`CENTRAL_JAR_SHA1`]) and the patched one: every member copied raw, plus
89+
/// [`MARKER`].
7790
async fn jars() -> (Vec<u8>, Vec<u8>) {
7891
let client = reqwest::Client::builder()
7992
.timeout(std::time::Duration::from_secs(60))
8093
.build()
8194
.expect("reqwest client");
82-
let get = |url: String| {
83-
let client = client.clone();
84-
async move { fetch_from_central(&client, &url).await }
85-
};
86-
let pristine = get(CENTRAL_JAR.to_string()).await;
87-
let sha1 = String::from_utf8(get(format!("{CENTRAL_JAR}.sha1")).await).unwrap();
88-
assert_eq!(hex_of::<Sha1>(&pristine), sha1.trim(), "Central sha1");
95+
let pristine = fetch_from_central(&client, CENTRAL_JAR_PATH).await;
96+
assert_eq!(hex_of::<Sha1>(&pristine), CENTRAL_JAR_SHA1, "Central sha1");
8997

9098
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(pristine.clone())).unwrap();
9199
let mut out = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
@@ -109,37 +117,51 @@ async fn jars() -> (Vec<u8>, Vec<u8>) {
109117
(pristine, patched)
110118
}
111119

112-
/// GET `url` from Maven Central, retrying what a CI runner sees as a
113-
/// transient blip: a transport error (DNS, connect, reset, timeout) or a
114-
/// 429 / 5xx. Any other status fails at once. Without this, one blip
115-
/// failed every cell of a leg within milliseconds, before `docker run`.
116-
async fn fetch_from_central(client: &reqwest::Client, url: &str) -> Vec<u8> {
117-
const ATTEMPTS: u32 = 5;
120+
/// GET `path` from Maven Central, alternating between [`CENTRAL_MIRRORS`]
121+
/// and retrying what a CI runner sees as a transient blip: a transport
122+
/// error (DNS, connect, reset, timeout) or a 403 / 404 / 429 / 5xx. Any
123+
/// other status fails at once. Without this, one blip failed every cell of
124+
/// a leg within milliseconds, before `docker run`. A 403 or 404 counts as a
125+
/// blip because `path` is a pinned, released, public artifact that Central
126+
/// never deletes or restricts: CI saw both from one runner's CDN edge
127+
/// while the other legs of the same run fetched the same jar fine (the 404
128+
/// is also what `sbt-warm-seed.sh` retries). The caller checks the bytes
129+
/// against a pinned sha1, so which mirror served them does not matter.
130+
async fn fetch_from_central(client: &reqwest::Client, path: &str) -> Vec<u8> {
131+
const ATTEMPTS: u32 = 6;
118132
let mut last = String::new();
119133
for attempt in 1..=ATTEMPTS {
134+
let url = format!(
135+
"{}/{path}",
136+
CENTRAL_MIRRORS[(attempt as usize - 1) % CENTRAL_MIRRORS.len()]
137+
);
120138
if attempt > 1 {
121-
eprintln!("{url}: attempt {} failed ({last}); retrying", attempt - 1);
122-
tokio::time::sleep(std::time::Duration::from_secs(1 << (attempt - 2))).await;
139+
eprintln!("{path}: attempt {} failed ({last}); retrying", attempt - 1);
140+
tokio::time::sleep(std::time::Duration::from_secs(1 << (attempt - 2).min(3))).await;
123141
}
124-
let resp = match client.get(url).send().await {
142+
let resp = match client.get(&url).send().await {
125143
Ok(resp) => resp,
126144
Err(e) => {
127-
last = format!("{e:?}");
145+
last = format!("{url}: {e:?}");
128146
continue;
129147
}
130148
};
131149
let status = resp.status();
132-
if status.is_server_error() || status == reqwest::StatusCode::TOO_MANY_REQUESTS {
133-
last = status.to_string();
150+
if status.is_server_error()
151+
|| status == reqwest::StatusCode::TOO_MANY_REQUESTS
152+
|| status == reqwest::StatusCode::NOT_FOUND
153+
|| status == reqwest::StatusCode::FORBIDDEN
154+
{
155+
last = format!("{url}: {status}");
134156
continue;
135157
}
136158
assert!(status.is_success(), "{url}: {status}");
137159
match resp.bytes().await {
138160
Ok(body) => return body.to_vec(),
139-
Err(e) => last = format!("{e:?}"),
161+
Err(e) => last = format!("{url}: {e:?}"),
140162
}
141163
}
142-
panic!("{url}: {ATTEMPTS} attempts failed, last: {last}");
164+
panic!("{path}: {ATTEMPTS} attempts failed, last: {last}");
143165
}
144166

145167
/// The authenticated patch API serving one whole-jar patch for

0 commit comments

Comments
 (0)