Skip to content

cli: avoid whole-file remote download timeouts - #1810

Draft
amacneil wants to merge 1 commit into
mainfrom
cursor/cli-remote-download-timeout-4d7f
Draft

cli: avoid whole-file remote download timeouts#1810
amacneil wants to merge 1 commit into
mainfrom
cursor/cli-remote-download-timeout-4d7f

Conversation

@amacneil

Copy link
Copy Markdown
Contributor

Context

mcap filter --allow-remote-scan of a large remote file (the ~40GB S3 case in #1803 (comment)) fails after about 3 minutes with nested operation timed out / request or response body error. Indexed commands (info, list, get) are unaffected because they only fetch a small tail.

object_store 0.13.2 applies a 30s client timeout from connect through the entire response body. Progress does not reset it. Failed attempts are retried until the 180s retry budget is exhausted, which matches the reporter's timing. v0.0.62 (Go CLI) did not have this timeout.

Local repro is transfer duration, not file size: a throttled HTTP body that takes longer than 30s hits the same error. Loopback 40GB can finish in under 30s, so throttling is the right repro, not a huge local file.

Approach

Whole-file downloads now:

  • Keep parse_url_opts (so GCS allow_http and other builder defaults stay intact).
  • Set a long per-GET timeout (7days) on the download path only. Indexed/bounded reads still use the default 30s timeout. Connect timeout is unchanged.
  • Bound waiting for each response head at 240s (above the 180s retry budget) so a silent endpoint cannot hang forever.
  • Detect a stalled body (no bytes for 120s) separately, so an in-progress transfer is not killed.
  • Download in 64MiB ranged GETs so each part gets a fresh object_store retry budget. Follow-up parts pin the first part's ETag (If-Match) so a mid-download overwrite fails instead of tearing the file.
  • Fall back to an unranged GET when the store ignores Range or the object is empty (HTTP 416).
  • Print TTY stderr progress (Downloading X / Y).

No new --timeout flag.

This does not make filter indexed. Rewrite commands still materialize the whole remote file; that remains a follow-up.

Testing

  • Unit tests for chunked range downloads, empty-object 416 fallback, download vs indexed timeout options, and 416 status matching (not a raw "416" substring).
  • cargo clippy -p mcap-cli --all-targets -- --no-deps -D warnings
  • cargo test -p mcap-cli

Questions for review

These are product choices I made and would like input on:

  1. Part size. 64MiB keeps a fresh 180s retry budget only above roughly 370KB/s. 16MiB would hold that guarantee down to ~90KB/s at 4× the request count.
  2. Stall at 95%. A 120s stall currently aborts the whole download. We already track offset and could re-issue the remainder of the stalled part a bounded number of times.
  3. Head-phase 240s cap. Pre-patch, a blackholing endpoint failed in ~3 minutes. I kept a finite head wait (240s) rather than waiting indefinitely with a frozen progress line.
  4. Mutating objects. Follow-up parts use If-Match. A replaced object fails with remote object changed while downloading.

Related: Linear DB-1587. This PR addresses the filter timeout in #1803; it does not close that issue (credentials / ~/.aws are separate, see #1808).

Open in Web Open in Cursor 

object_store's 30s request timeout covers connect through the entire
body, so mcap filter --allow-remote-scan of a large remote file dies
after retries exhaust the 180s budget. Use a long per-GET timeout for
downloads, stall detection, and ranged parts with a fresh retry budget.

Co-authored-by: adrian <adrian@foxglove.dev>
Comment thread rust/cli/src/source.rs
})?
}
};
let written = self.stream_get_to_writer(response, writer, &mut progress)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The "resume on the next part instead of restarting the whole object" property only holds between parts. object_store's fresh retry budget covers each part's head request, but a stall (120s) or a mid-body connection drop inside stream_get_to_writer bubbles up here via ? and aborts the entire download — discarding every byte already written to the temp file. So on a flaky link, one TCP reset in any single 64MiB part restarts from zero, which is the exact failure mode this PR set out to kill.

You already track offset and pin the ETag — re-issuing offset..end on a body error/stall (bounded retries) is what makes this genuinely resumable. This is the same root cause as your question #2 (stall at 95%); worth handling body errors and stalls together rather than only stalls. What does the user see today if the connection blips 30GB into a 40GB download — a full restart? 😭

@linear-code

linear-code Bot commented Aug 13, 2026

Copy link
Copy Markdown

DB-1602

@mwcondino

Copy link
Copy Markdown

Tested this out in the environment in which I originally saw the issue, it appears to have fixed the issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants