Skip to content

feat: Back off big segment synchronization on a rejected SDK key - #881

Merged
keelerm84 merged 1 commit into
v9from
mk/SDK-3157/big-segment-retry
Sep 21, 2026
Merged

keelerm84 merged 1 commit into
v9from
mk/SDK-3157/big-segment-retry

Conversation

@keelerm84

@keelerm84 keelerm84 commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

Summary

The big segment synchronizer retried on a flat 10s interval and intended to stop on an unrecoverable HTTP status. It never stopped. poll and connectStream both return &httpStatusError{...}, while syncSupervisor type-asserted for a value-typed httpStatusError, so isHTTPErrorRecoverable was 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:

  • A transient failure stays on a short curve: 10s base, 30s ceiling.
  • A failure unlikely to correct itself soon raises the ceiling to an hour, starting from a 5m base. That bounds how much load a fleet of Relay Proxy instances puts on a service that is rejecting all of its requests. Big segment data is reported as potentially stale long before that, so a long wait stays visible rather than silent.

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/retry package rather than in bigsegments, 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 v9 to parity with v8 #857 and #873. internal/retry is copied across unmodified -- it imports only math, math/rand and time.

TestSyncKeepsRetryingAfterUnauthorized is 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/retry strategy: 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.As on *httpStatusError (fixing a bug where a value-type assertion meant unrecoverable statuses never triggered the old stop/recover logic) and routes statuses through retry.ClassifyHTTPStatus. Transport/TLS errors stay on the normal curve; unexpected failures log at error and emit an info line when extended backoff engages. setSynced calls OnHealthy() so sustained successful store marks reset backoff after retryResetThreshold.

Also fixes a timer leak in syncSupervisor (removed defer 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.

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.
@keelerm84
keelerm84 marked this pull request as ready for review September 18, 2026 20:58
@keelerm84
keelerm84 requested a review from a team as a code owner September 18, 2026 20:58
@keelerm84
keelerm84 merged commit 4a2c737 into v9 Sep 21, 2026
18 checks passed
@keelerm84
keelerm84 deleted the mk/SDK-3157/big-segment-retry branch September 21, 2026 19:03
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