Skip to content

perf(retrymq): cut idle Redis polling to one command per interval - #1026

Open
alexluong wants to merge 3 commits into
mainfrom
feat/retrymq-idle-backoff
Open

perf(retrymq): cut idle Redis polling to one command per interval#1026
alexluong wants to merge 3 commits into
mainfrom
feat/retrymq-idle-backoff

Conversation

@alexluong

Copy link
Copy Markdown
Collaborator

Implements #1014. Makes the rsmq receiveMessage script self-contained — it reads vt from the :Q hash and calls TIME itself, dropping the MULTI/HMGET/TIME/EXEC that preceded every receive (5 commands / 2 RTTs → 1 / 1). When nothing is due the script also returns the time until the zset's earliest score, so the monitor sleeps until the next message comes due instead of every 100ms. At the new default that's 129.6M → 86.4K commands per month, per monitor.

RETRY_POLL_BACKOFF_MS keeps its meaning — how long the monitor waits when idle — and is redefined as the maximum idle sleep, default 10030000. For any fixed value the new sleep is min(nextDue − now, X) ≤ the old flat X, so existing values only improve. The effective cap is additionally clamped to the shortest configured retry delay (GetRetryPollBackoff), which makes added retry latency zero by construction for every config, including a custom retry_schedule with sub-30s entries.

Heads-up — the consecutive-error ladder no longer derives from pollBackoff: it now uses an internal 100ms base, so the ~1 minute of transient-infra tolerance documented at scheduler.go holds however the idle interval is configured. Previously raising the poll interval silently stretched (and lowering it shrank) that window.

go test ./internal/rsmq/ ./internal/scheduler/ ./internal/config/   # Dragonfly
TESTCOMPAT=1 go test ./internal/rsmq/                               # + real Redis

🤖 Generated with Claude Code

The retry monitor polled every 100ms, and each poll cost 5 client-observable
commands over 2 round trips: a MULTI/HMGET/TIME/EXEC to fetch the queue's
vt/delay/maxsize and the server clock, then the EVALSHA that used them. The
cost was per monitor instance and independent of traffic, so an empty queue
cost as much as a busy one and total load scaled with replica count.

Make receiveMessage self-contained — it reads vt from the :Q hash and calls
TIME itself, which upstream RSMQ could not do under pre-Redis-5 verbatim
script replication. When nothing is due it also returns the time until the
zset's earliest score, so the monitor sleeps until the next message comes due
instead of a flat interval. The zset holds both not-yet-due retries and
in-flight messages hidden by vt, so that score is the correct wake time in
either case, and every sleep is still capped.

RETRY_POLL_BACKOFF_MS is redefined as that cap, default 100 -> 30000, and the
effective value is clamped to the shortest configured retry delay so the idle
interval can never make a retry late. The consecutive-error backoff ladder no
longer derives from it, keeping the ~1 minute of transient-infra tolerance
fixed. At a 30s cap: 129.6M -> 86.4K commands per month, per monitor.

Closes #1014

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@alexbouchardd

Copy link
Copy Markdown
Contributor

See comment on issue regarding next message timestamp

alexluong and others added 2 commits August 10, 2026 20:11
…ew tests

- IdleSleepWakesOnDueMessage: wait for the Monitor goroutine to exit
  after cancel so it cannot log via t after the test completes, and
  assert the execution window as an elapsed range with a looser upper
  bound (3s) for loaded CI.
- MonitorRetriesTransientErrors: guard the msgs slice with a lock
  (msgLog helper) — exec runs on the monitor's goroutines, so the
  require.Eventually read raced with appends under -race.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… tests

The scheduler has always run exec on the monitor's goroutines, so the
unsynchronized msgs slices in TestScheduler_Basic, ParallelMonitor,
VisibilityTimeout, CustomID, and Cancel raced with test-side reads.
These races predate this branch (reproduced on main) but fail the
package under -race. Reuse the msgLog helper everywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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