Repository navigation
Pull the whole bundle in one streamed request over hgresume - #406
hahn-kev-bot wants to merge 9 commits into
Conversation
The floating 18.0.0-* libpalaso references started resolving to stable 18.0.0, whose minimums for L10NSharp, icu.net, System.Resources.Extensions and NUnit are above what Chorus pinned, so restore failed with NU1605. Raise those pins and pin libpalaso to 18.0.0. L10NSharp 11 no longer pulls in System.ServiceModel, so Chorus references it directly. Security: System.ServiceModel.Primitives 4.8.1 -> 10.1.0 drops the vulnerable System.Security.Cryptography.Xml chain from LibChorus's netstandard2.0 graph (4.10.x and 8.x still carry vulnerable versions). NuGet.CommandLine 6.7.0 -> 6.14.3 in l10n.proj (GHSA-68w7-72jg-6qpp). l10n.proj referenced a nonexistent PkgGitVersion_GitVersion_MsBuild property, so its manual GitVersion import never ran (NuGet already imports those targets) and RestartBuild was always true. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ChorusHub.wixproj installs its own SIL.BuildTasks via nuget.exe for the FileUpdate task that stamps the license year, separate from the PackageReference in ChorusHub.csproj. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Test Results 9 files ± 0 353 suites +6 50m 34s ⏱️ - 10m 5s Results for commit fdff357. ± Comparison against base commit 838af4c. This pull request removes 1 and adds 22 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
The resumable transport used to pull a bundle in chunks sized to finish inside a 30 second request timeout, so a slow link spent much of its time on round trips and a chunk that timed out was thrown away. Now a pull asks the server for everything after what is already on disk, in one pullBundleChunk request, and writes the body into the bundle file as it arrives. The timeout bounds each wait for data rather than the whole download, and when the connection drops the next request resumes from the bytes that did arrive. No server change is needed: the server already serves any offset and caps chunkSize at what is left of the bundle. IApiServer gains an Execute overload that streams the response body to a Stream, with a progress callback; non-200 responses are still buffered. The pull progress line is now rate based and updated at most once a second. PushStorageManager.GetChunk never allocates more than the bundle has left, and PullStorageManager can open the bundle for writing from an offset, truncating anything past it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LLSihB4ffNi95ioJQEvsLr
Tested against the hgresume server built from source: asking for the whole bundle in the first request cost every pull a 14 second wait, because the server only answers at once when it can satisfy the request from the bundle file as it stands, and otherwise polls for 14 seconds before answering "in progress". A pull of one small change took 22 s. So the client now asks for one byte first (served as soon as the bundle file starts, and it brings back the size so far), then for all but the last byte of what the server has (served at once, even while the server is still adding to the file), and only then for the rest in one streamed request. The same small pull takes three quick requests, and a 3 MB clone dropped from 37 s to 9 s. A cut connection still resumes from the bytes received; the body length of the cut response is kept as the bundle size seen so far. The integration tests can now run against a server already running on the machine (HGRESUME_URL and HGRESUME_REPO_ROOT) instead of the container, and gain a large-bundle clone and a clone through a proxy that cuts the connection four times part-way through the download. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LLSihB4ffNi95ioJQEvsLr
While the server is still making a bundle, a request for the rest of it waits out the server's whole 14 second poll and comes back "in progress" without saying how far the server has got. The client then asked for the rest again, so a long clone downloaded nothing more until the bundle was finished. Now an "in progress" reply sends the client back to a one byte probe, which the server answers as soon as the bundle has grown, so the download keeps up with the server again. A single request can now carry the whole bundle, so Cancel, which was only checked between requests, could do nothing for the length of the download. It is now checked as the body arrives; the bytes received stay in the bundle file and the next pull resumes after them. Also abort the stalled-body test's response, since HttpListener on Windows refuses to close a response that is short of its Content-Length. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
76eb8a2 to
b755902
Compare
The progress reporter's throttle started _lastReportMs at long.MinValue, so now - _lastReportMs overflowed to a negative number and every update was skipped until the whole bundle had arrived. On a slow link the bar and ETA sat still for the entire download. Start it one interval in the past instead, so the first report goes out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CopyBody turned any IOException into HttpRequestException, including one from writing the bundle file. On a full disk the pull took that for a dropped connection and retried forever, asking the server for the rest of the bundle each time. Only failures reading the response are now treated as connection errors; a write error goes up to the caller as it is, and the partial bundle stays on disk for the next pull to resume. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…quest-patches-1725c0
On .NET Framework, HttpListener (http.sys) never put the 400 bytes of the partial body on the wire before Abort, so the client saw a reset after 0 bytes and the test failed there, though the client itself keeps what arrives. A raw socket sends the headers and the 400 bytes and then resets the connection, the same on both frameworks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rmunn
left a comment
There was a problem hiding this comment.
LGTM, though I haven't actually run the code yet. I would like to give it some real testing, to make sure the works-on-my-machine isn't in operation in the reported speeds. But since I expect those findings to confirm the numbers reported, I think this can go ahead and be merged.
jasonleenaylor
left a comment
There was a problem hiding this comment.
The implementation looks good to me. I checked all the pitfalls and edge cases I could think of and it seems sound.
I'm holding off approving for one reason: when we last talked about this, the plan was to roll it out as a secondary option alongside the chunked pull, but this PR replaces the chunked pull outright. Is that still the plan? A selectable option would let us beta test it with real users on poor connections, where buffering proxies and antivirus are most likely to show up, and switch them back without a new release if it misbehaves.
This review was assisted by Claude Opus 5.5.
|
Thanks for the review. Yes that's still the plan. |
Waiting on #408 so we can release this while letting users use the old style if they need to.
AI summary
Summary
Over the resumable transport, a pull used to fetch the bundle in chunks sized to finish inside the 30 second timeout. Now it fetches it in as few requests as the server allows, writing the body to the bundle file as it arrives:
IApiServergains anExecuteoverload that streams a 200 body to aStream, with a progress callback.HGRESUME_URL,HGRESUME_REPO_ROOT). They add a 3 MB clone, and a clone through a proxy that cuts the connection four times.Evidence
Before: a pull of one small change took 22 s, and a 3 MB clone took 37 s.
After: the small pull takes three quick requests, and the clone takes 9 s.
Before:
Pull_UserCancelsMidBody_StopsAndNextPullResumesFromBytesReceivedfails. The cancelled request runs to the end, and the next pull resumes at byte 1049399.After: it passes. The download stops half way, and the next pull resumes at byte 524700.
Before:
Pull_InProgressWhenCaughtUp_ProbesNextfails because the next request asks for the rest again (chunkSize 2146434248), which waits out the server's 14 s poll every time.After: it passes. The next request is a 1-byte probe.
LibChorus.Testspass on net8.0.Merge Danger
Door: two-way
The client change needs no protocol change, and reverting it restores chunked pulls. A bundle half-downloaded by the new client resumes fine under the old one, because both resume from the end of the file.
Blast Radius: every resumable pull
fread, which allocates the full size, over the 128Mmemory_limit. Production needs to be on the C# image.Test plan
LibChorus.TestsHgResume tests pass on net8.0 (71/71 before stacking on Update SIL.*Tasks to 4.0.0 and fix restore against libpalaso 18.0.0 #404; the key 23 re-run after)LibChorus.IntegrationTests(large clone, connection cut four times) pass in CI against the hgresume image🤖 Generated with Claude Code
This change is