Skip to content

Replace the CI shell watchdog with Bobcat 0.8.0's supervisor observability - #4108

Merged
jeremydmiller merged 1 commit into
mainfrom
gh-bobcat-080-observability
Aug 24, 2026
Merged

Replace the CI shell watchdog with Bobcat 0.8.0's supervisor observability#4108
jeremydmiller merged 1 commit into
mainfrom
gh-bobcat-080-observability

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #4083. Closes #4089. Bobcat 0.8.0 (JasperFx/bobcat#145#150, cut today) moved everything build/ci-memory-sampler.sh (#4084/#4090) had to approximate from outside the process inside the supervisor — as facts instead of inferences. The watchdog guessed which pid was the test host (its first version latched onto sqlservr), inferred stalls from flat RSS + idle CPU, and raced the supervisor to the dump. The supervisor now simply states: which test is in flight past its budget, in which lane, in which pid.

What changes

build/SupervisedTests.cs — three report-only knobs on every supervised run:

build/StallCapture.cs (new) — the two consumer-side pieces Bobcat deliberately doesn't ship:

build/RetryLedger.cs — additive IsPartial / Stalled / StalledTests / PeakWorkerRssMb fields, PARTIAL and stalled banners on the step summary, independent ::warning annotations. build/flakiness-report.sh aggregates them with // 0 / // false defaults (verified against a mix of new-shape and old-shape ledgers, so an older baseline can't null the roll-up) and renders partial-jobs and stalled-tests sections.

tests.yml — the sampler launch, STALL_DEADLINE_SECONDS, and the entire stall_deadline matrix include: block are gone; build/ci-memory-sampler.sh is deleted. The measured-ceiling table it carried is obsolete by construction — there is no per-job deadline any more.

Known trade, stated rather than hidden

The sampler watched the largest process in the step's tree, which included pre-test phases — compile, Docker readiness gating. Bobcat's stall detection covers tests the supervisor has in flight. A wedge during compile or DockerUp is still bounded by the job cap, but the automatic dump for that rare case goes away.

Verified

  • CoreTests supervised against the published 0.8.0: 2633/2633 in 2:52, heartbeats every 30s, ledger fields populated — and the memory attribution immediately produced a real lead: peak worker RSS 5047 MB; top retainer: +685 MB CoreTests.Acceptance.wolverine_as_command_bus.use_iasync_enumerable_as_return_value.
  • The roll-up script exercised against synthetic old- and new-shape ledgers; bash -n and YAML validation on the workflow.

🤖 Generated with Claude Code

Bobcat 0.8.0 (JasperFx/bobcat#145-#150) moved everything the CI shell
watchdog (build/ci-memory-sampler.sh, GH-4083/GH-4089) approximated
from outside the process inside the supervisor, as facts instead of
inferences: which TEST is in flight past its budget, in which lane,
in which pid; a progress heartbeat; per-test RSS attribution; a
pre-kill diagnostic seam; and partial results on cancellation.

- StallThreshold 5m (the watchdog's own "a healthy suite is never
  idle for five minutes" calibration, now per test — no per-target
  deadline table for heavy suites to keep opting out of),
  HeartbeatInterval 30s, ResourceSampleInterval 15s. All report-only.
- OnBeforeKill runs the watchdog's own dotnet-dump collect +
  dumpasync --coalesce pipeline (ported to build/StallCapture.cs)
  against a live wedged worker before the kill discards its state.
- SIGTERM/SIGINT under Actions now snapshot the run and write a
  PARTIAL ledger inside the cancellation grace window (GH-4098: a
  capped job used to report nothing, and GitHub discards its logs),
  then best-effort capture the stalled workers' async stacks.
- Ledger gains IsPartial / Stalled / StalledTests / PeakWorkerRssMb
  (all additive; the roll-up jq defaults them for older baselines),
  with step-summary banners and ::warning annotations; the flakiness
  roll-up renders partial jobs and stalled tests.
- tests.yml sheds the sampler launch and the whole stall_deadline
  matrix block; build/ci-memory-sampler.sh is deleted.

Known trade, stated rather than hidden: the sampler also watched
pre-test phases (compile, Docker readiness). The job cap still bounds
those, but the automatic dump for that rare case goes away.

Smoke: CoreTests 2633/2633 against the published 0.8.0, heartbeats
every 30s, ledger fields populated — and the memory attribution
already named a real finding (+685 MB retained by one test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jeremydmiller
jeremydmiller merged commit 31e6cf8 into main Aug 24, 2026
38 checks passed
erdtsieck pushed a commit to erdtsieck/wolverine that referenced this pull request Aug 27, 2026
…edger actually writes

Found live on the first capped job after the cancellation handler
shipped (JasperFx#4108): CIKafka wedged on batch_processing_with_kafka.end_to_end,
the stall detector named the test and its pid in the log — and the
partial ledger never happened, because the runner signals only the
step's own shell and nothing in the bash -> build.sh -> `dotnet run`
chain forwards SIGTERM to the grandchild holding the handler.

The fix is a pid handshake: registerCancellationCapture publishes
Environment.ProcessId to .nuke/temp/build.pid, and the workflow steps
run through build/run-with-cancellation-relay.sh, whose INT/TERM trap
signals exactly that pid — deliberately NOT the process group, which
would also kill the wedged worker the handler wants to dump — and
holds the step open while the handler works, inside the runner's own
hard-kill deadline. Applied to tests.yml, http.yml, slow-tests.yml
and dotnet.yml's test steps; smoke/pack steps have nothing to relay to.

Proven by simulating the runner locally: SIGTERM to the relay while
CoreTests was mid-run produced "[stall] relaying cancellation to the
build process (pid N)" -> "[stall] cancellation signal received" ->
a ledger on disk with IsPartial=true, 533 clean passes preserved from
the live stream and 2107 honestly Indeterminate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant