fix(storage): size the storage expectation from measured corpus - #561
Merged
Conversation
The published storage expectation sat ~16% below what nodes actually
hold. Across all 35 file-backed mainnet nodes the ratio of disk used to
expectation ran 1.07-1.34x, and none was under 1.0.
The disk was never the mystery. On every node with Postgres on the blob
volume, `mediorumPathUsed` reconciles to within 0.3% of
lastSuccessfulRepair.ContentSize + databaseSize -- repair's own tally of
the blobs it decided to keep. The expectation was simply modelling the
wrong corpus.
(SUM(ff_probe.size) * 2 * replication_factor) / node_count
The `* 2` stood in for "we store a transcode as well as the original",
and was wrong in both directions at once:
- It over-counted transcodes. Most originals are lossless or
high-bitrate and downsample well below their own size; measured
across mainnet, transcodes are 0.66 TB against 1.06 TB of originals,
a ratio of 0.63, not 1.0. It also doubled image uploads, which have
no transcode at all.
- It omitted more than it over-counted. Repair iterates uploads,
audio_previews and qm_cids; only the first was in the numerator.
Now sized per artifact, each figure the consequence of a decision made
elsewhere in the package rather than a fitted parameter:
originals exact, from ffprobe
+ audio_duration * 40_000 320 kbps CBR (transcode.go)
+ previews * 1_200_000 30 s at that bitrate
+ legacy Qm corpus
all * replication_factor / node_count
The legacy term matters most and was the hardest to get: those blobs are
tracked only as keys in qm_cids with no size recorded anywhere in
Postgres, so no query over `uploads` can see them -- yet they are about a
third of what a node stores. Measured by sampling 20,000 of the 2,445,544
addressable keys against a storeAll node's /internal/blobs/info at a
99.9% hit rate: 14.654 MB mean over 1,045,437 bare CIDs, 0.685 MB over
1,400,107 .../original.jpg. Resized variants are excluded because repair
deletes them on sight (0 of 1,221 sampled existed anywhere).
It is derived from qm_cids row counts rather than hardcoded as a total so
that dev and stage, which have no legacy content, correctly get zero
instead of inheriting all of mainnet's history. The counts are stable --
qm_cids is filled once by a migration and never appended to -- so it is
computed once and kept.
Result: 2.60 TB/node against 2.571 TB measured on content-8, +1.2%,
versus -16.3% today. Independent cross-check: this puts legacy at 33.0%
of the corpus, where a filesystem walk of a different node's blob tree
measured 34.2%.
Still a fair-share model. It describes a node holding its rendezvous
share and says nothing about StoreAll, StoreRecent or archive tiers,
whose footprint is set by retention config instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
The gap
Storage expectation reads ~16% below what nodes actually hold. Across all 35 file-backed mainnet nodes, disk-used ÷ expectation runs 1.07–1.34×, and not one is below 1.0.
The disk was never the mystery. On every node with Postgres on the blob volume,
mediorumPathUsedreconciles to within 0.3% oflastSuccessfulRepair.ContentSize + databaseSize— repair's own tally of blobs it decided to keep. e.g. content-8: 2,689,227,333,632 used vs 2,570,710,940,019 + 125,780,483,431 = 2,696,491,423,450. The expectation was modelling the wrong corpus.What was wrong
The
* 2stood in for "we store a transcode as well as the original", and was wrong in both directions at once:uploads,audio_previewsandqm_cids(repair.go:331, :379, :442). Only the first was in the numerator.What it does now
Each constant is the consequence of a decision made elsewhere in the package, not a fitted parameter.
The legacy corpus
The term that matters most, and the hardest to get. Those blobs are tracked only as keys in
qm_cids, with no size recorded anywhere in Postgres — invisible to any query overuploads, despite being roughly a third of what a node stores.Measured by sampling 20,000 of the 2,445,544 addressable keys against a
storeAllnode's/internal/blobs/info, at a 99.9% hit rate on both classes:…/original.jpg(artwork)Variants are excluded because repair deletes them on sight — they're regenerated on demand (repair.go:708) — and 0 of 1,221 sampled existed anywhere.
It's derived from
qm_cidsrow counts rather than hardcoded as a total, so dev and stage correctly get zero instead of inheriting all of mainnet's history. The counts are stable (qm_cidsis filled once by drop_blobs.sql and never appended to), so it's computed once and kept.Result
2.60 TB/node against content-8's measured 2.571 TB — +1.2%, versus −16.3% today. It also sits inside the 2.278–2.628 TB range measured across all file-backed nodes.
Independent cross-check: this puts legacy at 33.0% of the corpus. A filesystem walk of a different node's blob tree measured legacy at 1052.5 GB of 3079.1 GB = 34.2%. Two unrelated instruments — HTTP sampling on one operator's node,
duon another's — agreeing to 1.2 percentage points.Scope
Still a fair-share model. It describes a node holding its rendezvous share and says nothing about
StoreAll,StoreRecentor archive tiers, whose footprint is set by retention config. For a StoreAll node the right shape iscorpus × rf/nfor primary andcorpus × (1 − rf/n)for archive — worth a follow-up, and it needs the per-bucket content split thatContentSizecurrently conflates.The legacy constant can only shrink (prune and declared data loss remove blobs; nothing adds them), so it will drift slowly and is worth re-measuring periodically.
Tests
computeStorageExpectationis split from the SQL so the arithmetic is testable without a database: rendezvous share, degenerate inputs (zero nodes / zero rf / empty network), each component counted exactly once, images not doubled, and a case pinning the real mainnet numbers to within 5% of measured while asserting the old formula is demonstrably >10% low.go build,go vetandgofmtclean. The package has three failures —TestPollDelistStatuses,TestRepair,TestUploadFile— which reproduce identically onmain; they need the Docker harness (make test-mediorum). No new failures.🤖 Generated with Claude Code