Skip to content

Commit 0ffeae9

Browse files
committed
chore: init tracker gets updates synchronously before action appliers
1 parent 5fe3ca7 commit 0ffeae9

File tree

2 files changed

+135
-3
lines changed

2 files changed

+135
-3
lines changed

pkgs/sdk/server/src/Internal/FDv2DataSources/FDv2DataSource.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public static IDataSource CreateFDv2DataSource(
6767
return new CompositeSource(sink, initializerFactory, circular: false);
6868
},
6969
(actionable) => new CompositeObserver(
70-
blacklistWhenSuccessOrOff(actionable), initializationObserver)
70+
initializationObserver, blacklistWhenSuccessOrOff(actionable))
7171
));
7272
}
7373

@@ -93,8 +93,7 @@ public static IDataSource CreateFDv2DataSource(
9393
// Only attach FDv1 fallback applier if FDv1 synchronizers are actually provided
9494
if (fdv1Synchronizers != null && fdv1Synchronizers.Count > 0)
9595
{
96-
return new CompositeObserver(fdv1FallbackApplierFactory(actionable),
97-
synchronizationObserver);
96+
return new CompositeObserver(synchronizationObserver, fdv1FallbackApplierFactory(actionable));
9897
}
9998

10099
return synchronizationObserver;

pkgs/sdk/server/test/Internal/FDv2DataSources/FDv2DataSourceTest.cs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,139 @@ public async Task OneInitializerNoSynchronizerIsWellBehaved()
916916
dataSource.Dispose();
917917
}
918918

919+
[Fact]
920+
public async Task OneInitializerOneSynchronizerIsWellBehaved()
921+
{
922+
// Create a capturing sink to observe all updates
923+
var capturingSink = new CapturingDataSourceUpdatesWithHeaders();
924+
925+
// Create dummy data for initializer and synchronizer
926+
var initializerDummyData = new FullDataSet<ItemDescriptor>(new Dictionary<DataKind, KeyedItems<ItemDescriptor>>());
927+
var synchronizerDummyData = new FullDataSet<ItemDescriptor>(new Dictionary<DataKind, KeyedItems<ItemDescriptor>>());
928+
929+
// Track whether the initializer factory was invoked
930+
bool initializerFactoryInvoked = false;
931+
IDataSourceUpdatesV2 initializerUpdateSink = null;
932+
933+
// Create initializer factory: emits Initializing, calls init with dummy data, then reports Valid
934+
SourceFactory initializerFactory = (updatesSink) =>
935+
{
936+
initializerFactoryInvoked = true;
937+
initializerUpdateSink = updatesSink;
938+
var source = new MockDataSourceWithInit(
939+
async () =>
940+
{
941+
// Emit Initializing
942+
updatesSink.UpdateStatus(DataSourceState.Initializing, null);
943+
await Task.Delay(10);
944+
945+
// Report Valid
946+
updatesSink.UpdateStatus(DataSourceState.Valid, null);
947+
await Task.Delay(10);
948+
949+
// Call Apply with dummy data (with selector to trigger switch to synchronizer)
950+
updatesSink.Apply(new ChangeSet<ItemDescriptor>(
951+
ChangeSetType.Full,
952+
Selector.Make(1, "dummy-state"),
953+
initializerDummyData.Data,
954+
null
955+
));
956+
await Task.Delay(10);
957+
}
958+
);
959+
return source;
960+
};
961+
962+
// Track whether the synchronizer factory was invoked
963+
bool synchronizerFactoryInvoked = false;
964+
IDataSourceUpdatesV2 synchronizerUpdateSink = null;
965+
966+
// Create synchronizer factory: emits Initializing, calls init with dummy data, then reports Valid
967+
SourceFactory synchronizerFactory = (updatesSink) =>
968+
{
969+
synchronizerFactoryInvoked = true;
970+
synchronizerUpdateSink = updatesSink;
971+
var source = new MockDataSourceWithInit(
972+
async () =>
973+
{
974+
// Emit Initializing
975+
updatesSink.UpdateStatus(DataSourceState.Initializing, null);
976+
await Task.Delay(10);
977+
978+
// Report Valid
979+
updatesSink.UpdateStatus(DataSourceState.Valid, null);
980+
await Task.Delay(10);
981+
982+
// Call Apply with dummy data
983+
updatesSink.Apply(new ChangeSet<ItemDescriptor>(
984+
ChangeSetType.Full,
985+
Selector.Make(2, "dummy-state"),
986+
synchronizerDummyData.Data,
987+
null
988+
));
989+
await Task.Delay(10);
990+
}
991+
);
992+
return source;
993+
};
994+
995+
// Create FDv2DataSource with one initializer, one synchronizer, and empty fdv1Synchronizers
996+
var initializers = new List<SourceFactory> { initializerFactory };
997+
var synchronizers = new List<SourceFactory> { synchronizerFactory };
998+
var fdv1Synchronizers = new List<SourceFactory>();
999+
1000+
var dataSource = FDv2DataSource.CreateFDv2DataSource(
1001+
capturingSink,
1002+
initializers,
1003+
synchronizers,
1004+
fdv1Synchronizers
1005+
);
1006+
1007+
// Start the data source
1008+
var startTask = dataSource.Start();
1009+
1010+
// Wait for all expected status updates to be recorded
1011+
// Expected sequence: Initializing (initializer), Valid (initializer), Interrupted (switching to synchronizer), Valid (synchronizer)
1012+
var statusUpdates = capturingSink.WaitForStatusUpdates(4, TimeSpan.FromSeconds(5));
1013+
1014+
// Verify that Start() completed successfully
1015+
var startResult = await startTask;
1016+
Assert.True(startResult);
1017+
1018+
// Verify status updates by position
1019+
// Position 0: Initializing (from initializer)
1020+
Assert.True(statusUpdates.Count > 0, "Expected at least 1 status update");
1021+
Assert.Equal(DataSourceState.Initializing, statusUpdates[0].State);
1022+
1023+
// Position 1: Valid (from initializer)
1024+
Assert.True(statusUpdates.Count > 1, "Expected at least 2 status updates");
1025+
Assert.Equal(DataSourceState.Valid, statusUpdates[1].State);
1026+
1027+
// Position 2: Interrupted (switching to synchronizer)
1028+
Assert.True(statusUpdates.Count > 2, "Expected at least 3 status updates");
1029+
Assert.Equal(DataSourceState.Interrupted, statusUpdates[2].State);
1030+
1031+
// Position 3: Valid (from synchronizer)
1032+
Assert.True(statusUpdates.Count > 3, "Expected at least 4 status updates");
1033+
Assert.Equal(DataSourceState.Valid, statusUpdates[3].State);
1034+
1035+
// Verify that both factories were invoked
1036+
Assert.True(initializerFactoryInvoked, "Initializer factory should have been invoked");
1037+
Assert.True(synchronizerFactoryInvoked, "Synchronizer factory should have been invoked");
1038+
1039+
// Verify that Apply was called twice: once for initializer, once for synchronizer
1040+
var firstChangeSet = capturingSink.Applies.ExpectValue(TimeSpan.FromSeconds(1));
1041+
Assert.Equal(ChangeSetType.Full, firstChangeSet.Type);
1042+
1043+
var secondChangeSet = capturingSink.Applies.ExpectValue(TimeSpan.FromSeconds(1));
1044+
Assert.Equal(ChangeSetType.Full, secondChangeSet.Type);
1045+
1046+
// Verify that there are no more Apply calls
1047+
capturingSink.Applies.ExpectNoValue(TimeSpan.FromMilliseconds(100));
1048+
1049+
dataSource.Dispose();
1050+
}
1051+
9191052
[Fact]
9201053
public async Task NoInitializersAndNoSynchronizersIsWellBehaved()
9211054
{

0 commit comments

Comments
 (0)