Skip to content

feat: Keep retrying a rejected auto-configuration key - #866

Merged
keelerm84 merged 1 commit into
v8from
mk/SDK-3061/autoconfig-retry
Sep 16, 2026
Merged

keelerm84 merged 1 commit into
v8from
mk/SDK-3061/autoconfig-retry

Conversation

@keelerm84

@keelerm84 keelerm84 commented Sep 10, 2026

Copy link
Copy Markdown
Member

Summary

A 401 or 403 on 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 404 from a misconfigured stream URI, any 5xx, 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 503 until the key becomes valid, exactly as it already did for a 404 or a 5xx on 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.md says so, for anyone who relied on the exit.

Two consequences in the code

The status is INTERRUPTED rather than OFF for a rejected key, because the stream is still trying. OFF now means only that Close was called.

The stream runs on a cancellable context. Close waits 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, and stream_manager.go is 124 added lines instead of 249.

Tests

Both behaviors are pinned against their own mutation. Removing the profile activation fails TestExtendedDelaysDoubleFromTheExtendedBase; restoring CloseNow: true for a 401 fails the recovery and status tests.

stream_manager_known_limits_test.go records one thing deliberately left alone: a server-directed retry: 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 lastConfigReceived reports 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 a 401 (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:33 claims that in standard mode Relay "will give up and shut down completely" when initTimeout elapses. It does not: waitForAllClients is the only thing that reports that, and relay.go:256 is its only caller, inside if 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.

Base automatically changed from mk/SDK-3063/big-segment-retry to v8 September 14, 2026 18:28
@keelerm84
keelerm84 force-pushed the mk/SDK-3061/autoconfig-retry branch from 4a29aaa to 536413e Compare September 14, 2026 18:30
@keelerm84
keelerm84 marked this pull request as ready for review September 14, 2026 18:30
@keelerm84
keelerm84 requested a review from a team as a code owner September 14, 2026 18:30
Comment thread internal/autoconfig/stream_manager.go Outdated
initTimeout time.Duration
// ignoreConnectionErrors keeps Relay running with no configuration rather than reporting
// a failure. It is the ignoreConnectionErrors configuration option.
ignoreConnectionErrors bool

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a temporary option for backwards compatibility in the current major version?

Comment thread internal/autoconfig/stream_manager.go Outdated
}
result.ActivateProfile = extendedProfile
select {
case authFailureCh <- struct{}{}:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@keelerm84
keelerm84 force-pushed the mk/SDK-3061/autoconfig-retry branch from 536413e to 99d1252 Compare September 15, 2026 16:29
@keelerm84
keelerm84 changed the base branch from v8 to mk/SDK-3096/autoconfig-status September 15, 2026 16:29
Base automatically changed from mk/SDK-3096/autoconfig-status to v8 September 15, 2026 19:53
@keelerm84
keelerm84 force-pushed the mk/SDK-3061/autoconfig-retry branch from 99d1252 to c577386 Compare September 15, 2026 19:55

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread internal/autoconfigcache/dynamodb_store.go Outdated
@keelerm84
keelerm84 force-pushed the mk/SDK-3061/autoconfig-retry branch from c577386 to 95f76b4 Compare September 15, 2026 20:13
@keelerm84
keelerm84 changed the base branch from v8 to mk/SDK-3101/transport-failures-normal September 15, 2026 20:13
@keelerm84
keelerm84 force-pushed the mk/SDK-3101/transport-failures-normal branch from 7380863 to fc8659e Compare September 15, 2026 20:15
@keelerm84
keelerm84 force-pushed the mk/SDK-3061/autoconfig-retry branch from 95f76b4 to 11ed0c6 Compare September 15, 2026 20:26
Base automatically changed from mk/SDK-3101/transport-failures-normal to v8 September 16, 2026 17:15
@keelerm84
keelerm84 force-pushed the mk/SDK-3061/autoconfig-retry branch from 2598562 to 18bcac3 Compare September 16, 2026 17:16

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread internal/autoconfig/stream_manager.go Outdated
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.
@keelerm84
keelerm84 force-pushed the mk/SDK-3061/autoconfig-retry branch from d4aa838 to 54d05ee Compare September 16, 2026 19:09
@keelerm84
keelerm84 merged commit 4bf3bb9 into v8 Sep 16, 2026
17 checks passed
@keelerm84
keelerm84 deleted the mk/SDK-3061/autoconfig-retry branch September 16, 2026 20:28
keelerm84 pushed a commit that referenced this pull request Sep 17, 2026
🤖 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>
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