fix(discovery): jitter the refresh retry backoff - #616
Conversation
RetryRefresh slept exactly 2s * 2^n capped at 60s, so instances retrying against one recovering ClickHouse fired in lockstep. Each sleep is now drawn uniformly below the backoff (full jitter), via an injectable retryDelay so the tests pin the backoff sequence without wall-clock sleeps. Closes #141. Part of #613. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
📚 Docs preview is live → https://00bdb03b-wavehouse-docs.wave-rf.workers.dev
|
Code Coverage OverviewLanguages: Go GoThe overall line coverage in commit f5d5e04 in the Show a line coverage summary of the most impacted files.
Updated |
…itter # Conflicts: # CHANGELOG.md
Brings in #647, #632, #616 and #655. One conflict, in config.go: main dropped the Cache struct's env-default tag while this branch moved Cache into backends.go; kept the move. #632 removed every env-default tag. The four *.backend fields and cache.l1_max_cost this branch declares in backends.go still carried theirs, so an explicit `backend: ""` would have become the default. Their defaults now live in defaults(); an empty backend, which Validate refuses, is pinned next to server.port's zero as a refusal; the docs test converts a documented default to a named string type such as MQBackend. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in main via feat/coord-leases: #618's squash, #619, #632, #616, #655 and #647. Per #632, the roles default moves from its env-default tag into defaults(): an explicit `roles: []` now reaches Validate (a refusedZeros entry pins it), and the doc-defaults test parses the roles cell as a comma-separated list. instance_id's documented default is *(empty)*, the value in defaults(); Load resolves it to <hostname>-<8 hex>. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
main now carries #612 and #618 as squashes, plus #632, #622, #627, #615, #619, #647, #616, #655 and #623. The merge was resolved against the pre-squash #618 head (f129d57) as its base, so main's version wins for everything this stack does not own and only the cache stack's changes (#614, #621, #626 as merged here, and this PR) are re-applied on top. Warnings keeps main's api-role gate for the cache.redis warnings too: a split's Deployments differ only in roles, so the API's cover the others'. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017aS7rLrH1RKkUMem7X4ckd
Closes #141. Part of #613.
What
SchemaRegistry.RetryRefreshused to sleep exactly2s * 2^n, capped at 60s. That means N instances retrying against one recovering ClickHouse fire in lockstep once they all reach the cap. With this change, each sleep is a uniform draw from[0, backoff)(full jitter). The bound still doubles from 2s to 60s.Why full jitter instead of the ±10% the issue proposed: at the 60s cap, ±10% spreads the retries over only 12s. Full jitter spreads them over the whole 60s window. That makes it the strongest way to break lockstep without state or coordination (AWS's "Exponential Backoff and Jitter" analysis). One side effect: the mean wait halves. So during an outage, a failing tenant's retries, its log lines and
wavehouse_schema_refresh_failures_totalcome about twice as often. Nothing in this repo consumes that counter yet (it was new in #610). The CHANGELOG entry mentions this.The random source is an injectable
retryDelay func(time.Duration) time.Durationfield (rand.Nby default), set up the same way as the existingfirstTick. It adds no dependency.Tests
TestRetryRefresh_BackoffIsBoundednow records the backoff each sleep is drawn within (1, 2, 4, 4, 4 ms) instead of timing the wall clock, so it can't flake.TestRetryRefresh_SleepsTheJitteredDelay: the loop sleeps the drawn delay, not the backoff.TestRetryRefresh_DelayIsSpreadOverTheBackoff: 200 draws from the productionretryDelayall fall in[0, backoff), reach both the bottom and top quarters, and are nearly all distinct.Left for later
RetryRefreshexponential backoff for clustered mode #141 criterion "confirm the jitter range once clustered mode has a topology config" is conditional and stays with that work.internal/auth(the JWKS retry) has its own unjittered loop, andfeat/ch-error-classesis adding a ClickHouse backoff ininternal/ingest. Consolidating them is a follow-up.🤖 Generated with Claude Code
https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL