feat: Back off big segment synchronization on a rejected SDK key - #881
Merged
Merged
Conversation
The synchronizer retried on a flat 10s interval and intended to stop on an unrecoverable HTTP status. It never stopped: poll and connectStream return *httpStatusError, while syncSupervisor type-asserted for a value-typed httpStatusError, so isHTTPErrorRecoverable was never consulted. A rejected SDK key therefore produced an unbounded 10s hot loop with no error-level log. Stopping would be wrong anyway, because an operator can make a key valid again without Relay knowing. The synchronizer now keeps retrying in every case and instead sorts each failure into one of two classes. A transient failure stays on a short curve. A failure unlikely to correct itself soon raises the ceiling to an hour, which bounds how much load a fleet of Relay Proxy instances puts on a service that is rejecting every request. Only an HTTP status can be unexpected. Every transport-level failure, certificate validation included, is treated as normal: it either resolves without the synchronizer's involvement, as a network problem does, or it resolves the moment an operator fixes it, and waiting minutes helps in neither case. Marking the store as synchronized is the health signal, not a successful HTTP response, because the store is what readers depend on. Enough consecutive marks return the retry state to the short curve. The new internal/retry package holds the classification and the curve so the auto-configuration stream can bind the same behavior to its own timings. Incidentally fixes a timer leak: the old "defer timer.Stop()" sat inside the retry loop, so it accumulated one undrained timer per retry for the life of the goroutine.
kinyoklion
approved these changes
Sep 21, 2026
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.
Summary
The big segment synchronizer retried on a flat 10s interval and intended to stop on an unrecoverable HTTP status. It never stopped.
pollandconnectStreamboth return&httpStatusError{...}, whilesyncSupervisortype-asserted for a value-typedhttpStatusError, soisHTTPErrorRecoverablewas never consulted. A rejected SDK key produced an unbounded 10s hot loop against a service refusing every request, with no error-level log to say so.Stopping would have been the wrong fix. An operator can make a key valid again without Relay knowing, so the synchronizer now keeps retrying in every case and sorts each failure into one of two classes instead:
Only an HTTP status can be unexpected, and among 4xx only 400, 408 and 429 are treated as transient. Every transport-level failure, certificate validation included, is normal: it either resolves without the synchronizer's involvement, as a network problem does, or it resolves the moment an operator fixes it, and waiting minutes helps in neither case.
Marking the store as synchronized is the health signal, not a successful HTTP response, because the store is what readers depend on. Enough consecutive marks return the retry state to the short curve, so a key that gets fixed does not leave the process stuck on the hour-long ceiling.
internal/retry
The classification and the curve live in a new
internal/retrypackage rather than inbigsegments, because the auto-configuration stream needs the same behavior bound to its own timings. That is the next ticket, and it is why this is a package and not a couple of methods on the synchronizer.Also in here
A timer leak. The old
defer timer.Stop()sat inside the retry loop, so it accumulated one undrained timer per retry for the life of the goroutine.Relationship to v8
This brings
v9to parity withv8#857 and #873.internal/retryis copied across unmodified -- it imports onlymath,math/randandtime.TestSyncKeepsRetryingAfterUnauthorizedis the regression test for the dead type assertion, and its comment records that history so the next reader does not have to rediscover it.Note
Overview
Replaces the big segment synchronizer’s fixed 10s retry with a shared
internal/retrystrategy: transient failures use a short exponential curve (10s base, 30s cap), while “unexpected” HTTP failures—mainly rejected credentials like 401—switch to a much slower curve (5m base, 1h cap) while still retrying (operators can fix keys without restarting Relay).Failure handling now uses
errors.Ason*httpStatusError(fixing a bug where a value-type assertion meant unrecoverable statuses never triggered the old stop/recover logic) and routes statuses throughretry.ClassifyHTTPStatus. Transport/TLS errors stay on the normal curve; unexpected failures log at error and emit an info line when extended backoff engages.setSyncedcallsOnHealthy()so sustained successful store marks reset backoff afterretryResetThreshold.Also fixes a timer leak in
syncSupervisor(removeddefer timer.Stop()inside the retry loop) and adds broad unit/integration tests for 401 persistence, 503 vs extended mode, health reset, stream-end doubling, and cert failures.Reviewed by Cursor Bugbot for commit 2370b36. Bugbot is set up for automated code reviews on this repo. Configure here.