feat: Retry data sources on terminal errors instead of stopping - #533
Conversation
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 4d62600. Configure here.
|
|
||
| public func onClosed() { | ||
| os_log("%s EventSource closed", log: service.config.logger, type: .debug, typeName(and: #function)) | ||
| connectedAt = nil |
There was a problem hiding this comment.
Close wipes healthy-stream backoff marker
Medium Severity
Clearing connectedAt in onClosed can run before eventSourceErrorHandler when an open stream fails. The error handler then sees a nil marker and skips the healthy-stream reset, so a connection that stayed up long enough still keeps the previous backoff.
Reviewed by Cursor Bugbot for commit 4d62600. Configure here.
There was a problem hiding this comment.
I don't think this is actually a bug. swift-eventsource calls our error handler first and onClosed() only after it returns (LDSwiftEventSource.swift:282-285, both on one serial queue), so the healthy check always reads connectedAt before it's cleared. And the clear is needed anyway, to stop a stale marker leaking across an offline/online cycle.
|
There is a possible gap in iOS: When the server closes the stream cleanly (didCompleteWithError with error == nil, in LDSwiftEventSource.swift), it just logs "Connection unexpectedly closed", calls onClosed(), and reconnects on its own 1s-to-30s backoff. So on iOS a server close skips the SDK's retry state, and it does not keep the extended backoff after a 401. Ruby needed StreamClosedByServerError for the same case. |
|
Informal approval, but @jsonbailey left a comment that I don't want to approve and risk accidental merge. |
|
Sorry, I rerequested review on this before seeing the latest comments. Looking into that now. |
I opened swift-eventsource#118 to address this. I think it's cleaner to address this there (similar to ruby), rather than doing it in this repo. |
Fixed |


Summary
Previously, the streaming and polling data sources stopped permanently on failures classified as terminal. They now retry those failures, after a longer backoff than other errors.
Implementation notes:
Notes for reviewers:
start(completion:)andidentify(completion:)can block forever. The timeout-based variants still return.setOnline(true)reconnects immediately but does not reset the backoff. Onlyidentifyand a background/foreground transition start a fresh data source and reset the backoff.ConnectionModecase would be source-breaking for FDv1. FDv2 will add interrupted.Contract tests declare the
retry-conformance-fdv1-streamingandretry-conformance-fdv1-pollingcapabilities. Long-running client-side retry tests are not implemented in the harness yet, and there is no client-side polling retry suite, so the polling capability is forward-looking.Note
Overview
Streaming and polling flag data sources now keep retrying after “terminal” sync failures instead of shutting down the synchronizer, marking the client initialized, or reporting connection mode as offline.
RetryStateadds jittered exponential backoff for streaming reconnects and per-poll delays, with a longer “extended” regime after unexpected (e.g. 4xx) failures.FlagSynchronizerowns reconnection: stream errors return.shutdownto EventSource, then tear down and schedule reconnect viaLDTimer; polling reschedules one-shot polls throughpollDidComplete. A ~60s healthy stream resets streaming backoff.LDClientno longer callsflagSynchronizer.isOnline = falseon terminal errors.ConnectionInformationrecords failures but leaves mode as establishing streaming or polling on terminal errors.LDSwiftEventSource is bumped to 3.4.0 (pod, SPM, Xcode). Contract tests advertise
retry-conformance-fdv1-streamingandretry-conformance-fdv1-polling.Reviewed by Cursor Bugbot for commit 147d849. Bugbot is set up for automated code reviews on this repo. Configure here.