返回资讯中心
外部精选
软件工程
#开发者实时流

Systemd WatchdogSec: The Kill Switch for Hung AI-Generated Code

systemd WatchdogSec: The Kill Switch for Hung AI-Generated Code

Hacker News 最新cheikhdev2026/11/5 · 今天17 分钟阅读

以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。

Programming

Ai Generated Code

Linux

systemd WatchdogSec: The Kill Switch for Hung AI-Generated Code

How systemd catches hung AI code

Cheikh seck

5 min read·

Just now

--

A watchdog is not a health check. A health check observes. A watchdog enforces.

In Linux, systemd has a built-in watchdog: you tell it "if this process doesn't prove it's alive every N seconds, kill it." No cron job. No sidecar. No if pgrep script. The kernel-facing supervisor does it.

WITHOUT WATCHDOG WITH WATCHDOG

Process: [alive] [alive] [HUNG...] Process: [alive] [alive] [HUNG...] systemd: (watching) (watching) systemd: ping ✓ ping ✓ ping ✗ Result: zombie forever Result: SIGKILL at T+10s Restart=always → fresh process

That’s the whole idea. The process must periodically send a heartbeat. Miss the deadline, systemd assumes it’s deadlocked, wedged on a network call, or stuck in a loop it can’t escape — and terminates it. Restart=always then brings it back clean.

For AI-generated code, this matters. Generated code can call external APIs, load models, run loops, or spawn concurrent work. A hung generated function can stall forever. A watchdog turns that silent failure into a loud, logged, auto-recovered one.

The experiment

I built a minimal Go program that pings systemd every 2 seconds, with a flag to simulate a hang. Then I ran it two ways:

Test 1 — Normal: ping every 2s, exit cleanly after 3s Expected: exit 0, result: success

Test 2 — Hang: ping every 2s, stop pinging after 2s, deadlock Expected: SIGKILL at ~10s, result: watchdog

systemd (PID 1) │ ├─ watchdog-test.service Type=notify WatchdogSec=10s │ └─ go-program -ping-interval=2s -normal-exit=3s │ └─ watchdog-hang.service Type=notify WatchdogSec=10s └─ go-program -ping-interval=2s -hang-after=2s

The Go program is just a means to demonstrate the utility. The point is the mechanism. Code: https://github.com/cheikh2shift/go-snippets/tree/main/watchdog-sec

The full program is ~90 lines. Four parts matter.

watchdog, err := daemon.SdWatchdogEnabled(false) if err != nil { log.Fatalf("Failed to check watchdog: %v", err) } if watchdog == 0 { log.Fatal("Watchdog not enabled — run under systemd with WatchdogSec set") } log.Printf("Watchdog enabled — must ping every %s", watchdog)

SdWatchdogEnabled reads WATCHDOG_USEC from the environment. systemd only sets that variable for Type=notify services with WatchdogSec= configured. If it's 0, you're not under a watchdog — fail fast instead of pretending.

_, err = daemon.SdNotify(false, daemon.SdNotifyReady) if err != nil { log.Fatalf("SdNotify READY failed: %v", err) } log.Printf("Sent READY=1 to systemd")

This is the step everyone misses. With Type=notify, systemd waits for READY=1 to consider the service started. Exit before sending it and systemd reports:

Finished with result: protocol Main processes terminated with: code=exited, status=0/SUCCESS

Exit code 0, but systemd-run --wait returns 1. The process succeeded; the protocol failed.

ctx, cancel := context.WithCancel(context.Background()) defer cancel()

go func() { ticker := time.NewTicker(*pingInterval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: _, err := daemon.SdNotify(false, daemon.SdNotifyWatchdog) if err != nil { log.Printf("Watchdog ping failed: %v", err) } else { log.Printf("Watchdog ping sent") } } } }()

daemon.SdNotify(false, daemon.SdNotifyWatchdog) sends WATCHDOG=1 over the NOTIFY_SOCKET datagram socket systemd gave us.

if *hangAfter > 0 { go func() { time.Sleep(*hangAfter) log.Printf("SIMULATING HANG — stopping pings, deadlocking") cancel() // stop watchdog pings select {} // deadlock forever }() }

cancel() stops the ping loop. select {} with no cases blocks forever. That's what a real hang looks like from systemd's perspective.

T+0s Program starts, sends READY=1 T+0s Ping loop starts (every 2s) T+2s Last ping sent T+2s cancel() → ping loop exits T+2s select{} → main goroutine deadlocks T+10s systemd: no ping for 10s → SIGKILL T+10s Result: watchdog

Four bugs I hit — and what they teach

Bug 1: Wrong import path

go: go-program imports github.com/coreos/go-systemd/v22/sdnotify: module ... found (v22.7.0), but does not contain package .../sdnotify

There is no sdnotify package. The correct path is github.com/coreos/go-systemd/v22/daemon, with functions daemon.SdNotify, daemon.SdNotifyWatchdog, daemon.SdNotifyReady, and daemon.SdWatchdogEnabled.

Unknown assignment: WatchdogSec=30s

WatchdogSec is a service property, not a scope property. Use a transient service:

systemd-run --wait --unit=watchdog-test
--property=Type=notify
--property=WatchdogSec=10s
--property=NotifyAccess=main
./go-program -ping-interval=2s -hang-after=2s

Bug 3: Missing READY=1 → result: protocol

Always send READY=1 at startup for Type=notify services.

A busy loop in one goroutine doesn’t stop the ping goroutine. Use context cancellation + select {}.

systemctl stop watchdog-test.service watchdog-hang.service 2>/dev/null || true systemctl reset-failed watchdog-test.service watchdog-hang.service 2>/dev/null || true systemctl daemon-reload

Or use a unique name per run: --unit="watchdog-test-$$".

=== systemd WatchdogSec — Quick Test === [2/3] Test 1: Normal run (watchdog active, no hang, exits 0) Finished with result: success Main processes terminated with: code=exited, status=0/SUCCESS Service runtime: 3.032s exit code: 0 (expected 0) ✓ PASS [3/3] Test 2: Hang simulation (hangs after 2s, watchdog kills at ~10s) Finished with result: watchdog Service runtime: 12.140s exit code: 1 (expected non-zero) ✓ PASS - watchdog killed hung program Nov 05 12:16:31 mainnet systemd[1]: watchdog-hang.service: Failed with result 'watchdog'.

Test 1: clean exit in 3 seconds, result: success. Test 2: pings stop at 2s, systemd kills at ~10s, result: watchdog.

systemd’s watchdog is the simplest liveness enforcement available to a Linux process. No sidecar, no external monitor, no polling loop. Three lines of setup (Type=notify, WatchdogSec=, SdNotifyReady) and a periodic SdNotifyWatchdog call, and a hung process becomes a logged, auto-restarted one.

The pitfalls are all in the setup, not the concept: wrong import path, --scope instead of a service, missing READY=1, and a hang simulation that doesn't actually stop the heartbeat. Get those four right and it works exactly as documented.

For AI-generated code, the watchdog interval is a design decision, not a constant. Model loading needs a longer window than token generation. Set WatchdogSec to 2–3× your longest legitimate blocking operation, and ping from a goroutine that isn't blocked by that operation.

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →