Skip to content

Bound and classify local version-manager network operations - #496

Open
sdairs wants to merge 3 commits into
issue-463-parse-version-operandsfrom
issue-459-version-network-bounds-v2
Open

Bound and classify local version-manager network operations#496
sdairs wants to merge 3 commits into
issue-463-parse-version-operandsfrom
issue-459-version-network-bounds-v2

Conversation

@sdairs

@sdairs sdairs commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Bound version resolution, build listing, master checks, and streamed downloads with explicit connect, read, and total operation deadlines through the shared HTTP client.
  • Classify failures by stage, host, and stable status category; treat only build-probe 403/404 as expected fallback and preserve the original unexpected probe failure when fallback also fails.
  • Retry installer downloads at most three times for transport, timeout, 429, and 5xx failures, honoring Retry-After within a five-second backoff cap.

Tests

  • cargo test -p clickhousectl version_manager (95 passed)
  • cargo test -p clickhousectl (all passed)
  • cargo fmt --all --check
  • cargo clippy -p clickhousectl --all-targets -- -D warnings

Stack

Closes #459

@sdairs sdairs changed the title issue 459 version network bounds v2 Bound and classify local version-manager network operations Aug 24, 2026
Comment on lines +100 to +107
let category = match status.as_u16() {
403 => NetworkCategory::Forbidden,
404 => NetworkCategory::NotFound,
429 => NetworkCategory::RateLimited,
_ if status.is_server_error() => NetworkCategory::Server,
_ if status.is_client_error() => NetworkCategory::Client,
_ => NetworkCategory::UnexpectedStatus,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High version_manager/network.rs:100

HTTP 408 responses are classified as Client, so download_with_retry treats transient request timeouts as non-retryable and aborts artifact installation immediately. Classify 408 as Timeout so is_retryable() applies the retry policy.

Suggested change
let category = match status.as_u16() {
403 => NetworkCategory::Forbidden,
404 => NetworkCategory::NotFound,
429 => NetworkCategory::RateLimited,
_ if status.is_server_error() => NetworkCategory::Server,
_ if status.is_client_error() => NetworkCategory::Client,
_ => NetworkCategory::UnexpectedStatus,
};
let category = match status.as_u16() {
403 => NetworkCategory::Forbidden,
404 => NetworkCategory::NotFound,
408 => NetworkCategory::Timeout,
429 => NetworkCategory::RateLimited,
_ if status.is_server_error() => NetworkCategory::Server,
_ if status.is_client_error() => NetworkCategory::Client,
_ => NetworkCategory::UnexpectedStatus,
};
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @crates/clickhousectl/src/version_manager/network.rs around lines 100-107:

HTTP 408 responses are classified as `Client`, so `download_with_retry` treats transient request timeouts as non-retryable and aborts artifact installation immediately. Classify 408 as `Timeout` so `is_retryable()` applies the retry policy.

Comment on lines 118 to +132
@@ -127,11 +129,20 @@ pub async fn list_available_versions_from_builds() -> Result<Vec<String>> {
for mm in (1..=12).rev() {
let version_path = format!("{}.{}", yy, mm);
let url = builds_probe_url(&version_path, &platform);
match client.head(&url).send().await {
Ok(resp) if resp.status().is_success() => {
match client.head(&url, NetworkStage::BuildsList).await {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High version_manager/list.rs:118

list_available_versions_from_builds returns a timeout instead of the available-version list when the 84 sequential HEAD probes exceed the single 30-second OperationClient deadline, even though the connection remains usable. Because the client is created once at line 119, every probe shares that deadline; create independent per-probe deadlines or give the full scan a deadline sized for all requests.

-    let first_url = builds_probe_url("20.1", &platform);
-    let client = OperationClient::metadata(NetworkStage::BuildsList, &first_url)?;
-
     let current_year = chrono::Utc::now().year() as u32;
@@
             let version_path = format!("{}.{}", yy, mm);
             let url = builds_probe_url(&version_path, &platform);
+            let client = OperationClient::metadata(NetworkStage::BuildsList, &url)?;
             match client.head(&url, NetworkStage::BuildsList).await {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @crates/clickhousectl/src/version_manager/list.rs around lines 118-132:

`list_available_versions_from_builds` returns a timeout instead of the available-version list when the 84 sequential HEAD probes exceed the single 30-second `OperationClient` deadline, even though the connection remains usable. Because the client is created once at line 119, every probe shares that deadline; create independent per-probe deadlines or give the full scan a deadline sized for all requests.

Comment on lines +271 to +275
let future = Utc::now() + chrono::Duration::seconds(30);
let parsed = parse_retry_after(&future.to_rfc2822()).unwrap();
assert!(parsed >= Duration::from_secs(28));
assert!(parsed <= Duration::from_secs(30));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium version_manager/network.rs:271

This test fails nondeterministically when the task is delayed for roughly two seconds between constructing future and calling parse_retry_after, because parsed then falls below the hard-coded 28-second lower bound. Use a fixed RFC 2822 input and a non-time-sensitive assertion instead.

-        let future = Utc::now() + chrono::Duration::seconds(30);
-        let parsed = parse_retry_after(&future.to_rfc2822()).unwrap();
-        assert!(parsed >= Duration::from_secs(28));
-        assert!(parsed <= Duration::from_secs(30));
+        let parsed = parse_retry_after("Thu, 01 Jan 2099 00:00:00 +0000").unwrap();
+        assert!(parsed > Duration::from_secs(60 * 60 * 24 * 365));
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @crates/clickhousectl/src/version_manager/network.rs around lines 271-275:

This test fails nondeterministically when the task is delayed for roughly two seconds between constructing `future` and calling `parse_retry_after`, because `parsed` then falls below the hard-coded 28-second lower bound. Use a fixed RFC 2822 input and a non-time-sensitive assertion instead.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High

if let Some(minor) = highest_available {

resolve_major returns an earlier highest_available minor after a later probe fails, so a timeout or HTTP 5xx while checking 25.11 causes 25.10 to be selected without checking 25.12. Only return the probed minor when no probe failure occurred; otherwise continue to the GitHub fallback or return the classified failure.

-    if let Some(minor) = highest_available {
+    if let Some(minor) = highest_available.filter(|_| first_probe_failure.is_none()) {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @crates/clickhousectl/src/version_manager/resolve.rs around line 164:

`resolve_major` returns an earlier `highest_available` minor after a later probe fails, so a timeout or HTTP 5xx while checking `25.11` causes `25.10` to be selected without checking `25.12`. Only return the probed minor when no probe failure occurred; otherwise continue to the GitHub fallback or return the classified failure.

return Some(Duration::from_secs(seconds));
}

let retry_at = DateTime::parse_from_rfc2822(value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium version_manager/network.rs:154

parse_retry_after returns None for valid HTTP-date Retry-After headers in RFC 850 (Sunday, 06-Nov-94 08:49:37 GMT) or ANSI asctime (Sun Nov 6 08:49:37 1994) form. Because line 154 only tries parse_from_rfc2822, those responses are ignored and the retry policy falls back to its short exponential delay; parse both additional HTTP-date formats before returning None.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @crates/clickhousectl/src/version_manager/network.rs around line 154:

`parse_retry_after` returns `None` for valid HTTP-date `Retry-After` headers in RFC 850 (`Sunday, 06-Nov-94 08:49:37 GMT`) or ANSI `asctime` (`Sun Nov  6 08:49:37 1994`) form. Because line 154 only tries `parse_from_rfc2822`, those responses are ignored and the retry policy falls back to its short exponential delay; parse both additional HTTP-date formats before returning `None`.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 39457ac. Configure here.

}

/// `install 25.12.9.61` — exact version, needs channel from GH API
async fn resolve_exact(version: &str, platform: &Platform) -> Result<ResolvedVersion> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major resolve drops confirmed builds

High Severity

resolve_major only returns a builds source when every probe succeeds. After some minors are marked Available, a later unexpected probe error still discards highest_available and forces GitHub fallback. If that fallback also fails, install errors even though a builds path was already confirmed.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 39457ac. Configure here.

)
.into());
}
Err(error) => return Err(error.into()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Builds list deadline too short

Medium Severity

list_available_versions_from_builds runs up to roughly seven years of monthly HEAD probes on one OperationClient::metadata client, which shares a single 30s total deadline. Moderate latency or a couple of slow responses exhausts that budget mid-scan and fails the whole local list instead of finishing the listing.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 39457ac. Configure here.

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.

Bound and classify local version-manager network operations

1 participant