Skip to content

fix(registry-client): clear blob blocks on cancelled/failed downloads - #3219

Merged
opaninakuffo merged 11 commits into
tetherto:mainfrom
a-mium:fix/clear-blob-blocks-on-cancel
Jul 28, 2026
Merged

fix(registry-client): clear blob blocks on cancelled/failed downloads#3219
opaninakuffo merged 11 commits into
tetherto:mainfrom
a-mium:fix/clear-blob-blocks-on-cancel

Conversation

@a-mium

@a-mium a-mium commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #3218

What problem does this PR solve?

@qvac/registry-client leaks hypercore blocks into the local corestore when a download is cancelled or fails. downloadModel/downloadBlob only cleared the blocks they pulled into the RocksDB-backed corestore on the success path; the catch (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 separate clearCache path, so only the corestore blocks leak and the visible models cache stays empty while the on-disk db dir keeps growing.

Before this change, success paths cleared via _clearBlobBlocks (core.clear(start, end, { diff: true }) + core.compact()) in both downloadModel and downloadBlob, while the catch blocks did not.

Empirical measurement against a running service on the downstream @qvac/sdk:

  • Cancelling three distinct models mid-download added +74 MB, +36 MB, +59 MB to the corestore db dir — monotonic — while the models cache dir stayed empty.
  • Cancelling the same model repeatedly did not grow monotonically (it re-fetches the same content-addressed range, which churns and later compacts) — consistent with the root cause: the leak is per distinct block range that is never cleared.

How does it solve it?

  • In the catch (error) of both downloadModel and downloadBlob, clear the pulled block range before closing the core, guarded by if (core && blockStart != null) so a failure before the range was computed is a no-op. This mirrors the existing success-path clearing.
  • Hoisted the blockStart/blockEnd/rangeDownload declarations out of the try (they were const inside it and thus out of scope in the catch); they are now let-declared alongside core/blobs and assigned in place.
  • Destroy the range download before clearing, matching the success-path ordering — a mid-flight clear can otherwise be refilled by ongoing replication and leave durable blocks after close.
  • Collapsed the destroy/clear/close teardown, which was copied across six sites, into a shared _releaseDownload. Close errors are now warned uniformly, so a close failure after a fully written file no longer rejects a download that actually succeeded.
  • Fixed a second leak on the no-outputFile path: cleanup was bound to stream end only, so a consumer destroying the returned stream (which emits close without end) never freed its blocks — the same cancel scenario this PR targets. Release now runs on end or close, behind a once-guard.
  • Added regression tests (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:unit and npm run lint both 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 only end fails the two stream-close tests, and removing the once-guard fails the single-release test with actual: 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: _clearBlobBlocks is stubbed in the unit tests, so they prove the orchestration — that clearing happens, once, in the right order, with the right block range — not that core.clear()/compact() reclaims disk. The measurements above remain the evidence for that.

Breaking changes

None.

@a-mium
a-mium requested review from a team as code owners July 10, 2026 18:20
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Status

Current Status: ✅ APPROVED
Approvals so far: Management: 1, Team Lead: 1

@opaninakuffo opaninakuffo left a comment

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.

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

Comment thread packages/registry-server/client/lib/client.js Outdated
Comment thread packages/registry-server/client/lib/client.js Outdated
Comment thread packages/registry-server/client/lib/client.js Outdated
Comment thread packages/registry-server/client/tests/unit/client.download-retry.test.js Outdated
@a-mium
a-mium force-pushed the fix/clear-blob-blocks-on-cancel branch from 978fd28 to 244987b Compare July 20, 2026 20:15
@a-mium

a-mium commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

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:

  • 54e7c65 collapses the destroy/clear/close teardown, which was copied across six sites, into a shared _releaseDownload. Close errors are now warned uniformly, so a close failure after a fully written file no longer rejects a download that succeeded.
  • 43f050d fixes a leak the original patch missed: the no-outputFile path bound cleanup to stream end only, so a consumer destroying the returned stream (emitting close without end) never freed its blocks - the same cancel scenario this PR targets.

@a-mium
a-mium force-pushed the fix/clear-blob-blocks-on-cancel branch from 244987b to 891be76 Compare July 20, 2026 20:21
@a-mium
a-mium requested review from opaninakuffo and yuranich July 20, 2026 20:21
@opaninakuffo opaninakuffo added the verified Authorize secrets / label-gate in PR workflows label Jul 23, 2026
@opaninakuffo

opaninakuffo commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

thanks @a-mium, updates lgtm. Kindly resolve the merge conflicts

Copilot AI review requested due to automatic review settings July 26, 2026 18:44
@a-mium
a-mium force-pushed the fix/clear-blob-blocks-on-cancel branch from 891be76 to 23762d8 Compare July 26, 2026 18:44
@github-actions github-actions Bot removed the verified Authorize secrets / label-gate in PR workflows label Jul 26, 2026
@a-mium

a-mium commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

thanks @a-mium, updates lgtm. Kindly resolve the merge conflicts

done

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 the catch path 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 stream close (destroyed stream) in addition to end.
  • 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.

@opaninakuffo opaninakuffo added the verified Authorize secrets / label-gate in PR workflows label Jul 27, 2026
Copilot AI review requested due to automatic review settings July 27, 2026 12:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

@github-actions github-actions Bot removed the verified Authorize secrets / label-gate in PR workflows label Jul 27, 2026
@opaninakuffo opaninakuffo added the verified Authorize secrets / label-gate in PR workflows label Jul 27, 2026
@localhost41

Copy link
Copy Markdown
Contributor

I independently checked the current head (f7da42d4). The earlier destroy-before-clear concern is addressed: both failure paths now stop rangeDownload before clearing, stream cancellation is covered through close, and the end/close guard releases only once.

Local verification from packages/registry-server/client:

  • npx brittle tests/unit/client.download-retry.test.js — 14/14 tests, 40/40 assertions passed
  • npm run test:unit — 33/33 tests, 127/127 assertions passed
  • npm run typecheck — passed
  • npm run lint — fails with one error at lib/client.js:520: blockStart != null violates eqeqeq

Because blockStart begins as undefined and is otherwise assigned a numeric offset, blockStart !== undefined preserves the guard while satisfying the project lint rule. I did not find another functional issue in the revised cleanup paths.

@yuranich yuranich left a comment

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.

🤖: 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.

a-mium added 9 commits July 28, 2026 11:15
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.
Copilot AI review requested due to automatic review settings July 28, 2026 08:15
@a-mium
a-mium force-pushed the fix/clear-blob-blocks-on-cancel branch from f7da42d to 4297de6 Compare July 28, 2026 08:15
@github-actions github-actions Bot removed the verified Authorize secrets / label-gate in PR workflows label Jul 28, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)

@a-mium

a-mium commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

@yuranich @opaninakuffo kindly help to merge, I see there is github error: Error: GitHub API error: label strip failed: HTTP 403 (status=403 method=DELETE path=/repos/tetherto/qvac/issues/3219/labels/verified), thank you

@opaninakuffo opaninakuffo added the verified Authorize secrets / label-gate in PR workflows label Jul 28, 2026
Copilot AI review requested due to automatic review settings July 28, 2026 08:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

@github-actions github-actions Bot removed the verified Authorize secrets / label-gate in PR workflows label Jul 28, 2026
@opaninakuffo opaninakuffo added the verified Authorize secrets / label-gate in PR workflows label Jul 28, 2026
Copilot AI review requested due to automatic review settings July 28, 2026 08:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

@github-actions github-actions Bot removed the verified Authorize secrets / label-gate in PR workflows label Jul 28, 2026
@opaninakuffo opaninakuffo added the verified Authorize secrets / label-gate in PR workflows label Jul 28, 2026
@opaninakuffo
opaninakuffo merged commit 7c40bbb into tetherto:main Jul 28, 2026
65 of 72 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-contribution PR or issue from an external contributor verified Authorize secrets / label-gate in PR workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

registry-client: cancelled/failed downloads leak hypercore blocks in corestore (never cleared)

6 participants