feat(daemon): ping/sentinel socket health checks (replaces drain-progress watchdog)#1735
feat(daemon): ping/sentinel socket health checks (replaces drain-progress watchdog)#1735Siddhant-K-code wants to merge 1 commit into
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
CIs will get fixed after merge & rebase of #1734 |
|
Sounds good — I'll rebase onto One note: the only red check here is |
8c010b6 to
8afd7c6
Compare
This stack of pull requests is managed by Graphite. Learn more about stacking. |
8afd7c6 to
0df3c52
Compare
|
Pushed 593f3f4 Why The linked repro is Git trace2 back-pressure: Git writes trace2 frames synchronously to the daemon's Unix socket. With small socket buffers, a large trace burst can fill the buffer and block raw This matches the ~500-file-change case Sasha flagged: macOS-like ~8 KiB buffers make the issue easy to trigger, while Linux's larger defaults can mask it for moderate bursts. What changed
Important This is a mitigation, not a replacement for the watchdog. A larger buffer absorbs bursty trace output, but any finite buffer can still fill if the daemon stops draining. The watchdog remains the recovery path for real daemon-side stalls. Validated
|
|
Pushed This is safe for Windows: the trace daemon uses Windows named pipes there, not Unix sockets, and the |
|
Pushed The The helper itself is correct; only the assertion was too strict. It now passes if the buffer either reaches the 1 MiB target or grew beyond the socket's default baseline, which holds on both Linux (clamped) and macOS. Fast lane (ubuntu + macOS core, all lint/fmt) is green again. |
|
Current remaining failure is This does not look related to the Unix-only trace socket buffer change. It may be covered by #1739; worth rerunning this PR after that lands. |
2697c54 to
7c967ab
Compare
| if let Some(stall) = control_health.observe(control_ping_ok, now) { | ||
| tracing::warn!( | ||
| stall_secs = stall.as_secs(), | ||
| "daemon control socket stopped acknowledging health pings" | ||
| ); | ||
| } | ||
| if let Some(stall) = trace_health.observe(trace_ping_acked, now) { | ||
| tracing::warn!( | ||
| stall_secs = stall.as_secs(), | ||
| "daemon trace socket stopped acknowledging health pings" | ||
| ); | ||
| } |
There was a problem hiding this comment.
📝 Info: Ping stall detection is alert-only by design, does not trigger restart
The SocketPingHealth detector only logs a warning when a socket stops acknowledging pings (src/daemon.rs:7206-7217). It deliberately does not trigger a restart — only the connect-probe failures at src/daemon.rs:7224 do that. The PR description and code comments explain this is intentional to avoid memory spikes and restart loops. This means a wedged read path that still accepts new connections will only produce log warnings, not self-heal. This is a conscious trade-off documented in the function's doc comment at src/daemon.rs:7131-7138.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
This is intentional, as the PR description calls out. The ping-stall detectors are deliberately alert-only to avoid the restart loops / memory spikes that aggressive drain-based restarts risked.
Hard back-pressure is still self-healing: the connect-probe path just below (src/daemon.rs:7208-7231) continues to spawn_self_restart() + request_shutdown() when a socket stops accepting new connections — which is the failure mode that actually wedges git commands. The ping stalls cover a softer "reader/handler quiet but socket still accepts" case where a warn is the safer signal. Agreed it's worth revisiting if we see stall warnings in production without recovery, but keeping it log-only for now is the deliberate trade-off.
There was a problem hiding this comment.
havent dug into the details yet, but do these end up as daemon logs so that we can read them on our end?
There was a problem hiding this comment.
Yes, on both fronts:
- Local daemon log: these are
tracing::warn!events, and the daemon's subscriber writes to stderr (src/daemon.rs:7335-7346), which isdup2-redirected into the per-daemon log file (maybe_setup_daemon_log_file). That file lives at~/.git-ai/internal/daemon/logs/<pid>.log(seedaemon_log_file_path,src/daemon.rs:2115-2127), so they're readable on-box. The defaultEnvFilteris info-level, soWARNis always captured (noGIT_AI_DEBUG=1needed). - Server-side: they also flow through
DaemonLogUploadLayer(src/daemon/daemon_log_layer.rs), which capturesWARNevents (Level::WARN -> DaemonLogLevel::Warn) and best-effort uploads them for diagnostics whenever thedaemon_log_uploadflag is on (default true). So a sustained stall warning shows up on our end without needing the user to pull logs manually.
7c967ab to
abc7288
Compare
abc7288 to
4a829fc
Compare

Summary
Reworks the daemon socket health check from a trace-ingest drain watchdog (which self-restarted the daemon when ingest appeared stalled) into a liveness ping/sentinel mechanism that alerts instead of restarting. The watchdog's aggressive restarts risked memory spikes and restart loops; a heartbeat that surfaces the problem is simpler and less edge-case-prone.
Each health-check tick (every
DAEMON_SOCKET_HEALTH_CHECK_SECS, default 30s) the loop now sends a liveness ping over both sockets and tracks when each was last acknowledged:ControlRequest::Pingand observe the synchronous round-trip result.{"event":"git_ai_health_ping"}. The trace reader recognizes it inprocess_trace_connection_line, stampslast_trace_ping_received_ns, and closes the connection without registering a root or enqueuing it for ingest. The loop confirms the watermark advanced.A small
SocketPingHealthdetector decides when to alert:If a socket goes unacknowledged for
DAEMON_PING_STALL_SECS(default 120s, overrideGIT_AI_DAEMON_PING_STALL_SECS,0disables) the loop emits a singletracing::warn!for that episode. It does not restart.The pre-existing connect-probe restart safety net (dead/stale listener →
spawn_self_restart) is retained unchanged — the pings are strictly additive and catch a wedge on an already-established connection that a bareaccept()probe cannot.Because the ping is on the health-check loop, nothing is added to the latency-sensitive trace ingestion path.
Scope note
Socket receive-buffer tuning (raising
SO_RCVBUFto absorb trace2 bursts) is split into its own small PR (#1762) so it can land independently of this change.Tests (TDD)
socket_ping_health_alerts_once_per_stall_then_rearms— threshold, single alert per episode, re-arm after recovery.socket_ping_health_zero_threshold_never_alerts— disable switch.trace_health_ping_sentinel_is_recorded_and_not_ingested— sentinel stamps the watermark, closes the connection, registers no root, and never enters the ingest queue (realActorDaemonCoordinator).daemon_trace_health_ping_sentinel_is_accepted_without_disrupting_ingest— end-to-end: after a sentinel, the real daemon still ingests a trace command and answers a controlPing.Link to Devin session: https://app.devin.ai/sessions/a9931f84ae914fcc82663a2e52106bd5
Requested by: @Siddhant-K-code