fix(notify): page once per breakage, and name what a redirect hit (ENG-2324) - #62
Merged
mindsdb-devops merged 3 commits intoSep 4, 2026
Merged
Conversation
A failure posted unconditionally and skipped the prior-run lookup entirely, so a standing breakage re-paged on every run. The public web probe runs on a `*/5` cron against two production routes that have been returning 301 and 403 since before the probe existed, which worked out to about 220 Slack messages a day for one already-known bug, in the channel that also carries the paging Cloudflare checks. Both directions now turn on the same evidence. The lookup runs whenever anything might post, a failure whose predecessor also failed stays quiet, and the two directions fail open in opposite ways on purpose: with no evidence an alert posts and a recovery does not, because an unreported failure costs more than a duplicate one. The lookup also asked for the branch's fifty most recent runs and filtered by workflow afterwards, so a frequent cron crowded every other workflow out of its own history: at five-minute cadence those fifty slots span about four hours, and any workflow whose previous run was older found no evidence and silently dropped its recovery message. It now asks the per-workflow runs endpoint, which cannot be crowded out. A caller whose `workflow_ref` names another repo keeps the branch scan, since no such endpoint exists here. Lucas Koontz - Probe public web endpoints every five minutes Refs: ENG-2324
…e report (ENG-2324) The alert said `production /cowork status 301` and stopped there. The status says a route moved; the destination says why it is broken. `/cowork` lands on `/cowork/`, which 403s because a docroot directory shadows the SPA fallback, and that hop was the whole diagnosis. `FetchResult` now carries the `Location` header and a failing redirect reads `status 301 to /cowork/`. The target is compacted to 64 characters, since the origin controls that header. `format_alert_label` raised a `ValueError` once failure identities outgrew the label cap, and `main` did not catch it. That turned the widest possible outage into a traceback in the one step whose job is to report that something is down. It now degrades to a count and leaves the identities in the run log. The compact path drops a redirect's destination before it drops an endpoint's identity. Lucas Koontz - Probe public web endpoints every five minutes Refs: ENG-2324
mindsdb-devops
previously approved these changes
Sep 4, 2026
…324) Collapsing repeated failures introduced a hole. A run's conclusion records THAT it failed, never WHAT failed, so two dead routes and a total console outage are both `failure`. With the previous run already red, a worsening outage posted nothing: production /cowork and /assets/ failing would have masked the whole console going down. A still-failing pipeline now reports itself again every `repeat-alert-after-minutes`, default 60. Windows are counted from the oldest failure in the current streak rather than from the last message, because nothing records when a message was sent; this run and its predecessor are each placed in a window and the repeat fires when they differ, which posts once per window at any cadence. For the five-minute probe that is 23 messages a day instead of 220, and a change in the failing set surfaces within the hour instead of never. Every unreadable input fails open. A missing or unparseable streak clock posts, a non-numeric interval falls back to the default, and zero or a negative interval cannot be used to mute the channel. A reminder that fires early costs one message; one that never fires hides a growing outage. Lucas Koontz - Probe public web endpoints every five minutes Refs: ENG-2324
mindsdb-devops
approved these changes
Sep 4, 2026
mindsdb-devops
deleted the
fix/eng-2324-alert-storm-and-probe-diagnostics
branch
September 4, 2026 21:13
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.
User story
As an engineer on call for MindsHub
I want one Slack message per breakage, and an alert that names what actually broke
So that the channel stays worth reading and the first message is enough to start diagnosing
Why this matters
Public web probemerged at 19:46Z on 2026-09-04 and began firing on its*/5cron seven minutes later. It is correct: productionconsole.mindshub.ai/coworkreturns 301 to/cowork/which returns 403, and/assets/returns 403. Both are real, both are ENG-2317, and the fix for them is onstagingwaiting for a release.The problem is what the notifier does with that. Nothing suppressed a repeated failure, so the same two routes re-paged every run. Measured cadence is 6.5 minutes once GitHub throttles the cron, which is about 220 Slack messages a day for one already-known bug, into
#monitoring-prod, the channel that also carries Cloudflare's paging 60-second checks. That is how a real page gets scrolled past.Two more defects surfaced while confirming it:
mainworkflow whose previous run was older found no evidence of itself and silently dropped its recovery message. The probe was degrading every other pipeline's alerting in this repo.format_alert_labelraised an uncaughtValueErroronce failure identities outgrew the label cap, in the one code path whose job is to report that something is down.What changed
flowchart TB subgraph Before R1["Run finishes red"] --> P1["post = true, unconditionally"] P1 --> S1["Slack alert"] S1 --> N1["Next run, 5 min later"] N1 --> R1 endflowchart TB subgraph After R2["Run finishes"] --> L2["Look up the previous conclusive run<br/>of THIS workflow on this branch"] L2 --> D2{"status vs predecessor"} D2 -->|"failed, predecessor green"| A2["Alert"] D2 -->|"failed, no evidence"| A2 D2 -->|"failed, predecessor also failed"| Q2["Quiet, already reported"] D2 -->|"recovered, predecessor failed"| G2["Recovery"] D2 -->|"recovered, predecessor green"| Q3["Quiet, routine green"] endA three-day outage now costs about 70 messages instead of about 660, and never fewer than one per hour while it lasts.
Collapsed is not muted, and that distinction is the point. A run's conclusion records that it failed, never what failed, so two dead routes and a total console outage are both
failure. Suppressing every repeat would let the second grow behind an alert already sent for the first — with/coworkred, the entire console going down would have posted nothing. A still-failing pipeline therefore repeats itself everyrepeat-alert-after-minutes(default 60), measured from the oldest failure in the current streak so it lands once per window at any cadence.The two directions fail open in opposite ways on purpose. With no evidence an alert posts and a recovery does not, because an unreported failure costs more than a duplicate one. A caller missing the
actions: readgrant therefore degrades to today's behaviour rather than to silence.Acceptance criteria
repeat-alert-after-minutes, and only once per window.repeat-alert-after-minutesof zero, a negative, or a non-number falls back to the default and cannot mute the channel.prev_conclusionempty, the refused-lookup case) still posts.cancelledandskippedare not evidence the channel was already told, so they let the alert through.--force-postoverrides the deduplication, so the reusable's own smoke test still posts.--force-post.workflow_refnames another repo keeps the branch scan and its name filter.How to test
uv run --with pyyaml --with pytest --python 3.12 -m pytest tests/ -qandactionlint -color.Expect
failure, which is what makes the next probe run stay quiet once this merges.postindecideback to an unconditionalTrueand re-runtests/test_notify_decision.py. Expect 5 failures. Restore.inputs.status == 'recovered' &&to theprevstep'sif:and re-runtests/test_notify_pipeline_status.py. Expect 2 failures. Restore.#monitoring-prod. The probe keeps running every five minutes and keeps failing until ENG-2317 reaches production, and it should post nothing until it recovers.Notes for the reviewer
2fed976adds the bounded reminder and the tests that fail without it. Worth reviewing that commit specifically. All 21 endpoints keep their contract, and no endpoint was dropped fromconfig/console-route-probe.jsonto get the run green./coworkand/assets/was the tempting fix and is the wrong one. They are the only checks that noticed the production bug, and removing them breaks 7 contract tests that pin the endpoint count. The contract is right; the notifier was wrong.notify-pipeline-statusnow gets per-workflow history. That is strictly better for recovery detection, and it is what stops the probe degrading the rest of this repo's alerting, but it does change which run counts as "previous" for any workflow that shares a branch with a busier one.per_pagedrops from 50 to 10 on the per-workflow path because the filter is now server-side. The branch-scan fallback keeps 50.Verified locally
pytest tests/ -qactionlint -colorworkflow_graph.py --allow-external-reusablesgit diff --checkraiserecovered, per-workflow endpoint removedprevstep executed against the real API, per-workflow pathprev_conclusion=failure, streak of 7, dated to the first probe failure at 19:53:09Zprevstep executed against the real API, branch-scan fallbackprev_conclusion=success, streak 0*/5probe against a standing breakagemindsdb/github-actionsfailure, the value that will suppress the next duplicatesuccess, so the cross-repo path still composes/cowork301 to/cowork/,/cowork/403,/assets/403; all 7 other probed routes 200 with the SPA markerShips with
Production fix for the routes this alert is reporting: mindsdb/mindshub_frontend#1529
Merge order: independent. This one only changes how the channel is told.