fix: decouple client download from merge; stop deleting parts under in-flight readers#243
Closed
peter-svensson wants to merge 1 commit into
Closed
Conversation
…r readers Two defects in Storage.download() for unmerged entries: 1. The first download of an unmerged entry tees the parts into both the client response and the /merged re-upload through one backpressured pipe (pumpPartsToStreams). Because the merge upload runs at queueSize:1, partSize:5MB, the client is throttled to a serial 5MB PUT loop -- a ~700MB restore stalls at ~124MiB and crawls at ~3-4 MB/s (observed ~3m40s). 2. On merge completion the parts folder is deleted inline. Concurrent downloads still streaming those parts then hit ObjectNotFound (500), which clients treat as a miss -> re-upload churn and entries that never stay merged. Fix: serve the client directly from streamParts() at full speed and run the merge as an independent background stream (its own part reads), so the client is never coupled to the merge. Claim the merge atomically (UPDATE ... WHERE mergeStartedAt IS NULL AND mergedAt IS NULL) so concurrent/replicated downloads don't start duplicate merges. Stop deleting parts inline -- the existing hourly cleanup:parts task already removes parts of merged entries, so in-flight readers are never cut off. Adds tests/concurrent-download.test.ts (fails on the inline-delete code, passes after). Related to #235 (which addresses defect 2 via a parts-delete grace period but leaves the defect-1 download throttling).
Author
|
Re-opening from a personal fork; closing this one. |
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.
Problem
Two issues in
Storage.download()for unmerged entries (large caches that haven't been merged yet):1. Client download is throttled to the merge upload (slow restores)
The first download of an unmerged entry feeds parts into both the client response and the
/mergedre-upload through one backpressured pipe (pumpPartsToStreams—await once(mergerStream, 'drain')). The merge upload runs atqueueSize: 1, partSize: 5MB(serial 5MB PUTs), so the client is capped to that rate.Observed on a ~705 MB cache (S3 driver): the restore streams the first ~124 MiB fast (~29 MB/s), then stalls and crawls in ~128 MiB steps with ~30 s pauses — ~3m40s total at ~3–4 MB/s:
2. Inline parts deletion races in-flight readers (500s / corruption)
On merge completion the code deletes
{folder}/partsinline. Concurrent downloads still streaming those parts then hitObjectNotFound→ 500 (ERR_HTTP_HEADERS_SENTafter the response started), which clients treat as a miss → re-upload churn, and entries that never stay merged keep falling back to the slow path forever.Fix
streamParts()at full speed; run the merge as an independent background stream (its own part reads). The client is never coupled to the merge upload.UPDATE storage_locations SET mergeStartedAt=now WHERE id=? AND mergeStartedAt IS NULL AND mergedAt IS NULL) so concurrent / multi-replica downloads don't start duplicate merges.cleanup:partstask already removes parts of merged entries, so in-flight readers are never cut off.pumpPartsToStreamsanddownloadFromCacheEntryLocation(no longer needed).Net: first read of an unmerged entry streams at full speed; once merged, reads use the presigned direct-download path as before. Restore of the same ~705 MB cache dropped from ~3m40s to ~20s in our environment.
Tests
Adds
tests/concurrent-download.test.ts: builds an unmerged multi-part entry, fires concurrent downloads, asserts every reader gets the full payload and that parts are not deleted inline (the merge keeps them forcleanup:parts). Fails on the current inline-delete code, passes after. Existingstale-cache+e2esuites pass (sqlite+filesystem and via the server e2e).Relation to #235
#235 addresses defect 2 (parts-delete race) via a 15-min grace period + per-part existence check — complementary, and its grace approach could be combined. This PR additionally fixes defect 1 (the download-throttled-to-merge stall), which #235 does not touch. Happy to align with #235's approach if preferred.