Skip to content

fix(ci): classify stalls and failures instead of guessing (REQ-317, REQ-316) - #890

Merged
avrabe merged 2 commits into
mainfrom
fix/req-316-317-ci-signal-honesty
Sep 5, 2026
Merged

fix(ci): classify stalls and failures instead of guessing (REQ-317, REQ-316)#890
avrabe merged 2 commits into
mainfrom
fix/req-316-317-ci-signal-honesty

Conversation

@avrabe

@avrabe avrabe commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Two CI signals were sending readers to the wrong conclusion, and both had
already produced a published misattribution.

REQ-317 — three defects in the liveness probe

  1. Its diagnose hint told every reader to run
    gh api repos/<repo>/actions/runners --jq .total_count and read
    0 = pool offline. Runners here are org-registered, so that returns 0
    on a perfectly healthy pool and can never say otherwise. The alert's own
    hint was the trap.
  2. It reported a duration and nothing else, so hosted starvation and a dead
    self-hosted pool produced identical alerts — and hosted capacity appears in
    the runners API at no scope, so no runner count can separate them.
  3. It counted queue age on runs that had already completed: 🚨 CI runner pool liveness alert #867 reported
    "queued 177m" for a cancelled run that was never going to start.

REQ-316 — a red X that means "the runner left"

A job killed by a fleet restart reports failure with no failed step — the
steps end skipped. GitHub renders that identically to a broken test, which is
how a wrong cause got published on #855.

Both are now classifiers with fixtures

classify_stall    no-queue | hosted-starved | pool-offline
                  | label-saturated | capacity-available
classify_failure  job-passed | runner-vanished | setup-failed | job-failed

A fifth failure mode turned up while building this and is covered:
label-partitioned saturation. On 2026-09-02 the pool read online=12 busy=8
a third of the fleet idle — while every rust-cpu job queued, because the four
idle runners carried only lean-mem. A global busy-vs-online count cannot see
that, so capability is computed per label against the labels of what's
queued.

Validated against live data, not only fixtures

5 hosted + 3 rust-cpu queued, only lean-mem/light idle → label-saturated
control: filter to just the hosted jobs               → hosted-starved
real Kani job from #888                               → setup-failed

That last one is the discriminator I'd been applying by hand all cycle, now
mechanical.

Negative-controlled: ignoring per-label capability, losing the runner-vanished
case, and dropping the hosted short-circuit each redden the oracle.

The tests actually run

They're wired into the YAML Lint job, which is in CI Gate's needs. A
classifier that never executes is documentation, not a gate.

They're shell — and their rivet: verifies markers are picked up by
coverage --tests only because REQ-319 taught the scanner to read shell
scripts earlier in this same round
. Before that they'd have been invisible and
neither requirement could have reached verified by any honest route. Which is
exactly what the #870 reporter said would happen.

Scope split — please note

REQ-316 asked for two things. This carries the legibility half. Finding the
cause on the runner hosts (unattended upgrades, a systemd timer, config
management, an autoscaler reclaim — all consistent with the 04:30–05:00 UTC
window both incidents fall in) needs shell access to those machines, so it's
deferred to REQ-323 at v0.37.0. Recorded, not dropped.

Gates

diagnose oracle (10 cases) · cargo fmt --check · cargo test --workspace
(exit 0, 66 ok) · rivet validate · rivet docs check · yamllint — all
exit 0. Both workflow files parse as YAML and every embedded shell block passes
bash -n.

Fourth of the v0.36.0 feature-loop run.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

📐 Rivet artifact delta

Change Count
Added 1
Removed 0
Modified 2
Downstream impacted (depth ≤ 5) 0

Graph

graph LR
  REQ_316["REQ-316"]:::modified
  REQ_317["REQ-317"]:::modified
  REQ_323["REQ-323"]:::added
  classDef added fill:#d4edda,stroke:#28a745,color:#155724
  classDef removed fill:#f8d7da,stroke:#dc3545,color:#721c24
  classDef modified fill:#fff3cd,stroke:#ffc107,color:#856404
  classDef overflow fill:#e2e3e5,stroke:#6c757d,color:#495057,stroke-dasharray: 3 3
Loading
Added
  • REQ-323
Modified
ID Changes
REQ-316
REQ-317

📎 Full HTML dashboard attached as workflow artifact rivet-delta-pr-890download from the workflow run.

Posted by rivet-delta workflow. The graph shows only changed artifacts; open the HTML dashboard (above) for full context.

@codecov

codecov Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…EQ-316)

Two CI signals were sending readers to the wrong conclusion, and both had
already produced a published misattribution.

