Skip to content

feat(observability): worker-unreachable watchdog (escalation #329)#4358

Merged
bokelley merged 1 commit into
mainfrom
bokelley/worker-watchdog
May 11, 2026
Merged

feat(observability): worker-unreachable watchdog (escalation #329)#4358
bokelley merged 1 commit into
mainfrom
bokelley/worker-watchdog

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

Web-side watchdog that polls /internal/jobs on the worker every 60s and emits logger.error after 3 consecutive failures. logger.error auto-routes to #admin-errors via the existing posthog notifier, so the alert reaches Slack without any extra wiring.

Why

Escalation #329 surfaced that the worker had crashlooped for 6 days (machine created 2026-05-04, recovered 2026-05-10). Every scheduled job — compliance heartbeat, escalation triage, weekly digest, announcement handlers, conversation insights, knowledge staleness, ~40 others — silently stopped firing. The only user-visible signal was Evgeny's stale "comply re-runner" status; the response from /api/admin/jobs already carried workerUnreachable: true but nothing was polling it.

Design

  • 60s tick, 3-failure threshold → minimum 3-minute outage before paging. Avoids transient blip spam.
  • Fire-once: re-alerts only after recovery. Without this a flapping worker pages every tick.
  • Recovery transition logs at info level with the prior failure count.
  • Same worker.process.<app>.internal:8080 lookup the existing /api/admin/jobs proxy uses (http.ts:2256), so the alert reflects the exact reachability that the admin UI does.

Tests

5 vitest cases:

  • single failure does not alert
  • threshold-crossing alerts exactly once and stays silent after
  • recovery from alerted state logs at info
  • non-2xx HTTP counts as failure
  • transient success in the middle of a streak resets
 Test Files  1 passed (1)
      Tests  5 passed (5)

Follow-up (separate PR)

The actual root cause of the 6-day crashloop — app.listen() racing the training-agent tenant warmup, killing the worker via health-check timeout during the 30-60s warmup window — is being addressed by re-landing PR #4062 (the boot gate that was reverted on May 4). Tracking separately.

🤖 Generated with Claude Code

Polls http://worker.process.<app>.internal:8080/internal/jobs from web every
60s and emits logger.error after 3 consecutive failures. logger.error
auto-routes to #admin-errors via the existing posthog notifier, so the
alert lands in Slack without any extra wiring.

Closes the silent-death gap behind escalation #329: the worker
crashlooped for 6 days. Every scheduled job (compliance heartbeat,
escalation triage, weekly digest, announcement handlers, etc.) silently
stopped firing and the only signal a user got was Evgeny's stale "comply
re-runner" status. workerUnreachable was already in the /api/admin/jobs
response shape but nothing was polling it.

Fire-once semantics: alert when streak crosses threshold, info-level
recovery line on the first successful tick after an alert. Without these
a flapping worker would page on every tick.

5 vitest cases cover threshold-crossing, single-alert dedup, recovery
transition, non-2xx as failure, and streak reset on transient success.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit feb8e86 into main May 11, 2026
13 checks passed
@bokelley bokelley deleted the bokelley/worker-watchdog branch May 11, 2026 06:23
bokelley added a commit that referenced this pull request May 18, 2026
PR #3374's `auto_start_machines = true` + `min_machines_running = 1` on
the worker `[[services]]` block are inert without a `[[services.ports]]`
entry — Fly only enforces those flags via fly-proxy, and a service with
no ports isn't routed through fly-proxy. Rolling deploys that leave the
worker `stopped` had no recovery path; the watchdog from PR #4358
observed the failure but couldn't act on it. Today's 04:31 UTC alert
required a manual `fly machine start`.

On probe failure the watchdog now calls the Fly Machines API, finds any
worker machines in `stopped` state, and starts them. The recovered
machine surfaces as a successful probe on the next tick. If start
didn't help (real crashloop), failures keep climbing and the alert
still fires at the threshold.

Requires the `FLY_API_TOKEN` secret on the app — without it, recovery
is a no-op and behavior matches PR #4358 exactly. Mint with
`fly tokens create deploy -a adcp-docs` and set via `fly secrets set`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 18, 2026
* docs(fly): correct misleading worker autostart comment (#4723)

The fly.toml block for the worker process group claimed that adding
`auto_start_machines = true` + `min_machines_running = 1` opted workers
into Fly's autostart lifecycle. It does not — per Fly docs, those flags
are enforced through fly-proxy, and a `[[services]]` block without
`[[services.ports]]` doesn't get fly-proxy routing. The settings are
inert; the block still earns its keep by attaching the http_check.

The actual recovery gap (rolling deploys can leave the worker `stopped`
with no automatic restart) is tracked in #4723.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(watchdog): active recovery for stopped workers (#4723)

PR #3374's `auto_start_machines = true` + `min_machines_running = 1` on
the worker `[[services]]` block are inert without a `[[services.ports]]`
entry — Fly only enforces those flags via fly-proxy, and a service with
no ports isn't routed through fly-proxy. Rolling deploys that leave the
worker `stopped` had no recovery path; the watchdog from PR #4358
observed the failure but couldn't act on it. Today's 04:31 UTC alert
required a manual `fly machine start`.

On probe failure the watchdog now calls the Fly Machines API, finds any
worker machines in `stopped` state, and starts them. The recovered
machine surfaces as a successful probe on the next tick. If start
didn't help (real crashloop), failures keep climbing and the alert
still fires at the threshold.

Requires the `FLY_API_TOKEN` secret on the app — without it, recovery
is a no-op and behavior matches PR #4358 exactly. Mint with
`fly tokens create deploy -a adcp-docs` and set via `fly secrets set`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <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

Development

Successfully merging this pull request may close these issues.

1 participant