Replies: 2 comments 1 reply
|
Following up on my own question — I went and did the reading I should have done before posting, so I'll narrow this down. The standard answer here is a dead man's switch / heartbeat: the job pings an external service on each run, and the service alerts on silence rather than on errors. Healthchecks.io, Dead Man's Snitch and similar all work this way, and there's a lot of prior discussion of the pattern. That solves two of my three problems outright:
So those two are answered, and I should have found that before asking. What I don't think heartbeats solve, and what I'd still like to hear about: A healthy no-op pings exactly like a real run does. My jobs gate themselves ("if this period's entry already exists, exit"), so most executions correctly do nothing — and they still exit 0, so they still ping. That means a job which has silently stopped doing its actual work, but is still being invoked and still exiting cleanly, keeps the switch happy indefinitely. Silence-based alerting can't see it, because there is no silence. The best idea I have so far is to stop treating the ping as a boolean. Instead of "I ran", send "I ran, and here is what I decided, and here is why I did nothing" — then alert not only on silence but on the wrong shape of activity: N consecutive no-ops for a job that should have produced something by now, or a run whose declared outcome doesn't match the artifact that should have appeared. That turns it from liveness monitoring into something closer to an expectation check. I haven't found prior art for that second part, which is really what I was asking about. If anyone is running something like "assert that this job's output changed this period, not just that the job executed", I'd like to hear how you express the expectation without hand-maintaining a rule per job. |
|
A no-op doesn't have to ping success. Healthchecks has a Then set the check's period to how often the job is supposed to produce something (ie 1 month for the monthly job), not how often the scheduler invokes it. A job that's still being invoked and still exiting 0 but has silently stopped producing then only ever sends
The only per-job config is period + grace on the check plus the artifact path and period format in your ledger row. PERIOD=$(date +%Y-%m) # format from the ledger row
had_entry=$(grep -c "^## $PERIOD" "$ARTIFACT" || true)
run_job
if ! grep -q "^## $PERIOD" "$ARTIFACT"; then
curl -fsS -m 10 --retry 3 --data-raw "$PERIOD: unsettled" "$PING_URL/log"
elif [ "$had_entry" -eq 0 ]; then
curl -fsS -m 10 --retry 3 --data-raw "$PERIOD: produced" "$PING_URL"
touch "stamps/$JOB-$PERIOD"
else
curl -fsS -m 10 --retry 3 --data-raw "$PERIOD: no-op" "$PING_URL/log"
fiThe stamp comes from the artifact so a silent failure leaves the period unsettled and the next invocation retries it; invoke the monthly job daily and you get ~30 chances to self-correct instead of 1. However this could false alarm for any job that can go a full period with nothing to produce... And |
Uh oh!
There was an error while loading. Please reload this page.
I run Claude unattended on a schedule — in my case from a local scheduler that shells out to
claude -p, but I think anyone driving this from aschedule:trigger in Actions ends up with the same problem, so I'm asking here.Each of my jobs starts with a gate step: "if this period's entry already exists in the log, exit immediately." A no-op is therefore a normal, frequent, healthy outcome — most runs of a weekly job correctly do nothing.
The problem is that a healthy no-op and a job that never fired look identical from the outside. Both leave me with no new output and either exit 0 or no process at all. I only discover something has been dead when a deliverable someone was waiting on fails to appear.
This has now bitten me three times, always the same shape: a job that had no entry of its own in the schedule config, and instead "rode along" inside another job's instructions. It silently stopped firing when that host job was edited, and nothing anywhere recorded the absence. The most recent one had never fired at all, and I only caught it because I happened to be reading the config for an unrelated reason.
What I have now:
Where I'm still stuck:
For people running this unattended over months rather than days: what actually catches "this never ran"?
I'm not looking for a better scheduler — the scheduling part works fine. It's the noticing I haven't solved.
All reactions