feat: stagger gateway startup after a recent loopstall crash dump - #2486
Conversation
867b5ce to
611c6ce
Compare
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo semantic defects found. The change is well-guarded: No findings. [OPUS-REVIEWED] 611c6ce |
Design Review (Fable 5, fork) — ✅ PASSAdvisory design-level review of Design-Verdict: PASS A real, observed re-wedge loop mitigated with the minimal proportionate shape: one reversible bool, fail-open everywhere, no new scheduling machinery. Suggestions
[DESIGN-REVIEWED] 611c6ce |
Cautious boot after a recent loop-stall crash
Problem
When a Kiro Crew gateway wedges under host memory pressure, the loop-stall watchdog dumps thread stacks and hard-exits so the service manager can restart it. The NEW instance already detects that dump at boot — it logs
⚠️ Prior loop-stall crash dump found: ... (0.0 hours ago)and notifies the user — and then launches its entire startup battery at once anyway: MCP gateway sidecar, cron scheduler (which immediately fires every overdue job), app backends, MCP server probes, and session restores, each of which spawns subprocesses.On a host that is still under the same memory pressure that killed the previous instance, that simultaneous burst is exactly what re-wedges the new one. This was observed in practice: a gateway died under memory pressure, the restarted instance detected the minutes-old dump, launched everything at once, its own watchdog wedged within seconds, and the host froze shortly after. The gateway had the signal and did not act on it.
What changed
A new
dashboard/cautious_boot.pymodule turns the existing detection into behavior:initialize(), evaluated viaasyncio.to_threadearly in gateway startup): is there a prior loop-stall dump with real stack content younger than 30 minutes?resource_status.probe) picks the pause length —tight/criticalhost → 10s between battery groups (maximum caution)amplehost → 2s between groups (mild stagger; anunknownposture reading deliberately does not escalate)await asyncio.sleep(...), so the event loop stays responsive and the loop-stall watchdog keeps getting its heartbeat throughout.pause_before()without a priorinitialize()(tests, embedded starts) is a no-op.🐢 Cautious boot ACTIVE: ...is logged at WARNING with the dump name, its age, the posture, and the chosen delay, so an operator reading the journal after an incident sees the gateway both noticed the crash and changed its behavior.One new config key, following the existing loader conventions (typed accessor with default, non-bool values fall back to the default):
dashboard.cautious_boot(bool, default on) — the recency threshold (30 min) and the two delays are deliberately module constants, keeping the config surface to a single switch.What is deliberately NOT here
Testing
New
test/test_cautious_boot.py(19 tests) covering the decision matrix (recent dump × ample/tight/critical/unknown posture, old dump, no dump, header-only dump, config off, missing config attribute), fail-open paths (unreadable store, probe exception, worker-thread exhaustion), and the async plumbing (decision caching, loud activation log,pause_beforeno-op when uninitialized/inactive, correct sleep when active).Also run green: crash-dump store and resource-status neighbors (65 tests), config schema/loader suites (275 tests), and the dashboard/API server startup suites (96 tests).
isort,flake8, andmypypass on all touched files.