Skip to content

Host controller wedges with a frozen heartbeat while the control plane and doctor still report healthy (two occurrences on 2026-09-04) #331

Description

@kyle-sexton

Summary

The host controller (v0.1.24, melo-lap-001) wedged twice within 13 hours. Both times the process stayed alive and the control plane kept answering host status, but the reconcile loop stopped: observed.heartbeatAt froze, no worker was created or retired, and every job for melodic-ubuntu-24.04-x64 sat queued at GitHub while GitHub's own status page reported all systems operational.

Evidence

Wedge 1 (2026-09-04 01:03Z to 01:53Z, PID 14732, uptime about 24 h):

  • host status --json: controller.phase=ready, observed.heartbeatAt=2026-09-04T01:03:14Z for 50 minutes; three workers reported (idle, busy, busy) while docker ps showed zero ci-runner-* containers. Worker log for dl64halo74o4-2v8 ends at 01:03:30Z with a clean Runner listener exit with 0 return code.
  • Process had consumed about 126,899 CPU-seconds over its lifetime (~1.4 cores continuously). host controller restart hung 240 s waiting for the drain confirmations the dead loop never produced. taskkill /F plus schtasks /Run /TN ci-runner-fleet recovered it; first fresh heartbeat 01:57:36Z, queue drained within two minutes.

Wedge 2 (2026-09-04, PID 206652 from 01:53Z, then PID 89152 after a graceful restart at 13:54Z):

  • From about 13:40Z GitHub stopped assigning queued jobs to the scale set although four workers were idle, capacityAcknowledged=true, maxCapacity=11, totalAssignedJobs=0; 17 github-iac jobs queued for 12 minutes. Controller log in that window shows only worker-retirement-zero-stability-unconfirmed (x10), worker-removal-capacity-stale (x9), one runner-registration-missing, one unregistered-worker-became-busy.
  • host controller restart completed cleanly this time (capacity-zero drain accepted, exit code 75, receipt verified, replacement PID 89152 started 13:54:51Z).
  • The replacement never left phase=disabled although desired.mode=enabled: observed.heartbeatAt stayed at 13:54:49Z for 9+ minutes, both listeners at capacity=0, zero workers, controller log contains only the controller-started line. Process CPU: 11.05 s in a 10 s window (busy loop), 23 threads, 148 MB, and the only TCP connection was the local OTEL exporter on 127.0.0.1:4317 (no GitHub or Docker API traffic). host enable --wait hung 180 s. host doctor reported every check PASS except the advisory pending-OS-reboot WARN.

Host: Windows 11 Pro 26200, Docker Desktop responsive throughout (docker version/docker info under 1 s), AC power, 37 GiB free memory, host CPU otherwise idle.

Fix

Unknown; needs the reconcile loop's own diagnostics. Suggested first steps:

  1. Make the loop's liveness observable: a stall of observed.heartbeatAt beyond reconcileInterval x N should be surfaced by host doctor as a FAIL, not PASS (it showed PASS with age=2m17s maximumAge=26m10s while the loop was already dead).
  2. Capture a goroutine dump (or the equivalent) on host controller restart timeout and on a heartbeat stall, written under diagnostics/.
  3. Check the startup path for a busy-wait: the replacement process spun at ~1 core with no network activity and no log lines after controller-started.

Verification

  • A controller that has stalled for longer than the documented threshold is reported by host doctor as FAIL with the stall age.
  • A diagnostic archive exists for each stall and names the blocked goroutine or loop.

Related

  • Wedge 1 recovery commands and timings are recorded in the operator's session notes for 2026-09-04.
  • Config: %LOCALAPPDATA%\ci-runner\config.yaml (schemaVersion 2, reconcileInterval: 5s, startupTimeout: 2m, power.policy: ac-only).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.priority: criticalProduction broken, data loss, active exploit, or release blocker.status: needs-decisionAwaiting a human or maintainer judgment call.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions