Skip to content

feat: conform FDv1 streaming and polling data sources to the RETRY spec - #2059

Open
tanderson-ld wants to merge 4 commits into
mainfrom
ta/SDK-2790/retry-wiring
Open

tanderson-ld wants to merge 4 commits into
mainfrom
ta/SDK-2790/retry-wiring

Conversation

@tanderson-ld

@tanderson-ld tanderson-ld commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Wires the reusable retry controller (added to @launchdarkly/js-sdk-common in #2045) into the Node server SDK's FDv1 streaming and polling data sources, bringing them into conformance with the RETRY spec. Streaming injects the controller through the js-eventsource retryDelayStrategy seam; polling drives the controller from its own scheduled loop. This replaces the old recoverable / permanent-stop model.

Scope is FDv1 streaming + polling in the Node server SDK only. FDv2 (the composite data source and its synchronizers) is intentionally untouched and will be conformed in a future effort.

Draft / dependency note. The streaming seam ships in an unreleased launchdarkly-eventsource; the committed pin here stays at 2.2.0, so CI does not yet exercise the injected controller end-to-end (2.2.0 ignores the option and falls back to its built-in backoff). The pin bump to the release carrying the seam, and the long-running contract-test verification, are follow-ups — this PR is up for review of the wiring and behavior.

Behavior changes (operator-facing)

  • No data-source failure is terminal. Previously-fatal HTTP statuses (401, 403, and other 4xx) no longer permanently stop the data source; they retry indefinitely at the extended-regime cadence (5 min → 1 hr). Normal/transient failures retry at the fast cadence (1 s → 30 s, streaming) as before.
  • A bad SDK key no longer fast-fails initialization. waitForInitialization() resolves on success and rejects only on its own timeout or on close(); it no longer rejects immediately with "Authentication failed…" on a 401. A misconfigured client keeps retrying and stays uninitialized until the timeout.
  • Malformed/empty data is recovered from, not fatal. Malformed stream JSON / empty payloads record a normal failure and re-establish the stream (self-initiated close + recreate after the backoff); malformed poll JSON records a normal failure and polls again on schedule.
  • Data-source failures surface as logs only. Consistent with how the SDK has always treated recoverable/interrupted failures, and with the Go/Java/.NET fleet: a fixed "will retry" message (warn for normal, error-level for unexpected/invalid-data), with the reconnect delay in the streaming onretrying info line. The 'error'/'failed' events and init-rejection remain reserved for genuinely terminal conditions (today, only the untouched FDv2 composite reaches them).
  • Polling cadence is completion-anchored (effective period ≈ interval + poll duration), matching the fleet.

Versioning: feat: / minor for all affected packages — matching how Go (7.16.0), Java (7.16.0), and .NET (8.17.0) each shipped RETRY conformance (minor, behavior changes documented in the PR, not marked breaking).

What's here

  • packages/shared/common — EventSourceRetryDelayStrategy type + optional retryDelayStrategy on the platform EventSourceInitDict (the injection seam; initialRetryDelayMillis/retryResetIntervalMillis made optional since they're unused under injection).
  • packages/shared/sdk-server — StreamingProcessor and PollingProcessor rewired to the controller (classification, always-retry, malformed-data recovery via a guarded close-and-recreate, completion-anchored polling). LDClientImpl is intentionally left unchanged; the reserved 'error'/'failed'/init-reject path is untouched.
  • packages/sdk/server-node — NodeRequests passes the strategy through; contract-test capabilities retry-conformance-fdv1-streaming / -polling; a nightly workflow (server-node-nightly.yaml) that runs the long-running RETRY suite (schedule + manual dispatch, no push trigger).

Testing

Unit suites green: @launchdarkly/js-server-sdk-common (1053 pass) and @launchdarkly/node-server-sdk (70 pass), covering the classification ladders (via the controller), the streaming close+recreate on malformed data (including a guard against a duplicate reconnect and a deserialization that throws), polling recovery, stop()-cancels-a-pending-retry interruptibility, status-less transport-error classification, single-failure accounting for a self-initiated restart, the reserved terminal 'error'/'failed'/init-reject path, and the restored onretrying delay log. The behavioral RETRY conformance itself is validated by the harness in the nightly (long-running), which is a post-merge / manual-dispatch activity.

Part of SDK-2790.


Note

Overview
Brings FDv1 streaming and polling in the Node server SDK in line with the RETRY spec by driving reconnect/poll timing from the shared RetryState controller instead of permanent-stop / recoverable-only logic.

Streaming injects that state through a new platform EventSourceRetryDelayStrategy on EventSourceInitDict; StreamingProcessor passes it via retryDelayStrategy, handles malformed payloads with a guarded close-and-recreate path, and always retries HTTP/transport failures (401/403 use extended backoff, logs only—no data-source error events). Polling schedules the next request from RetryState after each completion (including invalid JSON/deserialization failures) and no longer treats any HTTP status as terminal.

Supporting changes: optional default backoff millis in the browser EventSource when init fields are omitted; Node createEventSource stops overriding transport backoff caps; contract-test service advertises retry-conformance-fdv1-* capabilities; a nightly workflow runs the long-running RETRY harness (not on every PR).

Reviewed by Cursor Bugbot for commit 9fbd27a. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

Copy link
Copy Markdown
Contributor

@launchdarkly/js-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 29338 bytes
Compressed size limit: 29500
Uncompressed size: 141229 bytes

@github-actions

Copy link
Copy Markdown
Contributor

@launchdarkly/js-client-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 25437 bytes
Compressed size limit: 44000
Uncompressed size: 165420 bytes

@tanderson-ld
tanderson-ld deleted the ta/SDK-2790/retry-wiring branch September 29, 2026 15:43
@tanderson-ld
tanderson-ld restored the ta/SDK-2790/retry-wiring branch September 29, 2026 15:44
@tanderson-ld tanderson-ld reopened this Sep 29, 2026
Drive the server SDK's FDv1 streaming and polling reconnection from the
reusable retry controller via an injected retry-delay strategy:

- Classify HTTP and transport failures per the RETRY table; no failure is
  terminal, so 401/403 now retry in the extended regime instead of
  permanently stopping.
- Surface data-source failures as logs only, keeping the error-event
  channel reserved for terminal conditions.
- Restart the stream and reschedule polling on malformed or empty payloads,
  recording them as normal failures and guarding against a duplicate
  reconnect when several arrive in one parse pass.
- Wrap deserialization so structurally invalid data cannot escape and stall
  the data source.
- Advertise FDv1 retry-conformance contract-test capabilities and add a
  nightly workflow that runs the long-running suite.
@tanderson-ld
tanderson-ld force-pushed the ta/SDK-2790/retry-wiring branch from b77a558 to f1af537 Compare September 29, 2026 19:11
…classification

Add unit coverage for RETRY behaviors that the shared contract tests do not
exercise:

- A scheduled reconnect (streaming) and the next poll (polling) are cancelled
  when the data source is stopped before the timer fires.
- A transport error with no HTTP status is classified as a normal, retryable
  failure and logged at warn level, for both data sources.
- A self-initiated stream restart after malformed data records exactly one
  failure, so the SDK's own close is not counted a second time.
… optional

The shared EventSourceInitDict now makes initialRetryDelayMillis and
retryResetIntervalMillis optional, since an injected retryDelayStrategy can
replace the built-in backoff. The browser event source still uses the
built-in backoff, so default those bounds to the standard streaming values
(1s initial, 60s reset) where DefaultBackoff is constructed. The browser
always supplies concrete values, so behavior is unchanged; the defaults only
satisfy the widened type.
@github-actions

Copy link
Copy Markdown
Contributor

@launchdarkly/js-client-sdk size report
This is the brotli compressed size of the ESM build.
Compressed size: 32669 bytes
Compressed size limit: 34000
Uncompressed size: 117050 bytes

@tanderson-ld
tanderson-ld marked this pull request as ready for review September 29, 2026 20:40
@tanderson-ld
tanderson-ld requested a review from a team as a code owner September 29, 2026 20:40

@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 9fbd27a. Configure here.

agent: this._agent,
tlsParams: this._tlsOptions,
maxBackoffMillis: 30 * 1000,
jitterRatio: 0.5,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Streaming backoff options removed too early

High Severity

NodeRequests.createEventSource no longer sets maxBackoffMillis and jitterRatio, and FDv1 StreamingProcessor no longer passes initialRetryDelayMillis or retryResetIntervalMillis. The pinned launchdarkly-eventsource 2.2.0 ignores retryDelayStrategy and falls back to its built-in backoff, which those options used to configure. FDv2 streaming never injects a strategy and still depends on this layer for the 30s cap and jitter, so it loses exponential backoff even after the seam ships. Persistent stream failures can retry much more aggressively than the RETRY spec and the previous Node SDK behavior.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9fbd27a. Configure here.

This branch has not been deployed

No deployments
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.

1 participant