Skip to content

Commit 12509dd

Browse files
authored
chore: Add changeset sorting. (#188)
Add support for topological sort for changesets. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduce `SortChangeset` in `DataStoreSorter` to topologically sort change sets (preserving metadata) and add comprehensive unit tests. > > - **Server SDK Internals**: > - **Changeset sorting**: Add `DataStoreSorter.SortChangeset` to topologically sort `ChangeSet<ItemDescriptor>` entries by dependencies and `DataKind` priority (segments before features), preserving selector, type, and environment ID. > - **Tests**: > - Add unit tests in `DataStoreSorterTest` covering: > - Metadata preservation (`Type`, `Selector`, `EnvironmentId`). > - Ordering of prerequisite flags (`Features`) and `Segments` before `Features`. > - Empty changeset handling, multiple `DataKind`s, and deleted items retention. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5bd3ec0. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 837048f commit 12509dd

File tree

2 files changed

+221
-2
lines changed

2 files changed

+221
-2
lines changed

pkgs/sdk/server/src/Internal/DataStores/DataStoreSorter.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.Linq;
44
using LaunchDarkly.Sdk.Server.Internal.Model;
@@ -31,6 +31,26 @@ public static FullDataSet<ItemDescriptor> SortAllCollections(FullDataSet<ItemDes
3131
}
3232
return new FullDataSet<ItemDescriptor>(dataOut);
3333
}
34+
35+
/// <summary>
36+
/// Sort the data in the changeset in dependency order. If there are any duplicates, then the highest version
37+
/// of the duplicate item will be retained.
38+
/// </summary>
39+
/// <param name="inSet">the changeset to sort</param>
40+
/// <returns>a sorted copy of the changeset</returns>
41+
public static ChangeSet<ItemDescriptor> SortChangeset(ChangeSet<ItemDescriptor> inSet)
42+
{
43+
var dataOut = new SortedDictionary<DataKind, KeyedItems<ItemDescriptor>>(
44+
PriorityComparer.Instance);
45+
46+
foreach (var entry in inSet.Data)
47+
{
48+
var kind = entry.Key;
49+
dataOut.Add(kind, new KeyedItems<ItemDescriptor>(SortCollection(kind, entry.Value.Items)));
50+
}
51+
52+
return new ChangeSet<ItemDescriptor>(inSet.Type, inSet.Selector, dataOut, inSet.EnvironmentId);
53+
}
3454

3555
private static IEnumerable<KeyValuePair<string, ItemDescriptor>> SortCollection(DataKind kind,
3656
IEnumerable<KeyValuePair<string, ItemDescriptor>> input)

pkgs/sdk/server/test/Internal/DataStores/DataStoreSorterTest.cs

Lines changed: 200 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
3+
using System.Collections.Immutable;
34
using System.Linq;
45
using LaunchDarkly.Sdk.Server.Internal.Model;
6+
using LaunchDarkly.Sdk.Server.Subsystems;
57
using Xunit;
68

79
using static LaunchDarkly.Sdk.Server.Subsystems.DataStoreTypes;
@@ -106,5 +108,202 @@ internal static void VerifyDataSetOrder(FullDataSet<ItemDescriptor> resultData,
106108
}
107109
}
108110
}
111+
112+
#region SortChangeset tests
113+
114+
[Fact]
115+
public void SortChangeset_PreservesChangeSetMetadata()
116+
{
117+
var selector = Selector.Make(42, "test-state");
118+
const string environmentId = "test-env";
119+
var changeSet = new ChangeSet<ItemDescriptor>(
120+
ChangeSetType.Partial,
121+
selector,
122+
ImmutableList<KeyValuePair<DataKind, KeyedItems<ItemDescriptor>>>.Empty,
123+
environmentId
124+
);
125+
126+
var result = DataStoreSorter.SortChangeset(changeSet);
127+
128+
Assert.Equal(ChangeSetType.Partial, result.Type);
129+
Assert.Equal(selector.Version, result.Selector.Version);
130+
Assert.Equal(selector.State, result.Selector.State);
131+
Assert.Equal(environmentId, result.EnvironmentId);
132+
}
133+
134+
[Fact]
135+
public void SortChangeset_SortsPrerequisiteFlagsFirst()
136+
{
137+
var flagC = new FeatureFlagBuilder("c").Version(1).Build();
138+
var flagE = new FeatureFlagBuilder("e").Version(1).Build();
139+
var flagB = new FeatureFlagBuilder("b").Version(1)
140+
.Prerequisites(new List<Prerequisite>
141+
{
142+
new Prerequisite("c", 0),
143+
new Prerequisite("e", 0)
144+
})
145+
.Build();
146+
var flagA = new FeatureFlagBuilder("a").Version(1)
147+
.Prerequisites(new List<Prerequisite>
148+
{
149+
new Prerequisite("b", 0),
150+
new Prerequisite("c", 0)
151+
})
152+
.Build();
153+
154+
var changeSetData = ImmutableList.Create(
155+
new KeyValuePair<DataKind, KeyedItems<ItemDescriptor>>(
156+
DataModel.Features,
157+
new KeyedItems<ItemDescriptor>(ImmutableList.Create(
158+
new KeyValuePair<string, ItemDescriptor>("a", new ItemDescriptor(1, flagA)),
159+
new KeyValuePair<string, ItemDescriptor>("b", new ItemDescriptor(1, flagB)),
160+
new KeyValuePair<string, ItemDescriptor>("c", new ItemDescriptor(1, flagC)),
161+
new KeyValuePair<string, ItemDescriptor>("e", new ItemDescriptor(1, flagE))
162+
))
163+
)
164+
);
165+
166+
var changeSet = new ChangeSet<ItemDescriptor>(
167+
ChangeSetType.Partial,
168+
Selector.Make(1, "state1"),
169+
changeSetData,
170+
null
171+
);
172+
173+
var result = DataStoreSorter.SortChangeset(changeSet);
174+
175+
var flagsData = result.Data.First(kv => kv.Key == DataModel.Features);
176+
var resultKeys = flagsData.Value.Items.Select(kv => kv.Key).ToList();
177+
178+
// Verify ordering constraints
179+
Assert.True(resultKeys.IndexOf("c") < resultKeys.IndexOf("b"), "c should come before b");
180+
Assert.True(resultKeys.IndexOf("e") < resultKeys.IndexOf("b"), "e should come before b");
181+
Assert.True(resultKeys.IndexOf("b") < resultKeys.IndexOf("a"), "b should come before a");
182+
Assert.True(resultKeys.IndexOf("c") < resultKeys.IndexOf("a"), "c should come before a");
183+
}
184+
185+
[Fact]
186+
public void SortChangeset_SortsSegmentsBeforeFlags()
187+
{
188+
var flag1 = new FeatureFlagBuilder("flag1").Version(1).Build();
189+
var segment1 = new SegmentBuilder("seg1").Version(1).Build();
190+
191+
var changeSetData = ImmutableList.Create(
192+
new KeyValuePair<DataKind, KeyedItems<ItemDescriptor>>(
193+
DataModel.Features,
194+
new KeyedItems<ItemDescriptor>(ImmutableList.Create(
195+
new KeyValuePair<string, ItemDescriptor>("flag1", new ItemDescriptor(1, flag1))
196+
))
197+
),
198+
new KeyValuePair<DataKind, KeyedItems<ItemDescriptor>>(
199+
DataModel.Segments,
200+
new KeyedItems<ItemDescriptor>(ImmutableList.Create(
201+
new KeyValuePair<string, ItemDescriptor>("seg1", new ItemDescriptor(1, segment1))
202+
))
203+
)
204+
);
205+
206+
var changeSet = new ChangeSet<ItemDescriptor>(
207+
ChangeSetType.Full,
208+
Selector.Make(1, "state1"),
209+
changeSetData,
210+
null
211+
);
212+
213+
var result = DataStoreSorter.SortChangeset(changeSet);
214+
215+
var kinds = result.Data.Select(kv => kv.Key).ToList();
216+
Assert.Equal(2, kinds.Count);
217+
Assert.Equal(DataModel.Segments, kinds[0]);
218+
Assert.Equal(DataModel.Features, kinds[1]);
219+
}
220+
221+
[Fact]
222+
public void SortChangeset_HandlesEmptyChangeset()
223+
{
224+
var changeSet = new ChangeSet<ItemDescriptor>(
225+
ChangeSetType.Full,
226+
Selector.Make(1, "state1"),
227+
ImmutableList<KeyValuePair<DataKind, KeyedItems<ItemDescriptor>>>.Empty,
228+
null
229+
);
230+
231+
var result = DataStoreSorter.SortChangeset(changeSet);
232+
233+
Assert.Empty(result.Data);
234+
Assert.Equal(ChangeSetType.Full, result.Type);
235+
}
236+
237+
[Fact]
238+
public void SortChangeset_HandlesMultipleDataKinds()
239+
{
240+
var flag1 = new FeatureFlagBuilder("flag1").Version(1).Build();
241+
var segment1 = new SegmentBuilder("seg1").Version(1).Build();
242+
var segment2 = new SegmentBuilder("seg2").Version(2).Build();
243+
244+
var changeSetData = ImmutableList.Create(
245+
new KeyValuePair<DataKind, KeyedItems<ItemDescriptor>>(
246+
DataModel.Features,
247+
new KeyedItems<ItemDescriptor>(ImmutableList.Create(
248+
new KeyValuePair<string, ItemDescriptor>("flag1", new ItemDescriptor(1, flag1))
249+
))
250+
),
251+
new KeyValuePair<DataKind, KeyedItems<ItemDescriptor>>(
252+
DataModel.Segments,
253+
new KeyedItems<ItemDescriptor>(ImmutableList.Create(
254+
new KeyValuePair<string, ItemDescriptor>("seg1", new ItemDescriptor(1, segment1)),
255+
new KeyValuePair<string, ItemDescriptor>("seg2", new ItemDescriptor(2, segment2))
256+
))
257+
)
258+
);
259+
260+
var changeSet = new ChangeSet<ItemDescriptor>(
261+
ChangeSetType.Partial,
262+
Selector.Make(1, "state1"),
263+
changeSetData,
264+
null
265+
);
266+
267+
var result = DataStoreSorter.SortChangeset(changeSet);
268+
269+
Assert.Equal(2, result.Data.Count());
270+
271+
var segments = result.Data.First(kv => kv.Key == DataModel.Segments);
272+
Assert.Equal(2, segments.Value.Items.Count());
273+
274+
var flags = result.Data.First(kv => kv.Key == DataModel.Features);
275+
Assert.Single(flags.Value.Items);
276+
}
277+
278+
[Fact]
279+
public void SortChangeset_PreservesDeletedItems()
280+
{
281+
var changeSetData = ImmutableList.Create(
282+
new KeyValuePair<DataKind, KeyedItems<ItemDescriptor>>(
283+
DataModel.Features,
284+
new KeyedItems<ItemDescriptor>(ImmutableList.Create(
285+
new KeyValuePair<string, ItemDescriptor>("flag1", ItemDescriptor.Deleted(5))
286+
))
287+
)
288+
);
289+
290+
var changeSet = new ChangeSet<ItemDescriptor>(
291+
ChangeSetType.Partial,
292+
Selector.Make(1, "state1"),
293+
changeSetData,
294+
null
295+
);
296+
297+
var result = DataStoreSorter.SortChangeset(changeSet);
298+
299+
var flagsData = result.Data.First(kv => kv.Key == DataModel.Features);
300+
var item = flagsData.Value.Items.First();
301+
302+
Assert.Equal("flag1", item.Key);
303+
Assert.Null(item.Value.Item);
304+
Assert.Equal(5, item.Value.Version);
305+
}
306+
307+
#endregion
109308
}
110309
}

0 commit comments

Comments
 (0)