fix(registry-client): clear blob blocks on cancelled/failed downloads - #3219
Conversation
Review StatusCurrent Status: ✅ APPROVED |
ebf7c82 to
70c2ace
Compare
There was a problem hiding this comment.
thanks @a-mium! This fixes the exact leak from #3218 — catch paths clear via _clearBlobBlocks, and the regression tests lock that in. ran the unit file locally (9/9). blocking gap: catch doesn't destroy the outer rangeDownload before clear (success path does). harness against hypercore@11 showed mid-flight clear without destroy can refill and leave durable blocks after close; destroy-then-clear-then-close left 0. same treatment needed in downloadBlob.
🔴 requesting changes
978fd28 to
244987b
Compare
|
Force-pushed: all four review comments addressed, one commit each. Tests now run in-repo - 33/33 unit tests, 127/127 asserts, lint clean. Two commits go beyond the review comments, flagging them so they are not a surprise:
|
244987b to
891be76
Compare
|
thanks @a-mium, updates lgtm. Kindly resolve the merge conflicts |
891be76 to
23762d8
Compare
done |
There was a problem hiding this comment.
Pull request overview
This PR fixes a disk-growth leak in @qvac/registry-client where partially downloaded hypercore blocks could remain live in the local corestore when a download is cancelled, a stream is destroyed, or a download fails. It centralizes teardown so that block-range replication is stopped and cached blocks are cleared on both success and failure paths, matching the intent of Issue #3218.
Changes:
- Hoists block-range state (
blockStart/blockEnd/rangeDownload) so thecatchpath can release resources and clear cached blocks consistently. - Introduces shared release helpers (
_releaseDownload,_releaseOnStreamEnd) to stop replication, clear blocks (when applicable), and close resources, including handling streamclose(destroyed stream) in addition toend. - Adds unit regression tests covering failure-path cleanup, stream-destroy cleanup, single-release guarding, and destroy-before-clear ordering.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| packages/registry-server/client/lib/client.js | Ensures cached block ranges are released on failure/cancel paths and on stream close, consolidating cleanup into shared helpers to prevent corestore block leaks. |
| packages/registry-server/client/tests/unit/client.download-retry.test.js | Adds regression tests that assert the new cleanup orchestration (clear-on-catch, clear-on-stream-close, once-guard, and destroy-before-clear ordering). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
I independently checked the current head ( Local verification from
Because |
yuranich
left a comment
There was a problem hiding this comment.
🤖: Approved. This cleanup stops the active range and is a meaningful improvement. A real Corestore test still found that in-flight blocks can land after destroy and before clear; follow-up #3496 drains those requests before cleanup.
Aborted/failed downloads left their partially-downloaded hypercore blocks live in the corestore; only success paths cleared them. Clear in both downloadModel/downloadBlob catch blocks so cancelled loads stop leaking disk.
Makes it reachable from the catch in downloadModel/downloadBlob.
…ailure A mid-flight clear can be refilled by ongoing replication and leave durable blocks after close. Matches the success-path order, with tests for it.
…seDownload The destroy/clear/close sequence was copied across six sites in downloadModel and downloadBlob. Close errors are now warned everywhere, so a close hiccup after a fully written file no longer rejects the download.
A consumer destroying the returned stream emits 'close' without 'end', so the blocks leaked on exactly the cancel path this PR targets.
…ests Forwards the client logger into TAP output so the run shows which production branch executed, plus timing of the exercised path.
f7da42d to
4297de6
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
packages/registry-server/client/lib/client.js:513
- _releaseOnStreamEnd only triggers cleanup on 'end' and 'close'. If the hyperblobs read stream emits an 'error' without subsequently emitting 'close' (which can happen for some Readable implementations), the blob/core handles and cached block range may never be released on stream consumers that don't explicitly destroy the stream. Adding an 'error' listener here makes the release behavior robust for the failure path this PR is targeting.
stream.once('end', release)
stream.once('close', release)
|
@yuranich @opaninakuffo kindly help to merge, I see there is github error: |
Fixes #3218
What problem does this PR solve?
@qvac/registry-clientleaks hypercore blocks into the local corestore when a download is cancelled or fails.downloadModel/downloadBlobonly cleared the blocks they pulled into the RocksDB-backed corestore on the success path; thecatch (error)path closed the core and re-threw without clearing. The leaked blocks are logically live (not tombstoned), so RocksDB compaction never reclaims them and the corestore grows without bound across distinct cancelled loads. The assembled output file is cleaned up by a separateclearCachepath, so only the corestore blocks leak and the visible models cache stays empty while the on-diskdbdir keeps growing.Before this change, success paths cleared via
_clearBlobBlocks(core.clear(start, end, { diff: true })+core.compact()) in bothdownloadModelanddownloadBlob, while thecatchblocks did not.Empirical measurement against a running service on the downstream
@qvac/sdk:dbdir — monotonic — while the models cache dir stayed empty.How does it solve it?
catch (error)of bothdownloadModelanddownloadBlob, clear the pulled block range before closing the core, guarded byif (core && blockStart != null)so a failure before the range was computed is a no-op. This mirrors the existing success-path clearing.blockStart/blockEnd/rangeDownloaddeclarations out of thetry(they wereconstinside it and thus out of scope in thecatch); they are nowlet-declared alongsidecore/blobsand assigned in place._releaseDownload. Close errors are now warned uniformly, so a close failure after a fully written file no longer rejects a download that actually succeeded.outputFilepath: cleanup was bound to streamendonly, so a consumer destroying the returned stream (which emitsclosewithoutend) never freed its blocks — the same cancel scenario this PR targets. Release now runs onendorclose, behind a once-guard.tests/unit/client.download-retry.test.js) covering the catch path, the stream-close path, the release guards when no core or block range exists yet, and the destroy-before-clear ordering.Note on verification:
npm run test:unitandnpm run lintboth pass in the package (33/33 tests, 127/127 asserts). The new tests were confirmed to discriminate, not just pass: reverting the destroy-before-clear fails the ordering assertions, binding onlyendfails the two stream-close tests, and removing the once-guard fails the single-release test withactual: 2, expected: 1. The tests forward the client logger into TAP output, so the run shows which production branch actually executed on each path.Scope note:
_clearBlobBlocksis stubbed in the unit tests, so they prove the orchestration — that clearing happens, once, in the right order, with the right block range — not thatcore.clear()/compact()reclaims disk. The measurements above remain the evidence for that.Breaking changes
None.