Repository navigation
Conversation
This was referenced Oct 18, 2026
This branch has not been deployed
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.
Keep the retry scheduler steady when many retries come due at once: bounded concurrency, jittered backoff, and limits that can be configured.
Problem
A burst of failed deliveries schedules thousands of retries that come due within seconds. Three things in the scheduler make that worse:
Monitorstarts one goroutine per received message (go s.exec(...), no limit). In a local load test with ~6,000 retries due within 20 s, 3 instances reached ~3,400 goroutines in total, ~3,000 of them waiting for a ClickHouse connection (10 per instance). The client's pool wait is bounded by the dial timeout (10 s), so most lookups failed withacquire conn timeoutwithout reaching ClickHouse.Change
RETRY_MAX_CONCURRENCY(default100per process,0= unbounded). The monitor takes a slot before it receives, so it never holds a message it can't run; the rest stay in the queue.RETRY_MAX_RECEIVE_COUNT(default5, today's value; minimum1).CLICKHOUSE_MAX_OPEN_CONNS(default0= the client's default of 10) andCLICKHOUSE_DIAL_TIMEOUT_SECONDS(default10). clickhouse-go uses the dial timeout for the pool wait as well, so this is also the acquire timeout.Each new option is one row in the configuration reference.
Behavior changes
Results
Local reproduction, 18,000 events in 60 s, destination failing for the first 20 s, run on this branch (includes the receipt fix from the base PR; the retry lookup is still the unbounded one from
main):mainWith the unbounded lookup ClickHouse still does ~0.3 s of CPU per retry, so it stays at its limit until the backlog drains, and the remaining pool timeouts come from 300 retries in flight (3 instances x 100) sharing 30 connections. The time-bounded lookup (#1152) removes that cost: the stack with #1152 and #1154 has no pool timeouts and delivers all events in ~5 min.
How the numbers were measured
Local reproduction: ClickHouse 25.8 limited to 3 vCPU with 8 M rows in the attempts table (30 days, ~90 KB average payload), 3 Outpost instances (all services in each, 2 CPUs, 2 GiB), Redis-compatible store for the retry queue, NATS as the internal queue. Retry schedule
1, 5, 300, 1800, ...seconds,LOG_BATCH_SIZE=50,LOG_BATCH_THRESHOLD_SECONDS=10,RETRY_VISIBILITY_TIMEOUT_SECONDS=30. Workload: 18,000 events (12 KB payload) in 60 s to one webhook destination that returns 5xx for the first 20 s, then 200 (about 6,000 failed first attempts, so about 6,000 retries due within ~20 s). Lookup cost fromsystem.query_log; ClickHouse CPU from the container's cgroup. One run per build; three runs of themainbaseline agreed within 3 % on pool timeouts and delivered events (lookup p50 duration 3.2-3.6 s).Tests
internal/scheduler/scheduler_load_test.go: a burst larger than the cap never runs more than the cap at once (fails with the cap disabled).internal/scheduler/backoff_test.go: jitter bounds, including at the maximum.internal/deliverymq/retry_dlq_test.go: malformed tasks dead-lettered on the first receive; log store errors still retried; max receive count honored.internal/config/config_retry_test.goandinternal/clickhouse/clickhouse_test.go: parsing, defaults, validation and pool option wiring.Not in this PR
🤖 Generated with Claude Code