fix(alertmanager): stop routing the member resolve write through ctx.db.query (BLO-31035) - #1579
Merged
allyblockcast[bot] merged 2 commits intoSep 1, 2026
Conversation
…db.query (BLO-31035) `resolveAggregateMember` marked an aggregate member resolved with a single `UPDATE ... RETURNING issue_id` issued through `ctx.db.query`. That call path is SELECT-only (`validatePluginRuntimeQuery`), so the host rejected it with "ctx.db.query only allows SELECT statements" on every resolve delivery for an aggregate-tracked fingerprint. `handleWebhook` collects per-alert failures and then throws `AlertDeliveryIncompleteError`, so one rejected statement 502s the whole batch and Alertmanager redelivers it forever. Measured on the live estate before this change: ~4,400 webhook delivery failures per hour against a <=500/h baseline, 206 of these rejections in one 13k-line `paperclip-0` window, and zero alert-origin issues filed in ~26.5h. There is no host call that both writes and returns a column — `ctx.db.execute` reports only a row count — so the statement is split in two. The split is exact, not approximate: both statements are keyed on the members primary key (company_id, aggregate_key, fingerprint), so the read matches at most the single row the write targets; nothing deletes member rows, so it cannot vanish in between; and the write is idempotent via COALESCE, so a concurrent delivery resolving the same member lands on the same terminal state. The test double for `ctx.db.query` accepted any SQL, which is why this shipped green. It now asserts the host's SELECT-only contract over every recorded call, so each existing test in the file covers the bug class rather than just this one call site. Verified by reintroducing the defect: 8 tests fail with the host's exact message. Not fixed here, and deliberately separate: the 19 aggregates wedged in phase 'firing' are a distinct defect. The firing fence is released in a `finally`, so an exception cannot wedge it — only process death between claim and release can, which is what a rollout does. That needs restart-resumable fences, not this.
Author
|
🔗 Paperclip issue: BLO-31035 |
1 similar comment
Author
|
🔗 Paperclip issue: BLO-31035 |
Author
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
…gate-member-resolve-select-only
Author
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 4b7a6c5
Critical Issues (0)
Important Issues (0)
Suggestions (0)
Strengths
- The aggregate-member resolution now uses
ctx.db.queryonly for SELECT statements and routes the mutation throughctx.db.execute. - The membership lookup, fallback behavior, and sibling-resolution checks remain keyed by the aggregate membership identity.
- Tests now enforce the host query contract across contexts, including tests that install custom query implementations.
Recommended Action
- No Critical or Important issues found. This App-authored PR is reviewed as a formal comment.
Author
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: af0c315
Critical Issues (0)
Important Issues (0)
Suggestions (0)
Strengths
- The aggregate-member resolution now uses
ctx.db.queryonly for SELECT statements and routes the mutation throughctx.db.execute. - The membership lookup, fallback behavior, and sibling-resolution checks remain keyed by the aggregate membership identity.
- Tests now enforce the host query contract across contexts, including tests that install custom query implementations.
Recommended Action
- No Critical or Important issues found. This App-authored PR is reviewed as a formal comment.
1 task
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.
Thinking Path
Linked Issues or Issue Description
Bug summary (Paperclip-internal tracker, no GitHub issue):
POST /plugins/paperclip-plugin-alertmanager/webhooks/alertmanagerreturns 502 for every delivery containing a resolved, aggregate-tracked alert.ctx.db.query only allows SELECT statements;handleWebhookcollects the per-alert failure and throwsAlertDeliveryIncompleteError, so the whole batch 502s and Alertmanager redelivers it forever.paperclip-0window;sum(increase(alertmanager_notification_requests_failed_total{integration="webhook"}[1h]))≈ 4,400; 0 alert-origin issues in ~26.5h.What Changed
webhook-handler.ts—resolveAggregateMemberno longer issuesUPDATE … RETURNING issue_idthroughctx.db.query. The membership read is now aSELECTviaquery, and the resolve write is anUPDATEviaexecute.__tests__/worker.test.ts— thectx.db.querytest double previously accepted any SQL. AnafterEachnow asserts the host's SELECT-only contract (validatePluginRuntimeQuery) over every recordeddb.querycall, across every test in the file.__tests__/worker.test.ts— four mock branches that keyed on the oldUPDATE … aggregate_membersSQL now key on the newSELECT, so they keep exercising the membership path instead of silently falling through to "no membership".Verification
job-company-scope.test.ts, fails identically on unmodifiedmaster(pre-existing collection failure, embedded-postgres) — confirmed by a baseline run viagit stashbefore the change, which reported the same1 failed | 7 passed.ctx.db.query only allows SELECT statements, got: UPDATE alertmanager.alertmanager_aggregate_members. A regression test that cannot fail is worthless, so this was checked rather than assumed.alertmanager_aggregate_members_pkey PRIMARY KEY (company_id, aggregate_key, fingerprint).Risks
Low. The single statement becomes two, so the atomicity question is the only real one — and it is closed rather than hand-waved:
(company_id, aggregate_key, fingerprint), so the read matches at most the single row the write targets...._creation_claimsrows are deleted), so the row cannot vanish between the two statements.COALESCE(resolved_at, now())), so a concurrent delivery resolving the same member in between lands on the same terminal state and keeps the earlierresolved_at.Return values are unchanged:
no-membershipis still returned exactly when no member row matches that predicate, andresolvedIssueIdstill falls back to the passedissueId.Out of scope, deliberately. The 19 aggregates wedged in phase
firingare a separate defect. The firing fence is claimed at:852, thetryopens at:863, and thefinallyreleases at:1347with nothing throwable in between — so an exception cannot wedge it; only process death between claim and release can, which is what a rollout does. Fixing that needs restart-resumable fences and is tracked separately. Live DB shows19 firing / 92 active / 0 finalizing, confirming the two are independent.Model Used
claude-opus-5[1m]), 1M context, extended thinking, with tool use and code execution (repo checkout,vitest/tsc,kubectl, live Prometheus and PostgreSQL reads).Checklist
alert/select/fence/aggregate; the five open alertmanager PRs (ci: route lockfile drift alerts #1553, fix(alertmanager): keep severity=none alerts non-actionable #1539, fix(alertmanager): route team=devops and recognize severity=page/ticket #1360, fix(alertmanager): don't silently assign new alert issues to an uninvokable agent #1351, alertmanager-plugin: never make severity=none alerts agent-actionable #1277) all concern severity routing, owner invokability, and lockfile alerts, and none touchresolveAggregateMemberor thedb.query/db.executesplitFixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template