Commitd5832266Recorded29 Jun 2026Repositorycourier

Heartbeat before each poll so a slow poll doesn't false-trip the watchdog

Message

The isolated Telegram poller child emitted its liveness heartbeat only between polls (poller-sleep/heartbeat runs after tg-bot-tick returns). A single poll that blocks longer than the watchdog stale threshold (getUpdates runs over a blocking native TLS read with no timeout) thus left the child silent past the window and the watchdog killed it though it was alive and mid-request -- the recurring kill right after a fresh start, where the first poll's cold TLS handshake is slowest.

Emit a heartbeat immediately BEFORE each poll so the watchdog window resets at the start of the blocking call, giving every poll the full stale threshold to return. A literal heartbeat during the poll is impossible: the child is a single-threaded no-scheduler process and a blocking native read freezes every goroutine (the reason polling is process-isolated). A poll that truly exceeds the threshold is still a genuine wedge and is still killed + respawned.

Changed
 src/courier/poller.sgl | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)
Diff
src/courier/poller.sglmodified
@@ -194,6 +194,23 @@
194
(log-info "Telegram poller child started" pid: (process-id))
195
196
(let loop ((backoff *error-backoff-initial*))
+197
;; Heartbeat IMMEDIATELY BEFORE the (blocking) poll so the
+198
;; supervisor's liveness window resets right at the start of
+199
;; the risky call. tg-bot-tick runs over a blocking native
+200
;; TLS read with no timeout; heartbeating only *between*
+201
;; polls (poller-sleep/heartbeat, below) lets a single slow
+202
;; poll straddle the stale threshold, so the watchdog
+203
;; false-positive-kills a child that is alive and mid-request
+204
;; -- the fresh-start kill this fix targets, where the first
+205
;; poll (cold TLS handshake/DNS) is slowest. We cannot
+206
;; heartbeat *during* the poll: a blocking native freezes the
+207
;; child's scheduler (the reason polling is process-isolated
+208
;; at all), so the pre-poll heartbeat is the strongest
+209
;; available decoupling -- it gives each poll the full stale
+210
;; threshold to return before it looks wedged.
+211
(poller-emit-line! *heartbeat-line*)
+212
(when (poller-parent-gone?)
+213
(exit 0))
214
(let ((ok? (guard (e (else
215
(log-error (format "Poll loop error: ~a" e))
216
#f))