feat(engine): implement async httpx range download engine and worker pool - #131
Open
archittmittal wants to merge 2 commits into
Open
archittmittal wants to merge 2 commits into
archittmittal wants to merge 2 commits into
Conversation
…pool Replace the placeholder `download`/`resume` commands (which crashed with `AttributeError: 'StateManager' object has no attribute 'initialize'`) with a real parallel range downloader. - reliadl/download_engine.py: HEAD probe with one-byte range GET fallback (size, Accept-Ranges, ETag); fixed-size chunk planning; worker pool bounded by max_parallel_workers over a pooled httpx.AsyncClient (HTTP/2 when h2 is installed); positional writes into a pre-allocated <output>.part via SparseFileWriter; per-chunk streaming SHA-256; If-Range protection against mid-download changes; retries with exponential backoff, jitter and Retry-After; token-bucket bandwidth cap; fsync + atomic state checkpoint after every chunk; whole-file verification and atomic rename on completion. - Graceful shutdown: request_shutdown() (wired to SIGINT/SIGTERM) or task cancellation stops workers and checkpoints state; resume re-probes the origin, refuses if size/ETag changed, and re-hashes every chunk marked complete before trusting it. - CLI: download gains --expected-hash/--sha256, --workers, --chunk-size, --limit-rate, --config; resume drives the engine; progress line on TTY; exit code 130 on interruption. - New RangeNotSupportedError and DownloadCancelledError exceptions. - httpx added as a dependency, with optional http2/socks extras. - tests/unit/test_download_engine.py: 20 tests against a local threaded range server (concurrency bound, retries, abandon + resume, shutdown checkpoint, corrupted-chunk revalidation, remote-change rejection, CLI). Closes #41 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| await asyncio.sleep(0.02) | ||
| task.cancel() | ||
| with self.assertRaises(asyncio.CancelledError): | ||
| await task |
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #41
Why
Right now
reliadl downloadcrashes on every URL, both in the v0.3.0 PyPI release and on master:It was a placeholder, and
resumeonly printed a message. This PR adds the parallel range-download engine and connects both commands to it.What changed
reliadl/download_engine.py(new)HEADrequest for size,Accept-RangesandETag. IfHEADis refused or ranges aren't advertised, it falls back to a one-byteRange: bytes=0-0GET and reads the total fromContent-Range.asyncio.Queue, andmax_parallel_workersworkers share one pooledhttpx.AsyncClient. HTTP/2 is used whenh2is installed.Accept-Encoding: identitykeeps byte offsets meaningful.<output>.partusing the existingSparseFileWriter(pwrite). Each chunk is SHA-256 hashed as it streams.If-Rangewith a strong ETag (or Last-Modified), so a file that changes mid-download is caught. The engine also checksContent-Range, rejects over-long responses, and detects truncated bodies.Retry-Afteris respected. Errors that can't succeed on retry (4xx, 412, range ignored) stop the run straight away.TokenBucketRateLimiter..partfile is fsynced and then the state is saved atomically. The state file never claims bytes that aren't on disk yet..partis atomically renamed to the output, and the state file and empty state directory are removed.Graceful shutdown and resume
DownloadEngine.request_shutdown()is hooked to SIGINT and SIGTERM. Cancelling the task (Ctrl+C on Windows) does the same. Workers are cancelled, in-flight chunks go back toPENDING, state is saved asCANCELLED, andDownloadCancelledErroris raised. The CLI prints theresumecommand and exits with status 130.resumeprobes the server again and refuses if the size or ETag changed. It re-hashes every chunk marked complete in.partbefore trusting it, then fetches only what's missing.CLI
downloadgets new options:--expected-hash/--sha256,--workers/-j,--chunk-size,--limit-rateand--config.resumeaccepts the same engine options.Other
RangeNotSupportedErrorandDownloadCancelledError.httpx>=0.28.1is now a dependency, with optional extrasreliadl[http2]andreliadl[socks].docs/CLI_GUIDE.mdis updated.Out of scope
RangeNotSupportedErrorinstead of a crash, and feat(engine): implement single-stream fallback engine for non-Range servers #42 can catch that error to fall back.--adachunk/--whittleare still accepted but don't do anything yet, same as before. The engine uses fixed-size chunks.Testing
tests/unit/test_download_engine.pyadds 20 tests that run against a real local range server. They cover:download,resumeand invalid-configuration pathsruff check --select=E,Fis clean.https://proof.ovh.net/files/10Mb.datwith 4 workers and 1 MB chunks. The SHA-256 matchedcurl | shasum. A SIGINT in the middle exited 130 with 1 of 10 chunks checkpointed, andreliadl resumefinished with the same hash.reliadlpackage, and a download worked.🤖 Generated with Claude Code