Skip to content

fix: Make FDv1 fallback TTL-based and recover to FDv2 after the TTL - #239

Draft
beekld wants to merge 1 commit into
mainfrom
devin/1792430315-fdv1-fallback-ttl
Draft

beekld wants to merge 1 commit into
mainfrom
devin/1792430315-fdv1-fallback-ttl

Conversation

@beekld

@beekld beekld commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Requirements

  • I have added test coverage for new or changed functionality
  • I have followed the repository's pull request submission guidelines
  • I have validated my changes against all supported platform versions (CI runs Java 8/11/17/19; tested locally on Java 17)

Related issues

SDK-2987 (part of SDK-2935). Spec: sdk-specs#251 (DATASYSTEM v2 1.6). Reference: cpp-sdks#631.

Describe the solution you've provided

Makes the server SDK's FDv1 fallback directive TTL-based. Fallback no longer lasts forever, and the SDK goes back to FDv2 when the TTL is up.

  • The TTL is read from X-LD-FD-Fallback-TTL (polling and streaming) and from goodbye.protocolFallbackTTL. If a goodbye has protocolFallbackTTL, that counts as a directive by itself.
  • A TTL in (0, 1h] is used as-is, with no jitter. A TTL that is missing, malformed, zero, negative, or over 1h is replaced by the default: 1h minus a uniform random [0, 30m], so it lands in [30m, 1h]. This is the new public, experimental FDv1FallbackDirective. The old boolean fdv1Fallback factories on FDv2SourceResult still work (true means the jittered default).
  • On fallback, the FDv2 synchronizers that were available are suspended, not blocked for good. Once the TTL passes they come back, starting from the primary, and the FDv1 synchronizer is stopped. Synchronizers that had already failed terminally stay blocked.
  • If no FDv1 fallback synchronizer is configured, the data source no longer goes to OFF. It reports INTERRUPTED (keeping the original error), runs no synchronizer during the TTL, then goes back to FDv2.
  • If the FDv1 fallback synchronizer gets a new directive, the new TTL replaces the old one and FDv1 is not restarted (1.6.4.6).
Implementation details
  • Single writer: All store writes still happen on the single FDv2DataSource run thread, and only results from the active synchronizer get applied. When no synchronizer replaces the active one, SourceManager.getNextAvailableSynchronizerAndSetActive now closes it. Without this, the FDv2 synchronizer that sent the directive would stay connected while the SDK waits out the TTL with no FDv1 fallback.
  • TTL timer: FDv1FallbackExpiryCondition is a new condition type. While fallback is engaged, it is the only condition on the FDv1 synchronizer, so the normal fallback/recovery conditions don't apply. It resets its timer whenever it sees a result that carries a directive.
  • Recovery restarts synchronizers only. Initializers are not re-run, which 1.6.4.2 allows.
  • No FDv1 synchronizer: If an initializer sends the directive, start() stays pending until FDv2 recovers instead of finishing with false, so LDClient startup waits up to startWait. If there are no FDv2 synchronizers to go back to, the FDv1 synchronizer just keeps running.
  • Tests: Added unit tests showing that valid TTLs pass through unchanged, invalid TTLs get a default in [30m, 1h], and the header and goodbye TTLs are parsed. Also added data-source tests for recovery with and without an FDv1 synchronizer, for an FDv1-received directive replacing the TTL, and for the FDv1 synchronizer being stopped on recovery. The two tests that asserted OFF with no FDv1 synchronizer now assert INTERRUPTED followed by recovery.

Describe alternatives you've considered

Parsing protocolFallbackTTL in lib/shared/internal's Goodbye/FDv2ActionGoodbye. This PR deliberately leaves Goodbye.java unchanged. The server SDK depends on the released launchdarkly-java-sdk-internal (1.12.2), so a change there would need its own internal release and then a bump PR before the server could use it. Instead, the server reads protocolFallbackTTL straight from the goodbye event's JSON (FDv1FallbackDirectives.fromGoodbye). That parsing can move into Goodbye in a later change if reviewers want it.

Additional context

No contract test changes, because the sdk-test-harness has no fallback-TTL coverage yet.

Link to Devin session: https://app.devin.ai/sessions/cdc21935a71a406bbe1b22dba462357c
Open in Devin Desktop: https://app.devin.ai/desktop/session/cdc21935a71a406bbe1b22dba462357c?variant=devin
Requested by: @beekld

Co-Authored-By: Bee Klimt <bklimt@launchdarkly.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot added the devin-pr Label for PR's created using devin. label Oct 19, 2026

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

devin-pr Label for PR's created using devin.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant