Skip to content

Conversation

@kinyoklion
Copy link
Member

@kinyoklion kinyoklion commented Dec 12, 2025

Note

Adds the FDv2 data system with a new transactional update API, composite source orchestration, write-through store, and integrated streaming/polling with selector support, plus extensive tests.

  • FDv2 Data System (core):
    • Introduces IDataSourceUpdatesV2 (transactional updates API) and adapts legacy sources via DataSourceUpdatesV2ToV1Adapter.
    • Adds FDv2DataSystem, WriteThroughStore (memory+persistent, selector-aware), and SelectorSourceFacade.
    • Wires LdClient to use FDv2 when Configuration.DataSystem is set.
  • Composite source orchestration:
    • Refactors to CompositeSource using IDataSourceObserver, ObservableDataSourceUpdates, and DataSourceUpdatesSanitizer (V2).
    • Replaces fan-out/IActionApplier with observer pattern and disableable sinks; updates SourceFactory/ActionApplierFactory signatures.
  • FDv2 data sources:
    • Builds FDv2 streaming/polling with selector support and transactional applies.
    • FDv2DataSource composes initializers/synchronizers with fallback/blacklist logic and timed recovery.
    • Streaming: ignore unknown events (debug log), exposes handled event list.
  • Tests & support:
    • Extensive new/updated unit tests for composite, FDv2 streaming/polling, and data flow; contract-test suppressions added.
    • Adds test visibility in AssemblyInfo.

Written by Cursor Bugbot for commit dd9ec86. This will update automatically on new commits. Configure here.

@kinyoklion
Copy link
Member Author

bugbot review

if (!ApplyToLegacyPersistence(changeSet))
{
// TODO: Probably throw?
}
Copy link

Choose a reason for hiding this comment

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

Bug: Silent failure when applying to legacy persistent store

When ApplyToLegacyPersistence returns false (indicating a failure to write to a legacy non-transactional persistent store), the failure is silently ignored. The data has already been written to the memory store at this point, so the memory and persistent stores become inconsistent. On SDK restart, the persistent store would have stale data. The failure result from ApplyToLegacyPersistence is not propagated or handled, leaving only a TODO comment.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

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

This will be addressed later.

@kinyoklion kinyoklion marked this pull request as ready for review December 12, 2025 23:48
@kinyoklion kinyoklion requested a review from a team as a code owner December 12, 2025 23:48
kinyoklion and others added 2 commits December 15, 2025 09:21
Co-authored-by: Todd Anderson <127344469+tanderson-ld@users.noreply.github.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Bug: Task hangs forever if disposed during StartCurrent

The StartCurrent method checks _disposed outside the lock, then creates a TaskCompletionSource and enqueues an action to complete it. If Dispose is called between the check and the action being processed, InternalDispose clears _pendingActions, orphaning the TaskCompletionSource. The returned Task will never complete, causing any caller awaiting it to hang indefinitely. The EnqueueAction method doesn't check disposal state before enqueueing, allowing actions to be added and then immediately discarded.

pkgs/sdk/server/src/Internal/DataSources/CompositeDataSource/CompositeSource.cs#L239-L282

/// </summary>
public Task<bool> StartCurrent()
{
if (_disposed)
{
return Task.FromResult(false);
}
var tcs = new TaskCompletionSource<bool>();
EnqueueAction(() =>
{
IDataSource dataSourceToStart;
lock (_lock)
{
TryFindNextUnderLock();
dataSourceToStart = _currentDataSource;
}
if (dataSourceToStart is null)
{
// No sources available.
tcs.SetResult(false);
return;
}
// Start the source asynchronously and complete the task when it finishes.
// We do this outside the lock to avoid blocking the queue.
_ = Task.Run(async () =>
{
try
{
var result = await dataSourceToStart.Start().ConfigureAwait(false);
tcs.TrySetResult(result);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});
});
return tcs.Task;
}

pkgs/sdk/server/src/Internal/DataSources/CompositeDataSource/CompositeSource.cs#L94-L121

private void InternalDispose(DataSourceStatus.ErrorInfo? error = null)
{
// When disposing the whole composite, we bypass the action queue and tear
// down the current data source immediately while still honoring the same
// state transitions under the shared lock. Any queued actions become no-ops
// because there is no current data source
// been disconnected.
lock (_lock)
{
// cut off all the update proxies that have been handed out first
_disableableTracker.DisablePreviouslyTracked();
// dispose of the current data source
_currentDataSource?.Dispose();
_currentDataSource = null;
// clear any queued actions and reset processing state
_pendingActions.Clear();
_isProcessingActions = false;
_currentEntry = default;
_disposed = true;
}
// report state Off directly to the original sink, bypassing the sanitizer
// which would map Off to Interrupted (that mapping is only for underlying sources)
_originalUpdateSink.UpdateStatus(DataSourceState.Off, error);
}

Fix in Cursor Fix in Web


@kinyoklion kinyoklion force-pushed the rlamb/sdk1586/fdv2-data-system branch from ada9d23 to cd2491a Compare December 15, 2025 22:41
@kinyoklion kinyoklion force-pushed the rlamb/sdk1586/fdv2-data-system branch from cd2491a to 48f4568 Compare December 15, 2025 22:42
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Bug: Exception swallowing can cause Start() to hang indefinitely

The ProcessQueuedActions method catches and swallows all exceptions from enqueued actions. When StartCurrent() enqueues an action that calls TryFindNextUnderLock(), if the data source factory or action applier factory throws an exception, it gets silently caught. The TaskCompletionSource is never completed (no SetResult or SetException), causing the returned Task from Start() to hang indefinitely. Any caller awaiting Start() would block forever if factory initialization fails.

pkgs/sdk/server/src/Internal/DataSources/CompositeDataSource/CompositeSource.cs#L177-L186

// the next action to prevent one failure from stopping all processing.
try
{
action();
}
catch
{
// Continue processing remaining actions even if one fails
// TODO: need to add logging, will add in next PR
}

Fix in Cursor Fix in Web


@kinyoklion kinyoklion merged commit 84dd3dd into main Dec 16, 2025
15 checks passed
@kinyoklion kinyoklion deleted the rlamb/sdk1586/fdv2-data-system branch December 16, 2025 21:43
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