feat: Keep retrying a rejected auto-configuration key - #866
Conversation
4a29aaa to
536413e
Compare
| initTimeout time.Duration | ||
| // ignoreConnectionErrors keeps Relay running with no configuration rather than reporting | ||
| // a failure. It is the ignoreConnectionErrors configuration option. | ||
| ignoreConnectionErrors bool |
There was a problem hiding this comment.
Is this a temporary option for backwards compatibility in the current major version?
| } | ||
| result.ActivateProfile = extendedProfile | ||
| select { | ||
| case authFailureCh <- struct{}{}: |
There was a problem hiding this comment.
This is concluding the error is an auth failure when classifyAndLogStreamError only says it is Unexpected. Does the authFailureCh chan need to be generalized to unexpectedErrorChan? Does the other logic using the authFailureCh remain valid?
An unexpected error that is not an auth failure includes various TLS errors as well as any future errors that get added to the category.
536413e to
99d1252
Compare
99d1252 to
c577386
Compare
c577386 to
95f76b4
Compare
7380863 to
fc8659e
Compare
95f76b4 to
11ed0c6
Compare
2598562 to
18bcac3
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 18bcac3. Configure here.
A 401 or 403 on the auto-configuration stream stopped the stream and exited the process. Recovery took an operator restarting every instance, and a persistent store holding a usable configuration was thrown away. The stream now keeps retrying, which is what it already did for every other failure to reach LaunchDarkly. A 404, a 5xx, a network error and a certificate problem all left Relay running and trying; a rejected key was the only failure treated as permanent, and it was the one an operator was most likely to fix. A failure unlikely to correct itself soon activates a second eventsource retry profile, 5 minutes growing to a one-hour ceiling, so a fleet does not add load to a service that is rejecting every request. Failures that were already retried keep the delays they had, so an outage behaves exactly as before. Two consequences follow. The status is INTERRUPTED rather than OFF, because the stream is still trying; OFF now means only that Close was called. And the stream runs on a cancellable context, because Close waits on the subscribe goroutine, which cannot exit during a backoff wait that now reaches an hour. With no configuration from any source Relay stays up and answers 503 until the key becomes valid. Use the status resource to detect a rejected key, which reports the code alongside the stream state.
d4aa838 to
54d05ee
Compare
🤖 I have created a release *beep* *boop* --- ## [8.22.0](v8.21.0...v8.22.0) (2026-09-16) ### Features * Adopt go-server-sdk v7.17.0 so an upstream 401 retries ([#856](#856)) ([e152af7](e152af7)) * Back off big segment synchronization on a rejected SDK key ([#857](#857)) ([6ede85b](6ede85b)) * Classify transport failures as normal, including certificate failures ([#873](#873)) ([c3b6850](c3b6850)) * Keep retrying a rejected auto-configuration key ([#866](#866)) ([4bf3bb9](4bf3bb9)) * Report the auto-configuration stream's health in the status resource ([#870](#870)) ([5c1e3ee](5c1e3ee)) ### Bug Fixes * **autoconfig:** refresh stored environment defaults on update (SEC-9484) ([#842](#842)) ([e04e3e2](e04e3e2)) * **deps:** bump golang.org/x/crypto to v0.55.0 for CVE-2026-56854 ([#848](#848)) ([360d624](360d624)) * **deps:** bump golang.org/x/crypto to v0.56.0 for CVE-2026-78662 and CVE-2026-56855 ([#854](#854)) ([6d5d346](6d5d346)) * **deps:** bump supported Go versions to 1.27.0 and 1.26.7 ([#837](#837)) ([9b4fb46](9b4fb46)) * **deps:** bump supported Go versions to 1.27.1 and 1.26.8 ([#852](#852)) ([4b06ed6](4b06ed6)) * emit Vary: Origin on CORS responses (SEC-9501) ([#844](#844)) ([78a8f05](78a8f05)) * Report an incomplete DynamoDB auto-config cache write ([#874](#874)) ([0964dc0](0964dc0)) * **security:** redact all credential-bearing URL components in status dbServer ([#846](#846)) ([20effc5](20effc5)) * **streams:** treat a nil replay result as no event instead of panicking ([#845](#845)) ([7d7eace](7d7eace)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

Summary
A
401or403on the auto-configuration stream stopped the stream and exited the process. A whole fleet did that at once, recovery took an operator restarting every instance, and a persistent store holding a usable configuration was thrown away.The stream now keeps retrying. That is what Relay already did for every other failure to reach LaunchDarkly: a
404from a misconfigured stream URI, any5xx, a network error, a certificate problem -- all of them left Relay running and trying indefinitely. A rejected key was the single failure treated as permanent, and it was the one an operator was most likely to fix.A failure that is unlikely to correct itself soon activates a second eventsource retry profile, 5 minutes growing to a one-hour ceiling, so a fleet does not add load to a service that is rejecting every request. Failures that were already retried keep the delays they had, so an outage takes a byte-for-byte identical path.
What happens with no configuration
Relay stays up and answers
503until the key becomes valid, exactly as it already did for a404or a5xxon the same stream. If a persistent store holds a configuration, Relay serves those environments while it retries -- which is the case that previously exited and discarded the cache.Detecting a bad key is now the status resource's job rather than the process exiting. #870 reports the stream state and the status code, and the log carries
Invalid auto-configuration key; will keep retrying in case it becomes valid.docs/proxy-mode.mdsays so, for anyone who relied on the exit.Two consequences in the code
The status is
INTERRUPTEDrather thanOFFfor a rejected key, because the stream is still trying.OFFnow means only thatClosewas called.The stream runs on a cancellable context.
Closewaits on the subscribe goroutine, and that goroutine cannot exit during a backoff wait -- eventsource's retry loop selects on the request context and the delay timer and nothing else. Without the cancel, closing a Relay whose key was rejected would block for the remainder of a delay that now reaches an hour. This was already a latent bug at the old 30-second ceiling; the longer delays made it matter.On the size of this change
An earlier revision of this PR was twice as large. It kept the old "exit when Relay cannot serve" behavior and made it conditional on the cache being reachable and empty, which needed a notification channel, three pieces of wait-loop state, a give-up predicate, a distinction between an unreadable cache and an empty one, a bounded cache read, and two new constructor parameters -- about 113 lines implementing when to exit rather than how to retry.
Once it was clear that Relay already stayed up forever for every other failure, all of that became unnecessary.
NewStreamManager's signature is unchanged, no configuration option changes meaning, andstream_manager.gois 124 added lines instead of 249.Tests
Both behaviors are pinned against their own mutation. Removing the profile activation fails
TestExtendedDelaysDoubleFromTheExtendedBase; restoringCloseNow: truefor a 401 fails the recovery and status tests.stream_manager_known_limits_test.gorecords one thing deliberately left alone: a server-directedretry:hint replaces the active profile's base delay and is never cleared, so it would flatten the extended delays. LaunchDarkly does not send one, so reaching it needs an on-path intermediary.Deliberately not addressed
The cache has no write timestamp, so
lastConfigReceivedreports when Relay read it rather than how stale it is. A partial DynamoDB snapshot is still read as authoritative; #874 makes the write report the loss, but detecting it on read needs a completeness marker. Event forwarding still latches off permanently on a401(SDK-3067). Relay's SDK-facing auth gate still keys on the SDK client's construction error rather than on data availability.docs/proxy-mode.md:33claims that in standard mode Relay "will give up and shut down completely" wheninitTimeoutelapses. It does not:waitForAllClientsis the only thing that reports that, andrelay.go:256is its only caller, insideif c.Main.ExitAlways. That predates this change, so it needs its own fix.Release note
Nothing here has shipped. The last release is 8.21.0, and the SDK bump (#856), big segment backoff (#857), status reporting (#870), transport classification (#873) and the DynamoDB write fix (#874) all sit above it. The whole retry story lands as one new thing, so the note should describe the end state rather than the increments.