REQ-317 — the liveness probe had three defects, all fixed:

  1. Its diagnose hint told every reader to run
     `gh api repos/<repo>/actions/runners --jq .total_count` and read `0 = pool
     offline`. Runners here are ORG-registered, so that command returns 0 on a
     perfectly healthy pool and can never say otherwise. The alert's own hint
     was the trap.
  2. It reported a duration and nothing else, so hosted starvation and a dead
     self-hosted pool produced identical alerts — and hosted capacity appears
     in the runners API at NO scope, so no runner count can tell them apart.
  3. It counted queue age on runs that had already COMPLETED: #867 reported
     "queued 177m" for a cancelled run that was never going to start.

REQ-316 — a job killed by a fleet restart reports `failure` with NO failed
step, because the steps end `skipped`. GitHub renders that identically to a
broken test, which is how a wrong cause got published on #855.

Both are now shell functions in tools/ci-diagnose/diagnose.sh with fixtures:

  classify_stall    no-queue | hosted-starved | pool-offline | label-saturated
                    | capacity-available
  classify_failure  job-passed | runner-vanished | setup-failed | job-failed

A FIFTH failure mode turned up while building this and is covered: label-
partitioned saturation. On 2026-09-02 the pool read online=12 busy=8 — a third
of the fleet idle — while every rust-cpu job queued, because the four idle
runners carried only lean-mem. A global busy-versus-online count cannot see
that, so capability is computed PER LABEL against the labels of what is queued.

Validated against live data, not only fixtures: with 5 hosted and 3 rust-cpu
jobs queued and only lean-mem and light runners idle, classify_stall returns
label-saturated; the control that filters to just the hosted jobs returns
hosted-starved. classify_failure on the real Kani job from #888 returns
setup-failed — the same discriminator applied by hand all cycle, now mechanical.

Negative-controlled: ignoring per-label capability, losing the runner-vanished
case, and dropping the hosted short-circuit each redden the oracle.

The tests run in the YAML Lint job, which IS in CI Gate's needs. A classifier
that never executes is documentation, not a gate. They are shell, and their
`rivet: verifies` markers are picked up by `coverage --tests` only because
REQ-319 taught the scanner to read shell scripts earlier in this same round —
before that they would have been invisible.

REQ-316 is SPLIT, decided with the maintainer. This carries the legibility
half. Finding the cause on the runner hosts — unattended upgrades, a systemd
timer, config management or an autoscaler reclaim, all consistent with the
04:30-05:00 UTC window both incidents fall in — needs shell access to those
machines and is deferred to REQ-323 at v0.37.0. Recorded, not dropped.

Confirmed with the diagnose oracle (10 cases), cargo fmt --check, cargo test
--workspace (exit 0, 66 ok), rivet validate, rivet docs check, yamllint on the
CI paths — all exit 0. Both workflow files parse as YAML and every embedded
shell block passes `bash -n`.

Implements: REQ-316, REQ-317
Verifies: REQ-316, REQ-317
Answered on #862 by the infrastructure maintainer, not from this repository.
`needrestart` runs as a dpkg Post-Invoke hook and, without a TTY under
unattended-upgrades, restarts every service mapping an upgraded library.
`github-runner@*.service` was never excluded, so a library upgrade stops and
starts all twelve runners mid-dpkg. `apt-daily-upgrade.timer` fires
06:00-06:45 CEST — 04:00-04:45 UTC, the window both incidents fall in.

All three occurrences match to the second, each upgrading a library
Runner.Listener links for HTTPS: curl at +13s, openssl at +21s, libssh-4 at
+30s. Negative-controlled: on 2026-09-04 the upgrade installed only gnupg,
which the runners do not map, and no restart followed. The mechanism explains
the non-events as well as the events, which is what makes it a diagnosis rather
than a correlation.

Recorded as `implemented`, NOT `verified`: the drop-in is deployed and parses,
but suppression of a real restart has not been demonstrated — that needs an
actual library upgrade, and the maintainer is watching the next
apt-daily-upgrade. The acceptance clause is the suppression, not the diagnosis.

This also corrects #839: exit 143 on Kani in that window is this needrestart,
not hosted-runner reclaim — confirming REQ-316's note that the shutdown message
alone does not identify which pool restarted.

It removes A cause, not THE cause. Runners also vanish on a broker-service
wedge and under host memory pressure, so the REQ-316 legibility work stays
worth having on its own.

Refs: REQ-323, REQ-316
@avrabe
avrabe force-pushed the fix/req-316-317-ci-signal-honesty branch from d3034c7 to 3a14293 Compare September 5, 2026 15:43

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'Rivet Criterion Benchmarks'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.20.

Benchmark suite Current: 3a14293 Previous: 16bff52 Ratio
traceability_matrix/1000 59674 ns/iter (± 599) 46077 ns/iter (± 202) 1.30
query/10000 352091 ns/iter (± 3835) 244880 ns/iter (± 1851) 1.44

This comment was automatically generated by workflow using github-action-benchmark.

@avrabe
avrabe merged commit 2cb99c6 into main Sep 5, 2026
34 checks passed
@avrabe
avrabe deleted the fix/req-316-317-ci-signal-honesty branch September 5, 2026 16:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant