Skip to content

Conversation

@tanderson-ld
Copy link
Contributor

@tanderson-ld tanderson-ld commented Dec 2, 2025

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
    This may not be possible until more FDv2 logic is available.

Related issues

SDK-1636

Describe the solution you've provided

Adds composite datasource that can be set up with tuples of datasource factories and action appliers. Action appliers transform DataSourceUpdates invocations into actions that change the composite data source.

This PR does not include the definitions of how initializers fallback to synchronizers, that will come in the next PR.


Note

Introduce CompositeSource that can switch among multiple sources via factories and action appliers, with fan-out, sanitization, disableable update proxies, and comprehensive tests.

  • Server/Internal
    • Add CompositeSource (IDataSource, ICompositeSourceActionable) to dynamically switch among sources with queued, serialized actions (start/next/first/blacklist/dispose).
    • Add helpers:
      • DataSourceUpdatesSanitizer (dedupes status, maps OffInterrupted).
      • FanOutDataSourceUpdates (fan-out to multiple IDataSourceUpdates).
      • DisableableDataSourceUpdatesTracker (wrappable, disableable update proxies).
      • SourcesList<T> (iterable/removable source list with optional circular iteration).
    • Add interfaces: ISourceFactory, IActionApplier, IActionApplierFactory, ICompositeSourceActionable.
  • Tests
    • CompositeSourceTest: verifies fallback on Interrupted, blacklisting on Off, and blocking updates from disabled sources.
    • SourcesListTest: verifies replace/remove/cycling behavior.

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

@tanderson-ld tanderson-ld requested a review from a team as a code owner December 2, 2025 22:13

// Start the source asynchronously and complete the task when it finishes.
// We do this outside the lock to avoid blocking the queue.
dataSourceToStart.Start().ContinueWith(task =>
Copy link
Member

Choose a reason for hiding this comment

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

ContinueWith is maybe a little scary. I think we may need to specify which scheduler to use, versus depending on its default behavior. I also don't know if it may be important to make sure it doesn't attach. Basically internal to the SDK we want the behavior of ConfigureAwait(false), even if we aren't interacting with the promise that way.

Task.Run on an async method that does await may be more understandable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Something like this?

                // 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();
                        tcs.TrySetResult(result);
                    }
                    catch (Exception ex)
                    {
                        tcs.TrySetException(ex);
                    }
                });

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Committed the Task.Run solution, let me know if that isn't what you were thinking of.


// report state Off
_sanitizedUpdateSink.UpdateStatus(DataSourceState.Off, null);
}
Copy link

Choose a reason for hiding this comment

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

Bug: Disposed composite source can create new data sources

The Dispose() method clears _pendingActions and resets state, but there's a race window where an already-dequeued action can execute after Dispose() completes. Since Dispose() calls _sourcesList.Reset() and sets _currentDataSource to null, a subsequent GoToNext() or GoToFirst() action that was dequeued before disposal can invoke TryFindNextUnderLock(), which will create a new data source. This new data source will never be disposed, causing a resource leak, and will trigger status updates after Off was already reported, causing state inconsistency. A disposed flag could prevent TryFindNextUnderLock() from creating new sources post-disposal.

Additional Locations (1)

Fix in Cursor Fix in Web

{
try
{
var result = await dataSourceToStart.Start();
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
var result = await dataSourceToStart.Start();
var result = await dataSourceToStart.Start().ConfigureAwait(false);

The ConfigureAwait(false) means we don't care about the synchronization context we run on. It can be important if the code runs in something like if it was running in an HTTP context in ASP, or in an environment with winforms. The rule of thumb is basically to always do this, unless you know you need the same synchronization context, which is hardly ever the case.

Though in the client-side SDK it could be, if you were trying to do UI thread things being called from the UI thread. Which removes the need to like schedule something onto that thread, if you know the task will return on that thread.

I think this method specifically wouldn't matter, because you can never get to the entrypoint from caller code. Generally we should probably always use this setting. The reason I didn't want ContinueWith is because achieving the exact same thing is more complicated.

Copy link
Member

@kinyoklion kinyoklion left a comment

Choose a reason for hiding this comment

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

Check comment for ConfigureAwait feedback.

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.

3 participants