Repository navigation
Conversation
…lickHouse#3508) getPreviousAlertHistories grouped on a compound _id and filtered state in $match, so MongoDB fetched every history row of the 7-day lookback for each alert instead of the newest row per group. For a 1-minute alert with ~1,300 groups that is ~1.9M rows a day, and the check-alerts run slowed down (and ran out of memory on catch-up) as history grew. Group on _id: '$group' with only index fields in $match, which MongoDB answers with a DISTINCT_SCAN on {alert, group, createdAt}, and look up the newest non-ERROR row separately for a group whose newest row is ERROR. The returned map is unchanged.
🦋 Changeset detectedLatest commit: 6ccc760 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 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 |
|
@bcanvural is attempting to deploy a commit to the HyperDX Team on Vercel. A member of the Team first needs to authorize it. |
|
Hi @bcanvural, thanks for the pull request! Before we review code from a first-time contributor, a maintainer vouches for them, and you're not on our list yet. There's nothing else to file and this PR stays open — a maintainer will look at it, usually within a day or two, and vouch for you right here if it's a genuine attempt at the problem. To make that quick, check the PR description has:
More detail in our contributing guide. |
PR Review
1 finding(s): 🔴 0 critical · 🟠 0 major · 🔵 1 minor 1 posted as inline comment(s) on the changed lines. Severity is the reviewer's own estimate and is used for ordering, not filtering. No finding blocks merge automatically; a maintainer decides. |
Deep Review✅ No critical issues found. The correctness and performance reviewers independently traced the rewritten aggregation and the startup index-drop and confirmed the output map shape is preserved, the DISTINCT_SCAN is real on the CI engine (MongoDB 5.0.32), and the migration is replica-safe and idempotent. The remaining items are test-coverage and maintainability recommendations. 🟡 P2 — recommended
🔵 P3 nitpicks (6)
Reviewers (11): correctness, performance, data-migrations, reliability, kieran-typescript, testing, maintainability, project-standards, previous-comments, learnings-researcher, adversarial (timed out — no result). Testing gaps:
No finding blocks merge automatically. A maintainer will expect P0/P1 findings in code this PR changes to be fixed; P2/P3 are your call -- fix or reply. Never fix findings about surrounding code here; reply instead. Do not widen the PR. How to respond |
|
I'd really appreciate being vouched for btw! I'm trying to make metrics work with our 'unified observability' project, where we collect logs/metrics/traces in a single datastore. Opening issues as I see issues around dashboards containing metrics. Mostly using mcp to configure hyperdx, so also testing that path along the way. Cheers! |
Address review: the follow-up lookups ran via Promise.all, outside the concurrency limit. Run them one at a time in the slot the alert already holds (queuing them on the same queue could deadlock when it's full). The plan test now checks only the docs-examined bound, not the plan name.
|
Thanks for the review. On the three P3s:
All 383 checkAlerts and alerts API integration tests pass. |
|
Thanks. That's two review rounds, so per AGENTS.md I'll stop changing code here and leave the rest for a maintainer.
|
|
I don't really like the idea of introducing multiple queries here, I'd rather one query fulfill the request. Is it possible to do all in one query and address the identified issue? Potentially by changing the index? |
…xed index
Add state as the last field of the {alert, group, createdAt} index, so the
ERROR exclusion becomes an index bound and the single aggregation keeps a
DISTINCT_SCAN. This replaces the per-group ERROR follow-up lookups.
|
Thanks, good point. It can be one query if The change: With
An ungrouped alert whose newest 50 rows are ERROR: 51 keys, 1 doc. At a larger scale (MongoDB 5.0.32): 1,500 groups × 300 one-minute evaluations, plus a group-less ERROR row every minute (450,300 rows):
Over the full 7-day window, main grows with groups × evaluations: about 15 million docs for this alert. The new query stays at one doc per group, plus one key per ERROR row in the window. The remaining keys are the ERROR rows newer than a group's newest non-ERROR row. They are read as keys only, and there is at most one per failed window, because ERROR rows are upserted per window. Other notes:
If this looks good, I'll push it to this PR. |
| // `state` comes last so the ERROR exclusion is an index bound and the query | ||
| // keeps a DISTINCT_SCAN. Replaces {alert: 1, group: 1, createdAt: -1}, which is | ||
| // a prefix of this one. | ||
| AlertHistorySchema.index({ alert: 1, group: 1, createdAt: -1, state: 1 }); |
There was a problem hiding this comment.
🟠 major — Old {alert, group, createdAt} index is never dropped, so the busiest collection keeps both indexes on every deployment
Mongoose autoIndex (run by the API; the worker passes autoIndex: false in packages/api/src/tasks/checkAlerts/providers/default.ts:417) only creates indexes and never drops them. Every existing deployment will build {alert:1, group:1, createdAt:-1, state:1} and also keep alert_1_group_1_createdAt_-1. The comment says the new index replaces the old one, but nothing removes it. Nothing reads the old index any more, yet every AlertHistory insert still maintains it, on a collection that this PR's own description puts at about 1.9M rows a day for one alert. Add a migrate-mongo migration under packages/api/migrations/mongo/ that drops alert_1_group_1_createdAt_-1 once the new index exists. Also correct the PR description and changeset: they still say "No index or schema change" and describe ERROR follow-up lookups this diff no longer has. Operators need to know that an index build on a large collection happens at API startup.
Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond
There was a problem hiding this comment.
Fixed in 6ccc760: the API now drops alert_1_group_1_createdAt_-1 at startup, in runStartupMigrations, once Mongoose has built the new index (kept if the new one is missing). I used the startup migration rather than migrations/mongo/, because migrate-mongo runs only in dev (make dev-migrate-db against localhost) and no deployment would run it. The changeset and PR description are updated too.
Mongoose creates indexes but never drops them, so every deployment kept
{alert, group, createdAt} next to {alert, group, createdAt, state} and paid
for both on every insert. runStartupMigrations now drops the old index once
the new one exists. Also update the changeset.
|
|
||
| export async function dropReplacedAlertHistoryIndex() { | ||
| // Resolves when Mongoose's index builds finish (autoIndex). | ||
| await AlertHistory.init(); |
There was a problem hiding this comment.
🔵 minor — AlertHistory.init() makes API startup wait for the whole new index build
Server.start() awaits runStartupMigrations() at packages/api/src/server.ts:103, and await AlertHistory.init() doesn't resolve until Mongoose finishes building {alert, group, createdAt, state}. The changeset says that build "takes a while" on a large collection. Until it finishes, everything after line 103 waits: setupTeamDefaults in local app mode, plus any startup migration added later. If the build fails, init() rejects and only an error is logged. Fix: don't await this step in the startup path (void dropReplacedAlertHistoryIndex().catch(log)), or run it after the other startup work.
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
|
Update: I pushed this right away rather than waiting, sorry for the confusion in my last comment. The PR now has:
The description is updated. The remaining bot items are minor or P2; happy to change any of them you want. @knudtty, ready for another look when you have time. |
|
@knudtty, this is now a single query as you suggested: |
|
@knudtty, a gentle ping on this one when you have a moment. It's the single-query version you suggested, and it still merges cleanly. Happy to update the branch or change anything else. |
Summary
Fixes #3508.
getPreviousAlertHistorieslooks up each group's newest AlertHistory row before every alert check. The{alert, group, createdAt}index is meant to make that one seek per group, but MongoDB fetched every row of the 7-day lookback for the alert. A 1-minute alert with ~1,300 groups writes ~1.9 M rows a day, so its check got slower every day. On our deployment that reached 4 minutes per run and an out-of-memory crash loop on catch-up.Two details of the pipeline prevented the per-group plan (
DISTINCT_SCAN): the compound$groupkey{alert, group}, and thestate: {$ne: ERROR}predicate, which the old index could not answer.This PR:
stateas the last field of the index:{alert: 1, group: 1, createdAt: -1, state: 1}. The ERROR exclusion becomes an index bound, so the planner keeps theDISTINCT_SCANand steps over ERROR keys without fetching those documents;state: {$ne: 'ERROR'}in$match. The only change is the$groupkey:_id: '$group'instead of{alert, group}, because a compound_idrules out theDISTINCT_SCAN;group || '') and uses only operators DocumentDB supports; callers are unchanged;{alert, group, createdAt}index at API startup (dropReplacedAlertHistoryIndexinrunStartupMigrations), after Mongoose has built the new one. Mongoose creates indexes but never drops them.explain()on 20 groups × 200 rows plus 200 group-less ERROR rows, the same on MongoDB 5.0.32 (CI) and 8.0.34:mainAt a larger scale (MongoDB 5.0.32): 1,500 groups × 300 one-minute evaluations, plus a group-less ERROR row every minute (450,300 rows):
mainexamines 450,300 docs in ~700 ms; this PR examines 1,500 docs and 1,800 keys in 10–18 ms. Over the full 7-day windowmaingrows with groups × evaluations (about 15 million docs for this alert); this PR stays at one doc per group, plus one key per ERROR row in the window.Index change (for operators)
{alert: 1, group: 1, createdAt: -1, state: 1}at startup (MongooseautoIndex), like the index added in perf: optimize AlertHistory queries (HDX-3706) #1928. On a large AlertHistory collection this takes a while. The build holds an exclusive lock only briefly at the start and the end, and the 30-day TTL bounds the collection's size. The API serves requests during the build.alert_1_group_1_createdAt_-1. Until then, checks keep using it. If the new index is missing (for example, its build failed), the old index is kept.autoIndex: falseand doesn't touch indexes; the API owns them.How to reproduce
Starting point:
make dev-int-style services (MongoDB and ClickHouse fromdocker-compose.ci.yml); no UI involved.On
main:state: 'OK', plus a group-less ERROR row every minute.explain('executionStats')on thegetPreviousAlertHistoriespipeline:FETCH > IXSCANwithtotalDocsExamined= 450,300.On this branch:
DISTINCT_SCAN,totalDocsExamined= 1,500.How to test on Vercel preview
N/A — non-UI change.
Tests
getPreviousAlertHistoriesreturns the newest non-ERROR row per group, with ERROR rows with and without a group, a group that stopped reporting while in ALERT, rows without a group (OK, then a newer ERROR), and a group with only ERROR rows. It passes onmaintoo: same results.main's index (4,200 docs examined vs ≤ 21).dropReplacedAlertHistoryIndexdrops the old index once the new one exists, is idempotent, and keeps the old index while the new one is missing.References
getPreviousAlertHistoriesreads every history row of the last 7 days, so check-alerts slows down and runs out of memory as history grows #3508Written with help from an AI assistant (Claude Code). The numbers come from the integration test setup on
mainand on this branch.