Skip to content

fix: fall back from a silent synchronizer during FDv2 startup - #461

Open
tanderson-ld wants to merge 2 commits into
v7from
ta/SDK-3007/fdv2-init-fallback-fix
Open

tanderson-ld wants to merge 2 commits into
v7from
ta/SDK-3007/fdv2-init-fallback-fix

Conversation

@tanderson-ld

@tanderson-ld tanderson-ld commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Requirements

  • I have followed the repository's pull request submission guidelines
  • I have added test coverage for new or changed functionality
  • I have validated my changes against all supported platform versions — n/a, no platform-specific behavior

Related issues

SDK-3235. Also fixes the one known failure in the nightly contract tests added in #454: streaming/fdv2/permanent fallback with recovery.

Describe the solution you've provided

The data system has two fallback timers, selected by reading the status: a 10-second one for startup (cannotInitialize) and a 1-minute one for steady state (interruptedAtRuntime).

A synchronizer that fails permanently during startup reports Off, which the loop publishes as Interrupted. Nothing restores Initializing, so the startup branch — which tested the status value — was unreachable for the rest of startup, leaving only the 1-minute timer. That only becomes visible when the next synchronizer connects and then stays silent: it produces no result for the loop to act on, so the loop depends entirely on the timer, and the client's start-wait expires first. The healthy source is never contacted.

-cannotInitialize := status.State == interfaces.DataSourceStateInitializing &&
+cannotInitialize := !fdv2.dataApplied.Get() &&
     time.Since(status.StateSince) > 10*time.Second

Keying on whether data has ever been applied expresses the actual predicate and cannot go stale as states are added. Runtime behavior is unchanged: once data is applied, only the 1-minute timer applies.

Pre-existing, not a regression — it reproduces on 7.15.6, before the RETRY work.

Describe alternatives you've considered

  • Resetting the status to Initializing on removal. Rejected on cross-SDK evidence: Java and .NET both report INITIALIZING → INTERRUPTED and neither re-publishes Initializing after a source fails. .NET makes that choice explicitly ("spoof interrupted"). This keeps Go's externally visible status sequence identical to today's and consistent with both siblings.
  • Adding Interrupted to the startup branch's state check. Rejected: Interrupted && >10s subsumes Interrupted && >1min, which would make the runtime timer dead code and abandon a source after 10 seconds instead of a minute — and recovery back to it takes 5 minutes of healthy operation.
  • Only refreshing StateSince on the transition. Insufficient: the 1-minute timer would restart and fire at ~70s, still past a 60s start-wait.

Additional context

The fallback and recovery conditions had zero execution coverage before this (the closure bodies measured 0 executions, while the file overall sat at 59.6%) — they are only evaluated on a 10-second tick with 2+ synchronizers configured, which nothing but the long-running contract test reached. The new in-package tests cover both the predicate and the multi-synchronizer loop path, and both fail when the fix is reverted.

Verified: all 4 streaming/fdv2 fallback contract tests pass against the patched SDK, including the recovery assertion that had never executed for Go; all 28 root-package FDv2 end-to-end tests pass; golangci-lint reports 0 issues.


Note

Overview
Fixes FDv2 startup getting stuck when an early synchronizer fails permanently and the next one connects but never sends results.

The 10-second startup fallback (cannotInitialize) no longer requires DataSourceStateInitializing. After a permanent failure the published status stays Interrupted, so that branch never fired and only the 1-minute runtime timer could advance the synchronizer chain—often past the client init timeout.

Startup fallback is now keyed on !dataApplied (no flag data applied yet) plus the same 10s StateSince threshold, so a silent second source still triggers fallback to the next synchronizer. Once data has been applied, behavior is unchanged: only Interrupted > 1 minute triggers fallback.

Adds unit tests for the fallback predicate (including the Interrupted-during-startup regression) and an integration test that walks failing → silent → healthy synchronizers using real timing.

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

A synchronizer that fails permanently during startup reports Off, which
the data system publishes as Interrupted. Nothing restored Initializing,
so the ten-second startup fallback branch -- which tested the status
value -- was unreachable for the rest of startup and only the one-minute
runtime timer applied. When the next synchronizer connected and then
stayed silent it produced no result for the loop to act on, so the
client's start-wait expired before the fallback could advance to a
healthy source.

Key the startup branch on whether data has ever been applied rather than
on the status value. The fallback and recovery conditions previously had
zero execution coverage; this adds in-package tests for both the
predicate and the multi-synchronizer loop path.

SDK-3235
@tanderson-ld
tanderson-ld requested a review from a team as a code owner October 1, 2026 15:22

@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 338196d. Configure here.

cannotInitialize := status.State == interfaces.DataSourceStateInitializing &&
// Cannot initialize is intentionally not a status check since a permanent
// failure during startup leaves the status on Interrupted, which made this
// branch unreachable for the rest of startup.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Comments use historical framing

Low Severity

New comments describe the previous Initializing check and "before the fix" behavior instead of the current rule. Comments on fallbackCond and the new tests mix in old status-keying and the one-minute hang; that history belongs in the PR, not in the code.

Additional Locations (2)
Fix in Cursor Fix in Web

Triggered by learned rule: Go comments: no spec citations, tickets, or historical framing

Reviewed by Cursor Bugbot for commit 338196d. Configure here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Agreed.

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