Skip to content

Wake the analyser on demand instead of polling the website - #39

Draft
kasnder wants to merge 1 commit into
mainfrom
claude/analyser-push-wake
Draft

kasnder wants to merge 1 commit into
mainfrom
claude/analyser-push-wake

Conversation

@kasnder

@kasnder kasnder commented Oct 20, 2026

Copy link
Copy Markdown
Member

Replaces #38 (polling back-off).

Why

The analyser called /ping and /queue about every 30 seconds. Railway only sleeps a service after 5–10 minutes without outbound traffic, and a request to the website also reaches Postgres, so neither ever slept. With this change, the analyser only contacts the website when there is work.

What

  • /queue says when the next app falls due. With the queue empty, it sets X-Analysis-Next-Due-Seconds: the earliest time an app becomes claimable without being queued (a stale analysis, a retryable failure, an abandoned claim). Apps.nextDueAt works this out with the same conditions as nextApp. If nothing will ever fall due, the header is absent.
  • Queueing an app wakes the analyser. The request form and scripts/queue-refetch.js --apply post a bare wake to an ntfy topic (lib/analyserWake.js). This is best effort, with a 3 s timeout, and never throws. An idle analyser subscribes to the topic with curl and resumes on the first message. since= replays a wake-up sent between the /queue check and the subscription.
    • NTFY_TOPIC must be set on both sides; without it everything is inert.
    • NTFY_TOKEN is optional, so publishing isn't subject to ntfy.sh's per-IP limits for anonymous use.
    • NTFY_SERVER allows self-hosting.
  • The analyser waits instead of polling (wait_for_work in processQueue.sh). It waits until the next due time, a wake-up or MAX_IDLE_WAIT (default 24 h), whichever comes first.
    • The wait is at least MIN_IDLE_WAIT (60 s). If ntfy is unreachable, the analyser still sleeps out the full wait.
    • A failed /queue call is retried after QUEUE_RETRY_WAIT (300 s).
    • Every curl call retries transient failures (including Railway's cold-start 502) for up to 60 s.
  • "Analyser online" comes from /queue. Each /queue call records a check-in in a new one-row analyser_heartbeat table (migration 015). A woken web process reads it back once. /ping is removed, and the online window widens from 1 h to 26 h: the daily check-in plus one analysis.

Trade-off: if a wake-up is lost (ntfy down, rate-limited or not configured), a requested app waits until the next due time or until the daily check.

Rollout

  1. Pick a topic, for example with openssl rand -hex 16. Set it as NTFY_TOPIC on the Railway web service and in analyser/.env. Optionally set NTFY_TOKEN too.
  2. Deploy the website. Migration 015 runs on start.
  3. Update processQueue.sh on the Pi and restart it. Until then, the old script's /ping calls get 404s, and its /queue calls still work and count as check-ins.
  4. Make sure Railway's App Sleeping is enabled for the web service (and Postgres, if wanted).

Testing

  • npm test: 173/173 pass. New tests:
    • test/analyserWake.test.js: topic validation, default and self-hosted servers, the token header, failures never throwing.
    • test/analyserQueue.test.js: the due header (future, overdue, never, absent once claimed), heartbeat writes and the stored heartbeat being read once, /ping returning 404, and the request form posting the wake-up.
  • Ran nextDueAt against Postgres 16 with all migrations applied. It returned null on an empty table and found the earliest of a stale analysis, a retryable failure (non-retryable ignored) and an old-version abandoned claim. It agreed with nextApp: overdue exactly when nextApp claimed, then the new claim's timeout.
  • Ran wait_for_work against a fake ntfy stream sending blank keepalive lines:
    • woken after 2 s by a message;
    • returned at the 3 s due time with no message;
    • slept the full wait with ntfy unreachable;
    • clamped to MIN_IDLE_WAIT without a topic;
    • left no curl process behind.
  • Not tested against the live ntfy.sh: the sandbox's shared IP got HTTP 429 on publishing, which is also why NTFY_TOKEN exists.
  • npm audit --omit=dev: 0 vulnerabilities.

https://claude.ai/code/session_01Fi3H98SycMK13rrr8ySSU1


Generated by Claude Code

The analyser called /ping and /queue every 30 seconds, so the website,
and through it the database, never went quiet long enough for Railway to
sleep them. The analyser now waits while the queue is empty and only
contacts the website when there is work:

- /queue reports, via X-Analysis-Next-Due-Seconds, when the next app
  falls due on its own (a stale analysis, a retryable failure, an
  abandoned claim), from a query mirroring nextApp's conditions.
- Queueing an app (the request form or queue-refetch) posts a bare
  "wake" to an ntfy topic (NTFY_TOPIC, optionally NTFY_TOKEN), which the
  idle analyser subscribes to. A lost wake-up only delays the analysis.
- The analyser asks at least daily (MAX_IDLE_WAIT), and every /queue
  call records a check-in, stored in a new analyser_heartbeat row so a
  woken web process still knows the analyser is online. The separate
  /ping endpoint is removed and the online window widens to 26 hours.
- curl retries transient failures for up to a minute, so requests
  survive the website waking up.

Claude-Session: https://claude.ai/code/session_01Fi3H98SycMK13rrr8ySSU1

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants