Decouple cron-ticker liveness from tick execution (gateway heartbeat thread) - #39720
Closed
tjkang wants to merge 1 commit into
Closed
Decouple cron-ticker liveness from tick execution (gateway heartbeat thread)#39720tjkang wants to merge 1 commit into
tjkang wants to merge 1 commit into
Conversation
A long-running sequential cron job monopolises the single cron-ticker thread and holds the .tick.lock flock for its whole duration, freezing the lock's mtime. External liveness monitors keyed on that mtime then false-alarm "cron stopped" even though the gateway is healthy. Add a dedicated daemon heartbeat thread that bumps a separate <HERMES_HOME>/cron/.gateway.heartbeat file every 30s, gated on an event-loop liveness probe so a genuinely hung loop still trips the monitor. tick() locking and the sequential/parallel job partition are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tjkang
force-pushed
the
heartbeat-decoupling
branch
from
June 11, 2026 01:46
2f5d4c2 to
3ee2a30
Compare
9 tasks
Contributor
|
Thanks for the careful separation of tick progress from gateway liveness. This is now redundant on current
Automated hermes-sweeper review. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
cron/scheduler.py::tick()runs on a single in-process background thread(
gateway/run.py::_start_cron_ticker, 60s interval). It acquiresfcntl.flock(LOCK_EX)on<HERMES_HOME>/cron/.tick.lockand holds it for theentire tick, and
tick()runsworkdir/profilejobs sequentially(intended invariant — those jobs mutate process-global
_hermes_home/os.environ).When a sequential job is long-running (e.g. a multi-minute browser+LLM pipeline),
the ticker thread is monopolised for that whole duration. Consequences:
LOCK_NBand returns early →.tick.lockmtime freezes..tick.lockmtime false-alarms"cron stopped" even though the gateway is perfectly healthy and the job is
progressing normally.
The freeze is not a throughput bug (long sequential execution is by design).
The only real defect is that the liveness signal is coupled to the work thread.
Fix
A dedicated daemon thread bumps a separate
<HERMES_HOME>/cron/.gateway.heartbeatfile mtime every 30s, independent of
tick():cron/scheduler.py: add_get_heartbeat_path()andheartbeat_loop(stop_event, alive_check, interval=30).The bump is a path-based
touch+os.utime— unaffected by theflockheldon the different
.tick.lockinode, so it stays fresh mid-tick.gateway/run.py: start the heartbeat thread next to the cron ticker and stopit on shutdown. The bump is gated on
_gateway_alive(), which actively probesthe event loop with
loop.call_soon_threadsafe(event.set)+event.wait(timeout)— if the loop is genuinely hung the probe never returns, the heartbeat stops,
and the monitor correctly fires.
.tick.lockkeeps its original meaning ("a tick actually ran"), so tick deadlocksremain detectable;
.gateway.heartbeatanswers the distinct question "is thegateway event loop alive?".
Why a separate file (not reusing
.tick.lock)Reusing
.tick.lockfor the heartbeat would conflate two signals — a healthyevent loop with a stuck/deadlocked cron tick would then be indistinguishable.
A distinct file preserves both diagnostics.
Invariants preserved
tick()lock acquisition/release and the sequential/parallel job partition areunchanged (byte-for-byte).
_gateway_alive()gate is ahard requirement, otherwise an always-on heartbeat would permanently mask real
outages from the health monitor.
Tests
Unit test covers: heartbeat stays fresh while a long job holds the tick lock;
alive_check()==False⇒ no bump (hung-loop gate); prompt exit onstop_event;path-based auto-create.
Notes for adopters
External monitors should treat
.gateway.heartbeatas the gateway-livenesssignal and
.tick.lockas the tick-progress signal: a stale.tick.lockwith afresh
.gateway.heartbeatmeans a long job is in progress (no alarm).