Skip to content

chore: add FDv2 recovery primitives to SourceManager and Conditions - #1856

Open
joker23 wants to merge 4 commits into
mainfrom
skz/SDK-2911/fdv1-fallback-fix-recovery-primitives
Open

chore: add FDv2 recovery primitives to SourceManager and Conditions#1856
joker23 wants to merge 4 commits into
mainfrom
skz/SDK-2911/fdv1-fallback-fix-recovery-primitives

Conversation

@joker23

@joker23 joker23 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds SourceManager.fdv2Recovery() (reverses fdv1Fallback() -- blocks FDv1 slots, unblocks non-FDv1 slots, resets the synchronizer index) and a read-only SourceManager.isCurrentSynchronizerFDv1Fallback getter. Also guards handleFdv1Fallback in FDv2DataSource against re-triggering fallback when the active synchronizer is already the FDv1 fallback slot (regression SDK-2617).

Per review discussion (#1856 (comment)), FDv2 recovery after a directed FDv1 fallback is not modeled as a Condition here -- the originally-proposed Conditions.createFDv2RecoveryCondition(ttlMs) and 'fdv2Recovery' condition type were removed. Conditions are only ever created through getConditions(), which produces no conditions at all when one or zero synchronizers are available -- exactly the state a directed FDv1 fallback puts the SDK in -- so a Condition-based recovery attempt would be silently skipped in the case DATASYSTEM v2 section 1.6 requires it to fire unconditionally.

fdv2Recovery() / isCurrentSynchronizerFDv1Fallback remain primitives with no caller in this diff. The actual recovery scheduling (a plain elapsed-time check, not a Condition) is being wired into PR #1858.


Note

Overview
Adds SourceManager.fdv2Recovery() (mirror of fdv1Fallback: block FDv1 slots, unblock FDv2, reset synchronizer index) and isCurrentSynchronizerFDv1Fallback so the orchestrator can tell when the active synchronizer is already on FDv1.

handleFdv1Fallback in FDv2DataSource now skips re-running fallback when the current synchronizer is an FDv1 fallback slot but still emits fdv1Fallback on its results—fixing a loop where FDv2 could be recreated repeatedly (regression SDK-2617).

Conditions gets small robustness/docs tweaks: idempotent fallback timer start on repeated interrupted statuses, and clarified comments on close() and the synchronizer try/finally.

Unit tests cover fdv2Recovery / isCurrentSynchronizerFDv1Fallback and the FDv1 re-trigger guard. fdv2Recovery is not wired into the orchestration loop in this diff (primitives for a follow-up).

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

@joker23 joker23 changed the title skz/SDK 2911/fdv1 fallback fix recovery primitives chore: add FDv2 recovery primitives to SourceManager and Conditions Aug 13, 2026
@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: 26360 bytes
Compressed size limit: 29000
Uncompressed size: 129188 bytes

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

@launchdarkly/browser size report
This is the brotli compressed size of the ESM build.
Compressed size: 179960 bytes
Compressed size limit: 200000
Uncompressed size: 833634 bytes

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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

@joker23

joker23 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

@cursor review

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit ab34b76. Configure here.

@joker23
joker23 marked this pull request as ready for review August 13, 2026 16:46
@joker23
joker23 requested a review from a team as a code owner August 13, 2026 16:46

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional findings.

Open in Devin Review

Comment thread packages/shared/sdk-client/src/datasource/fdv2/Conditions.ts Outdated
Comment thread packages/shared/sdk-client/src/datasource/fdv2/Conditions.ts Outdated
@joker23
joker23 requested a review from kinyoklion August 14, 2026 17:28
@joker23
joker23 force-pushed the skz/SDK-2911/fdv1-fallback-fix-recovery-primitives branch 2 times, most recently from bb71b36 to b3def90 Compare August 18, 2026 20:19
@joker23
joker23 force-pushed the skz/SDK-2911/fdv1-fallback-fix-recovery-primitives branch from b3def90 to ad07e81 Compare August 18, 2026 20:29
…nizer itself

If the FDv1 fallback synchronizer's own result is flagged fdv1Fallback, don't
re-run the fallback machinery -- the SDK is already on FDv1.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment on lines +226 to +231
fdv2Recovery() {
synchronizerSlots.forEach((slot) => {
slot.state = slot.isFDv1Fallback ? 'blocked' : 'available';
});
synchronizerIndex = -1;
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 New recovery path back to the modern data connection is never used, so a fallback stays permanent

A new way to switch back from the legacy fallback connection to the normal one is added (fdv2Recovery() at packages/shared/sdk-client/src/datasource/fdv2/SourceManager.ts:226-231) but nothing in the product code ever calls it, so once the SDK falls back it stays there for the life of the client.
Impact: Customers whose SDK is temporarily directed to the legacy connection never return to the normal one, even after the directive's expiry time has passed.

Why the recovery path is unreachable

A repo-wide search shows fdv2Recovery and the accompanying isCurrentSynchronizerFDv1Fallback getter are referenced only from packages/shared/sdk-client/__tests__/datasource/fdv2/SourceManager.test.ts:475,497 (plus the guard use of the getter at packages/shared/sdk-client/src/datasource/fdv2/FDv2DataSource.ts:137). The orchestrator's recovery branch only calls sourceManager.resetSourceIndex() (packages/shared/sdk-client/src/datasource/fdv2/FDv2DataSource.ts:289-291), which does not unblock the FDv2 slots that fdv1Fallback() blocked. Furthermore, after fdv1Fallback() only one slot remains available, so getConditions() (packages/shared/sdk-client/src/datasource/fdv2/Conditions.ts:213-215) returns an empty group and no recovery timer is ever created. The fdv1FallbackTtlMs value carried on results is likewise never consumed by the orchestrator.

Prompt for agents
SourceManager gains fdv2Recovery(), which blocks FDv1 fallback slots, unblocks non-FDv1 slots and resets the synchronizer index, but no production code path calls it. In FDv2DataSource.runSynchronizers the 'recovery' condition branch only calls resetSourceIndex(), and after fdv1Fallback() there is exactly one available synchronizer so getConditions() returns an empty group and no recovery timer is created at all. The fdv1FallbackTtlMs value that sources attach to results is also never read by the orchestrator. Either wire fdv2Recovery() into a TTL-driven recovery path (e.g. schedule a recovery based on fdv1FallbackTtlMs while running on the FDv1 fallback synchronizer, and call fdv2Recovery() when it fires), or drop the unused API until the recovery feature is implemented.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this will be handled later in the stack #1875

@joker23
joker23 marked this pull request as draft August 20, 2026 23:01
@joker23
joker23 marked this pull request as ready for review August 21, 2026 23:22
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