Summary
@qvac/registry-client leaks hypercore blocks into the local corestore whenever a download is cancelled or fails. The blocks a download pulls into the RocksDB-backed corestore are only ever cleared on the success path; the catch (error) path closes the core and re-throws without clearing them. Because the leaked blocks are logically live (not tombstoned), RocksDB compaction never reclaims them, so the corestore grows without bound across distinct cancelled model loads.
Root cause
File: packages/registry-server/client/lib/client.js
Both downloadModel and downloadBlob download a hypercore block range into the corestore, stream it out, and on success call _clearBlobBlocks(core, blockStart, blockEnd) — which does core.clear(start, end, { diff: true }) + core.compact() — to free the cached blocks.
_clearBlobBlocks is only reached on success paths:
downloadModel: the outputFile branch (lib/client.js:361) and the stream cleanup on 'end' (lib/client.js:373).
downloadBlob: the outputFile branch (lib/client.js:495) and the stream cleanup on 'end' (lib/client.js:507).
The asymmetry is in the error paths. Both catch (error) blocks close blobs and core and re-throw, but never call _clearBlobBlocks:
downloadModel catch: lib/client.js:400-419 — closes blobs/core, throw error, no clear.
downloadBlob catch: lib/client.js:527-542 — closes blobs/core, throw error, no clear.
So when a load is cancelled mid-download (the abort signal in _streamBlobToFile rejects with Download cancelled, lib/client.js:620/626) or fails after exhausting retries, the partially-downloaded blocks are left live in the corestore forever.
Note: the block range (blockStart/blockEnd) is currently computed with const inside the try (lib/client.js:340-341 and 471-472), so it is not even in scope in the catch — the declarations need hoisting for the catch to clear the right range.
Impact
Unbounded corestore disk growth across distinct cancelled/failed model loads. The assembled output file in the models dir is cleaned up correctly by a separate clearCache path — only the corestore blocks leak, so it is easy to miss (the visible models cache stays empty while the on-disk db dir keeps growing).
Empirical measurements
Measured 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 block range, which churns and later compacts. This is consistent with the root cause — the leak is per distinct block range that is never cleared.
Fix
In the catch (error) of both downloadModel and downloadBlob, clear the partial block range before closing the core (guarded for the case where the range was not yet computed), symmetric with the success paths. PR to follow.
Summary
@qvac/registry-clientleaks hypercore blocks into the local corestore whenever a download is cancelled or fails. The blocks a download pulls into the RocksDB-backed corestore are only ever cleared on the success path; thecatch (error)path closes the core and re-throws without clearing them. Because the leaked blocks are logically live (not tombstoned), RocksDB compaction never reclaims them, so the corestore grows without bound across distinct cancelled model loads.Root cause
File:
packages/registry-server/client/lib/client.jsBoth
downloadModelanddownloadBlobdownload a hypercore block range into the corestore, stream it out, and on success call_clearBlobBlocks(core, blockStart, blockEnd)— which doescore.clear(start, end, { diff: true })+core.compact()— to free the cached blocks._clearBlobBlocksis only reached on success paths:downloadModel: theoutputFilebranch (lib/client.js:361) and the streamcleanupon'end'(lib/client.js:373).downloadBlob: theoutputFilebranch (lib/client.js:495) and the streamcleanupon'end'(lib/client.js:507).The asymmetry is in the error paths. Both
catch (error)blocks closeblobsandcoreand re-throw, but never call_clearBlobBlocks:downloadModelcatch:lib/client.js:400-419— closesblobs/core,throw error, no clear.downloadBlobcatch:lib/client.js:527-542— closesblobs/core,throw error, no clear.So when a load is cancelled mid-download (the abort signal in
_streamBlobToFilerejects withDownload cancelled,lib/client.js:620/626) or fails after exhausting retries, the partially-downloaded blocks are left live in the corestore forever.Note: the block range (
blockStart/blockEnd) is currently computed withconstinside thetry(lib/client.js:340-341and471-472), so it is not even in scope in thecatch— the declarations need hoisting for the catch to clear the right range.Impact
Unbounded corestore disk growth across distinct cancelled/failed model loads. The assembled output file in the models dir is cleaned up correctly by a separate
clearCachepath — only the corestore blocks leak, so it is easy to miss (the visible models cache stays empty while the on-diskdbdir keeps growing).Empirical measurements
Measured against a running service on the downstream
@qvac/sdk:dbdir — monotonic — while the models cache dir stayed empty.Fix
In the
catch (error)of bothdownloadModelanddownloadBlob, clear the partial block range before closing the core (guarded for the case where the range was not yet computed), symmetric with the success paths. PR to follow.