[POC] Bucket storage report (operations vs rows) - #683
Conversation
🦋 Changeset detectedLatest commit: 082868c The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
rkistner
left a comment
There was a problem hiding this comment.
I had a quick check on $sampleRate performance, and unfortunately it's not a magic bullet in terms of performance. Essentially MongoDB still has to scan through the index at least before applying the $sampleRate. I can work, as long as we ensure:
- Limit the number of index entries a query is scanning to around 100k-1M at most.
- Make sure no document lookup is performed before filtering using
$sampleRate. E.g. if we scan through 100k index entries to sample around 1000 of them, the query must scan through 1000 documents, not 100k documents (fetching documents is much slower than the index entries). Use MongoDB's explain to confirm this.
As an additional safeguard, we can use readPreference: 'secondaryOnly' for these queries, to make sure they don't affect performance on the primary node.
|
|
||
| const pipeline: mongo.Document[] = [{ $match: match }]; | ||
| if (sampled) { | ||
| pipeline.push({ $sample: { size: BUCKET_SELECTION_SAMPLE_SIZE } }); |
There was a problem hiding this comment.
$sample does not help for performance unless it's the first stage in the pipeline.
Potential options:
$samplefirst, then filter. That would require the initial sample size to be higher than the limit we want.- Filter first, then use
$sampleRate. I'm not actually what the performance is like for$sampleRate- would need some testing.
And if you go for option 2, node that current _id.b / _id.g filters aren't efficient either, and require a full collection scan. Do filter efficiently, you need to use a pattern such as _id: {$gte: ..., $lt: ...} - there should be a couple of examples like that in this repo you can use as a starting point.
There was a problem hiding this comment.
The bucket report does not read bucket_data anymore.
The only sampling left is for picking the top buckets in bucket_state (when above 50k buckets) and it uses option 2
|
@bean1352 The MongoDB V3 storage had a significant change now in what we store in I believe that should allow using only the More specifically:
This would still leave cases with V1 storage. I'd recommend focusing on V3, and give more limited stats for V1, rather than attempting to do a more expensive scan for V1. That would further encourage moving users over to V3. |
The bucket report only reads The row count of a bucket comes from
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 631f5e022a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // every matched document only to discard most of them. | ||
| pipeline.push( | ||
| { $project: { _id: 1 } }, | ||
| { $match: { $sampleRate: sampleRate } }, |
There was a problem hiding this comment.
Avoid sampling away the worst offenders
When an active configuration has more than 50,000 buckets, this uniform sample is applied before the operation-count sort. At 50,001 buckets, a single pathological bucket has roughly an 80% chance of being omitted, causing both the returned “worst offenders” and the scaled operation total to miss precisely the outlier this endpoint is intended to diagnose. The totals.estimated flag does not make the randomly truncated top-bucket list actionable; determine the top buckets from the full matched set, even if sampling remains necessary for aggregate totals.
Useful? React with 👍 / 👎.
| return await this.aggregateTopBuckets( | ||
| this.db.bucketStateV1, | ||
| { _id: idPrefixFilter<{ g: number; b: string }>({ g: this.replicationStreamId }, ['b']) }, |
There was a problem hiding this comment.
Include legacy v1/v2 buckets in the report
On upgraded storage-version 1/2 instances, buckets created before bucket_state tracking can be absent from this collection indefinitely: BucketStateDocumentBase explicitly notes at models.ts:138-139 that no migration populated existing data and only new updates create state. Querying only bucketStateV1 therefore omits untouched legacy buckets and their operations while returning estimated: false, contradicting the endpoint's exact-count contract for supported v1/v2 storage; use the bucket-data source or backfill/flag incomplete results.
AGENTS.md reference: AGENTS.md:L70-L77
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 082868cf17
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| { $sort: { operations: -1 } }, | ||
| { $limit: limit } |
There was a problem hiding this comment.
Apply the fragmentation tie-break before limiting buckets
When more than limit buckets share the operation count at the cutoff, this database sort selects an arbitrary subset because it considers only operations; assembleBucketReport() applies the documented fragmentation tie-break only after the subset has already been limited. Consequently, a more fragmented tied bucket can be omitted from the worst-offender list. Include the fragmentation tie-break in the aggregation before $limit, or avoid promising that secondary ranking.
Useful? React with 👍 / 👎.
Bucket storage report
What this adds
A new admin endpoint,
POST /api/admin/v1/bucket-report. It shows how much operation history each bucket carries compared to its actual rows.How it works
The report only reads
bucket_state. It never scans the operation history.operations,operation_bytes: kept up to date by writers and compactors. Exact.rows: the row count captured by the bucket's last full compact.nullif the bucket has never been fully compacted.fragmentation:operations / rows. Near 1 is healthy. 167 means new clients download 167 operations per live row.uncompacted_operations: operations written after that compact, which is how stalerowsmight be.last_full_compact_at,next_compact_at: when the row stats were captured, and when the scheduler next considers the bucket. A suggested compact with a future date is already planned, just throttled until then.suggested_action:compact,defragment,both,none, orunknownwhen there are no compact statistics to reason from.Worst buckets come first. There is also a rollup per definition and totals for the instance.
limitdefaults to 50 and caps at 1000; invalid values get a 400 error instead of being clamped.Example
Response (from a real test run). This bucket was fully compacted at 07:35, then 15 more operations were written to it, so
rowsis a slightly stale snapshot and the report says so:{ "buckets": [ { "bucket": "by_user.1.3[\"u1\"]", "operations": 48, "operation_bytes": 9022, "uncompacted_operations": 15, "rows": 5, "fragmentation": 9.6, "last_full_compact_at": "2026-08-26T07:35:41.325Z", "next_compact_at": "2026-08-26T07:41:34.066Z", "suggested_action": "both" } ], "definitions": [ { "definition": "by_user.1.3", "bucket_count": 4, "operations": 192, "operation_bytes": 36124, "uncompacted_operations": 60, "rows": 20, "fragmentation": 9.6, "suggested_action": "both" } ], "totals": { "bucket_count": 11, "operations": 281, "operation_bytes": 62302, "estimated": false }, "buckets_truncated": false, "definitions_truncated": false }Scaling
Up to 50k buckets the scan is exact. Above that the report ranks a sample of about 10k buckets, using only the
_idindex, and marks the totalsestimated: true. Past 1M buckets it fails fast instead of scanning without bound.Storage v1/v2 do not record compact statistics, so they report operation counts only: rows, fragmentation and the dates are
nulland the action isunknown. That keeps v1 cheap and nudges users towards v3.AI disclaimer
I developed this change using Claude, and reviewed and tested it myself.