fix(poller): drain stale backlogs, and stop reporting health that isn't (v0.23.19-beta)
v0.23.17 stopped the poller creating an unbounded backlog but did nothing about
the one already there. A reporter with sixty devices upgraded and still had
752,318 jobs queued, nine stale devices, and an endpoint cheerfully reporting
1.59x headroom. Fair to call that not fixed.
Three faults, all mine.
**Retention never touched pending work.** cleanupJobHistory cleaned 'completed'
and 'failed' only. The queue of outstanding polls — the part the operator
actually feels — was out of scope, so an upgrade left it draining for days.
Stale pending jobs are now swept, at startup and every five minutes. The rule is
that a periodic poll which has waited longer than its own cadence is worthless:
running it yields a reading the next cycle would have taken anyway, while
occupying a worker the live fleet is waiting on. Discarding queued work is safe
precisely because it is periodic — the schedule re-enqueues within one interval.
**Deduplication failed in the case it existed for.** The in-flight claim was a
Redis key with a 135-second TTL. Under a large backlog a job waits hours, so the
claim lapsed long before it ran and the scheduler re-enqueued every cycle: the
guard worked only on a queue that was already short.
Replaced with a deterministic job id, which cannot lapse. While a job for a
device and kind exists in any state, adding it again is a no-op. That requires
finished jobs to be removed immediately or the id would stay taken and the
device would never be polled again — which costs nothing now that
device_poll_stats records every attempt, duration and error.
**The headroom metric lied.** It describes flow and is blind to accumulated
work, so it read "healthy" beside three-quarters of a million queued jobs. It
now reports backlog and drain ETA alongside, plus a single status verdict that
cannot say ok while a backlog exists. Verified by injecting 15,125 jobs: status
went to 'draining' with headroom still at 85x, which is exactly the case the old
metric got wrong.
Also added, because the reporter asked and was right to: an operator-triggered
drain (POST /api/system/poller/drain), and a Polling tab in Settings showing
queue depth, headroom, backlog, per-device staleness and the drain control. It
was all buried in an API response before. A dashboard insight fires when the
poller falls behind, so a starved poller announces itself rather than presenting
as a fleet of broken devices.
Two corrections found by testing rather than reasoning:
* BullMQ rejects ':' in a custom job id. `fast:2` threw on every enqueue and
killed the scheduler outright — three of four devices went unpolled for
eight minutes before it was caught. Hyphens now, and the dedup is exercised
directly: 500 enqueues of one device yield one queued job.
* Redis maxmemory-policy moves from volatile-lru to noeviction. I had reasoned
that only TTL-bearing keys are evicted and BullMQ job hashes carry none —
true, but its *control* keys do. Job locks and stalled-check markers both
expire, and evicting one causes duplicate processing or a stalled-job storm.
BullMQ warns about this on connect and was right.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
deb71363c9
commit
745a7cda93
+12
-7
@@ -85,16 +85,21 @@ services:
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
# A memory ceiling so a runaway producer can never take the host down. The
|
||||
# eviction policy is deliberately volatile-lru rather than allkeys-*: BullMQ
|
||||
# job data carries no TTL and must never be evicted (that would silently
|
||||
# destroy queued work), while the poller's caches and in-flight claims all
|
||||
# set one and are safe to drop. If pressure ever exhausts the volatile keys
|
||||
# Redis refuses writes instead of evicting queue state — loud, and survivable.
|
||||
# A memory ceiling so a runaway producer can never take the host down, with
|
||||
# eviction disabled.
|
||||
#
|
||||
# volatile-lru looks tempting — it would evict only keys with an expiry, and
|
||||
# BullMQ's job hashes have none. But its *control* keys do: job locks and
|
||||
# per-queue stalled-check markers both carry TTLs, and losing one causes
|
||||
# duplicate processing or a stalled-job storm. BullMQ warns about this on
|
||||
# connect, and it is right. noeviction means Redis refuses writes when full
|
||||
# rather than quietly corrupting queue state: the host stays up, and the
|
||||
# failure is loud. Retention limits and the stale-pending sweep are what keep
|
||||
# us away from the ceiling in the first place.
|
||||
command: >
|
||||
redis-server --appendonly yes
|
||||
--maxmemory ${REDIS_MAXMEMORY:-512mb}
|
||||
--maxmemory-policy volatile-lru
|
||||
--maxmemory-policy noeviction
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
restart: unless-stopped
|
||||
|
||||
Reference in New Issue
Block a user