From 1c400a15a5c348d6722798ff57d8e08d24960848 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 27 Sep 2024 10:19:39 -0700 Subject: [PATCH] refactor: extract topographical sort logic into dedicated package (#196) The existing data source update sink has logic for computing a topological sort on the SDK's data set. This is ultimately used to insert data into a persistent store via `Init`. If we didn't do this, then (since stores are not atomic) evaluations for a given flag might require a prerequisite or segment that doesn't yet exist in the store. This logic is also needed in the FDv2 data system. Since it was an implementation detail of the existing v1 data sources, I've extracted it into a dedicated package. Finally, this commit modifies the types and methods to use graphing terminology, hoping to make its function more intuitive. --- .../datasource/data_model_dependencies.go | 164 ++--------------- .../data_model_dependencies_test.go | 104 +++++------ .../data_source_update_sink_impl.go | 20 ++- internal/toposort/toposort.go | 166 ++++++++++++++++++ 4 files changed, 246 insertions(+), 208 deletions(-) create mode 100644 internal/toposort/toposort.go diff --git a/internal/datasource/data_model_dependencies.go b/internal/datasource/data_model_dependencies.go index 7281eae0..b681b768 100644 --- a/internal/datasource/data_model_dependencies.go +++ b/internal/datasource/data_model_dependencies.go @@ -1,153 +1,21 @@ package datasource import ( - "sort" - - "github.com/launchdarkly/go-sdk-common/v3/ldvalue" - "github.com/launchdarkly/go-server-sdk-evaluation/v3/ldmodel" - "github.com/launchdarkly/go-server-sdk/v7/internal/datakinds" - "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoreimpl" + "github.com/launchdarkly/go-server-sdk/v7/internal/toposort" st "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoretypes" ) -type kindAndKey struct { - kind st.DataKind - key string -} - -// This set type is implemented as a map, but the values do not matter, just the keys. -type kindAndKeySet map[kindAndKey]bool - -func (s kindAndKeySet) add(value kindAndKey) { - s[value] = true -} - -func (s kindAndKeySet) contains(value kindAndKey) bool { - _, ok := s[value] - return ok -} - -func computeDependenciesFrom(kind st.DataKind, fromItem st.ItemDescriptor) kindAndKeySet { - // For any given flag or segment, find all the flags/segments that it directly references. - // Transitive references are handled by recursive logic at a higher level. - var ret kindAndKeySet - checkClauses := func(clauses []ldmodel.Clause) { - for _, c := range clauses { - if c.Op == ldmodel.OperatorSegmentMatch { - for _, v := range c.Values { - if v.Type() == ldvalue.StringType { - if ret == nil { - ret = make(kindAndKeySet) - } - ret.add(kindAndKey{datakinds.Segments, v.StringValue()}) - } - } - } - } - } - switch kind { - case ldstoreimpl.Features(): - if flag, ok := fromItem.Item.(*ldmodel.FeatureFlag); ok { - if len(flag.Prerequisites) > 0 { - ret = make(kindAndKeySet, len(flag.Prerequisites)) - for _, p := range flag.Prerequisites { - ret.add(kindAndKey{ldstoreimpl.Features(), p.Key}) - } - } - for _, r := range flag.Rules { - checkClauses(r.Clauses) - } - return ret - } - - case ldstoreimpl.Segments(): - if segment, ok := fromItem.Item.(*ldmodel.Segment); ok { - for _, r := range segment.Rules { - checkClauses(r.Clauses) - } - } - } - return ret -} - -func sortCollectionsForDataStoreInit(allData []st.Collection) []st.Collection { - colls := make([]st.Collection, 0, len(allData)) - for _, coll := range allData { - if doesDataKindSupportDependencies(coll.Kind) { - itemsOut := make([]st.KeyedItemDescriptor, 0, len(coll.Items)) - addItemsInDependencyOrder(coll.Kind, coll.Items, &itemsOut) - colls = append(colls, st.Collection{Kind: coll.Kind, Items: itemsOut}) - } else { - colls = append(colls, coll) - } - } - sort.Slice(colls, func(i, j int) bool { - return dataKindPriority(colls[i].Kind) < dataKindPriority(colls[j].Kind) - }) - return colls -} - -func doesDataKindSupportDependencies(kind st.DataKind) bool { - return kind == datakinds.Features //nolint:megacheck -} - -func addItemsInDependencyOrder( - kind st.DataKind, - itemsIn []st.KeyedItemDescriptor, - out *[]st.KeyedItemDescriptor, -) { - remainingItems := make(map[string]st.ItemDescriptor, len(itemsIn)) - for _, item := range itemsIn { - remainingItems[item.Key] = item.Item - } - for len(remainingItems) > 0 { - // pick a random item that hasn't been visited yet - for firstKey := range remainingItems { - addWithDependenciesFirst(kind, firstKey, remainingItems, out) - break - } - } -} - -func addWithDependenciesFirst( - kind st.DataKind, - startingKey string, - remainingItems map[string]st.ItemDescriptor, - out *[]st.KeyedItemDescriptor, -) { - startItem := remainingItems[startingKey] - delete(remainingItems, startingKey) // we won't need to visit this item again - for dep := range computeDependenciesFrom(kind, startItem) { - if dep.kind == kind { - if _, ok := remainingItems[dep.key]; ok { - addWithDependenciesFirst(kind, dep.key, remainingItems, out) - } - } - } - *out = append(*out, st.KeyedItemDescriptor{Key: startingKey, Item: startItem}) -} - -// Logic for ensuring that segments are processed before features; if we get any other data types that -// haven't been accounted for here, they'll come after those two in an arbitrary order. -func dataKindPriority(kind st.DataKind) int { - switch kind.GetName() { - case "segments": - return 0 - case "features": - return 1 - default: - return len(kind.GetName()) + 2 - } -} - // Maintains a bidirectional dependency graph that can be updated whenever an item has changed. type dependencyTracker struct { - dependenciesFrom map[kindAndKey]kindAndKeySet - dependenciesTo map[kindAndKey]kindAndKeySet + dependenciesFrom toposort.AdjacencyList + dependenciesTo toposort.AdjacencyList } func newDependencyTracker() *dependencyTracker { - return &dependencyTracker{make(map[kindAndKey]kindAndKeySet), make(map[kindAndKey]kindAndKeySet)} + return &dependencyTracker{ + make(toposort.AdjacencyList), + make(toposort.AdjacencyList), + } } // Updates the dependency graph when an item has changed. @@ -156,8 +24,8 @@ func (d *dependencyTracker) updateDependenciesFrom( fromKey string, fromItem st.ItemDescriptor, ) { - fromWhat := kindAndKey{kind, fromKey} - updatedDependencies := computeDependenciesFrom(kind, fromItem) + fromWhat := toposort.NewVertex(kind, fromKey) + updatedDependencies := toposort.GetNeighbors(kind, fromItem) oldDependencySet := d.dependenciesFrom[fromWhat] for oldDep := range oldDependencySet { @@ -171,23 +39,23 @@ func (d *dependencyTracker) updateDependenciesFrom( for newDep := range updatedDependencies { depsToThisNewDep := d.dependenciesTo[newDep] if depsToThisNewDep == nil { - depsToThisNewDep = make(kindAndKeySet) + depsToThisNewDep = make(toposort.Neighbors) d.dependenciesTo[newDep] = depsToThisNewDep } - depsToThisNewDep.add(fromWhat) + depsToThisNewDep.Add(fromWhat) } } func (d *dependencyTracker) reset() { - d.dependenciesFrom = make(map[kindAndKey]kindAndKeySet) - d.dependenciesTo = make(map[kindAndKey]kindAndKeySet) + d.dependenciesFrom = make(toposort.AdjacencyList) + d.dependenciesTo = make(toposort.AdjacencyList) } // Populates the given set with the union of the initial item and all items that directly or indirectly // depend on it (based on the current state of the dependency graph). -func (d *dependencyTracker) addAffectedItems(itemsOut kindAndKeySet, initialModifiedItem kindAndKey) { - if !itemsOut.contains(initialModifiedItem) { - itemsOut.add(initialModifiedItem) +func (d *dependencyTracker) addAffectedItems(itemsOut toposort.Neighbors, initialModifiedItem toposort.Vertex) { + if !itemsOut.Contains(initialModifiedItem) { + itemsOut.Add(initialModifiedItem) affectedItems := d.dependenciesTo[initialModifiedItem] for affectedItem := range affectedItems { d.addAffectedItems(itemsOut, affectedItem) diff --git a/internal/datasource/data_model_dependencies_test.go b/internal/datasource/data_model_dependencies_test.go index d7dea6bd..c878e143 100644 --- a/internal/datasource/data_model_dependencies_test.go +++ b/internal/datasource/data_model_dependencies_test.go @@ -4,6 +4,8 @@ import ( "strings" "testing" + "github.com/launchdarkly/go-server-sdk/v7/internal/toposort" + "github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest/mocks" "github.com/launchdarkly/go-sdk-common/v3/ldvalue" @@ -20,7 +22,7 @@ func TestComputeDependenciesFromFlag(t *testing.T) { flag1 := ldbuilders.NewFlagBuilder("key").Build() assert.Len( t, - computeDependenciesFrom(datakinds.Features, sharedtest.FlagDescriptor(flag1)), + toposort.GetNeighbors(datakinds.Features, sharedtest.FlagDescriptor(flag1)), 0, ) @@ -41,14 +43,14 @@ func TestComputeDependenciesFromFlag(t *testing.T) { Build() assert.Equal( t, - kindAndKeySet{ - {datakinds.Features, "flag2"}: true, - {datakinds.Features, "flag3"}: true, - {datakinds.Segments, "segment1"}: true, - {datakinds.Segments, "segment2"}: true, - {datakinds.Segments, "segment3"}: true, + toposort.Neighbors{ + toposort.NewVertex(datakinds.Features, "flag2"): struct{}{}, + toposort.NewVertex(datakinds.Features, "flag3"): struct{}{}, + toposort.NewVertex(datakinds.Segments, "segment1"): struct{}{}, + toposort.NewVertex(datakinds.Segments, "segment2"): struct{}{}, + toposort.NewVertex(datakinds.Segments, "segment3"): struct{}{}, }, - computeDependenciesFrom(datakinds.Features, sharedtest.FlagDescriptor(flag2)), + toposort.GetNeighbors(datakinds.Features, sharedtest.FlagDescriptor(flag2)), ) flag3 := ldbuilders.NewFlagBuilder("key"). @@ -61,11 +63,11 @@ func TestComputeDependenciesFromFlag(t *testing.T) { Build() assert.Equal( t, - kindAndKeySet{ - {datakinds.Segments, "segment1"}: true, - {datakinds.Segments, "segment2"}: true, + toposort.Neighbors{ + toposort.NewVertex(datakinds.Segments, "segment1"): struct{}{}, + toposort.NewVertex(datakinds.Segments, "segment2"): struct{}{}, }, - computeDependenciesFrom(datakinds.Features, sharedtest.FlagDescriptor(flag3)), + toposort.GetNeighbors(datakinds.Features, sharedtest.FlagDescriptor(flag3)), ) } @@ -73,7 +75,7 @@ func TestComputeDependenciesFromSegment(t *testing.T) { segment := ldbuilders.NewSegmentBuilder("segment").Build() assert.Len( t, - computeDependenciesFrom(datakinds.Segments, st.ItemDescriptor{Version: segment.Version, Item: &segment}), + toposort.GetNeighbors(datakinds.Segments, st.ItemDescriptor{Version: segment.Version, Item: &segment}), 0, ) } @@ -86,18 +88,18 @@ func TestComputeDependenciesFromSegmentWithSegmentReferences(t *testing.T) { Build() assert.Equal( t, - kindAndKeySet{ - {datakinds.Segments, "segment2"}: true, - {datakinds.Segments, "segment3"}: true, + toposort.Neighbors{ + toposort.NewVertex(datakinds.Segments, "segment2"): struct{}{}, + toposort.NewVertex(datakinds.Segments, "segment3"): struct{}{}, }, - computeDependenciesFrom(datakinds.Segments, st.ItemDescriptor{Version: segment1.Version, Item: &segment1}), + toposort.GetNeighbors(datakinds.Segments, st.ItemDescriptor{Version: segment1.Version, Item: &segment1}), ) } func TestComputeDependenciesFromUnknownDataKind(t *testing.T) { assert.Len( t, - computeDependenciesFrom(mocks.MockData, st.ItemDescriptor{Version: 1, Item: "x"}), + toposort.GetNeighbors(mocks.MockData, st.ItemDescriptor{Version: 1, Item: "x"}), 0, ) } @@ -105,14 +107,14 @@ func TestComputeDependenciesFromUnknownDataKind(t *testing.T) { func TestComputeDependenciesFromNullItem(t *testing.T) { assert.Len( t, - computeDependenciesFrom(datakinds.Features, st.ItemDescriptor{Version: 1, Item: nil}), + toposort.GetNeighbors(datakinds.Features, st.ItemDescriptor{Version: 1, Item: nil}), 0, ) } func TestSortCollectionsForDataStoreInit(t *testing.T) { inputData := makeDependencyOrderingDataSourceTestData() - sortedData := sortCollectionsForDataStoreInit(inputData) + sortedData := toposort.Sort(inputData) verifySortedData(t, sortedData, inputData) } @@ -132,7 +134,7 @@ func TestSortCollectionsLeavesItemsOfUnknownDataKindUnchanged(t *testing.T) { }}, {Kind: datakinds.Segments, Items: nil}, } - sortedData := sortCollectionsForDataStoreInit(inputData) + sortedData := toposort.Sort(inputData) // the unknown data kind appears last, and the ordering of its items is unchanged assert.Len(t, sortedData, 3) @@ -146,7 +148,7 @@ func TestDependencyTrackerReturnsSingleValueResultForUnknownItem(t *testing.T) { dt := newDependencyTracker() // a change to any item with no known depenencies affects only itself - verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag1", kindAndKey{datakinds.Features, "flag1"}) + verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag1", toposort.NewVertex(datakinds.Features, "flag1")) } func TestDependencyTrackerBuildsGraph(t *testing.T) { @@ -188,40 +190,40 @@ func TestDependencyTrackerBuildsGraph(t *testing.T) { // a change to flag1 affects only flag1 verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag1", - kindAndKey{datakinds.Features, "flag1"}, + toposort.NewVertex(datakinds.Features, "flag1"), ) // a change to flag2 affects flag2 and flag1 verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag2", - kindAndKey{datakinds.Features, "flag2"}, - kindAndKey{datakinds.Features, "flag1"}, + toposort.NewVertex(datakinds.Features, "flag2"), + toposort.NewVertex(datakinds.Features, "flag1"), ) // a change to flag3 affects flag3 and flag1 verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag3", - kindAndKey{datakinds.Features, "flag3"}, - kindAndKey{datakinds.Features, "flag1"}, + toposort.NewVertex(datakinds.Features, "flag3"), + toposort.NewVertex(datakinds.Features, "flag1"), ) // a change to segment1 affects segment1 and flag1 verifyDependencyAffectedItems(t, dt, datakinds.Segments, "segment1", - kindAndKey{datakinds.Segments, "segment1"}, - kindAndKey{datakinds.Features, "flag1"}, + toposort.NewVertex(datakinds.Segments, "segment1"), + toposort.NewVertex(datakinds.Features, "flag1"), ) // a change to segment2 affects segment2, flag1, and flag2 verifyDependencyAffectedItems(t, dt, datakinds.Segments, "segment2", - kindAndKey{datakinds.Segments, "segment2"}, - kindAndKey{datakinds.Features, "flag1"}, - kindAndKey{datakinds.Features, "flag2"}, + toposort.NewVertex(datakinds.Segments, "segment2"), + toposort.NewVertex(datakinds.Features, "flag1"), + toposort.NewVertex(datakinds.Features, "flag2"), ) // a change to segment3 affects segment2, which affects flag1 and flag2 verifyDependencyAffectedItems(t, dt, datakinds.Segments, "segment3", - kindAndKey{datakinds.Segments, "segment3"}, - kindAndKey{datakinds.Segments, "segment2"}, - kindAndKey{datakinds.Features, "flag1"}, - kindAndKey{datakinds.Features, "flag2"}, + toposort.NewVertex(datakinds.Segments, "segment3"), + toposort.NewVertex(datakinds.Segments, "segment2"), + toposort.NewVertex(datakinds.Features, "flag1"), + toposort.NewVertex(datakinds.Features, "flag2"), ) } @@ -240,9 +242,9 @@ func TestDependencyTrackerUpdatesGraph(t *testing.T) { // at this point, a change to flag3 affects flag3, flag2, and flag1 verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag3", - kindAndKey{datakinds.Features, "flag3"}, - kindAndKey{datakinds.Features, "flag2"}, - kindAndKey{datakinds.Features, "flag1"}, + toposort.NewVertex(datakinds.Features, "flag3"), + toposort.NewVertex(datakinds.Features, "flag2"), + toposort.NewVertex(datakinds.Features, "flag1"), ) // now make it so flag1 now depends on flag4 instead of flag2 @@ -253,14 +255,14 @@ func TestDependencyTrackerUpdatesGraph(t *testing.T) { // now, a change to flag3 affects flag3 and flag2 verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag3", - kindAndKey{datakinds.Features, "flag3"}, - kindAndKey{datakinds.Features, "flag2"}, + toposort.NewVertex(datakinds.Features, "flag3"), + toposort.NewVertex(datakinds.Features, "flag2"), ) // and a change to flag4 affects flag4 and flag1 verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag4", - kindAndKey{datakinds.Features, "flag4"}, - kindAndKey{datakinds.Features, "flag1"}, + toposort.NewVertex(datakinds.Features, "flag4"), + toposort.NewVertex(datakinds.Features, "flag1"), ) } @@ -273,14 +275,14 @@ func TestDependencyTrackerResetsGraph(t *testing.T) { dt.updateDependenciesFrom(datakinds.Features, flag1.Key, st.ItemDescriptor{Version: flag1.Version, Item: &flag1}) verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag3", - kindAndKey{datakinds.Features, "flag3"}, - kindAndKey{datakinds.Features, "flag1"}, + toposort.NewVertex(datakinds.Features, "flag3"), + toposort.NewVertex(datakinds.Features, "flag1"), ) dt.reset() verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag3", - kindAndKey{datakinds.Features, "flag3"}, + toposort.NewVertex(datakinds.Features, "flag3"), ) } @@ -289,14 +291,14 @@ func verifyDependencyAffectedItems( dt *dependencyTracker, kind st.DataKind, key string, - expected ...kindAndKey, + expected ...toposort.Vertex, ) { - expectedSet := make(kindAndKeySet) + expectedSet := make(toposort.Neighbors) for _, value := range expected { - expectedSet.add(value) + expectedSet.Add(value) } - result := make(kindAndKeySet) - dt.addAffectedItems(result, kindAndKey{kind, key}) + result := make(toposort.Neighbors) + dt.addAffectedItems(result, toposort.NewVertex(kind, key)) assert.Equal(t, expectedSet, result) } diff --git a/internal/datasource/data_source_update_sink_impl.go b/internal/datasource/data_source_update_sink_impl.go index 7d349401..7ce4ab21 100644 --- a/internal/datasource/data_source_update_sink_impl.go +++ b/internal/datasource/data_source_update_sink_impl.go @@ -5,6 +5,8 @@ import ( "sync" "time" + "github.com/launchdarkly/go-server-sdk/v7/internal/toposort" + "github.com/launchdarkly/go-sdk-common/v3/ldlog" intf "github.com/launchdarkly/go-server-sdk/v7/interfaces" "github.com/launchdarkly/go-server-sdk/v7/internal" @@ -71,7 +73,7 @@ func (d *DataSourceUpdateSinkImpl) Init(allData []st.Collection) bool { } } - err := d.store.Init(sortCollectionsForDataStoreInit(allData)) + err := d.store.Init(toposort.Sort(allData)) updated := d.maybeUpdateError(err) if updated { @@ -101,8 +103,8 @@ func (d *DataSourceUpdateSinkImpl) Upsert( if updated { d.dependencyTracker.updateDependenciesFrom(kind, key, item) if d.flagChangeEventBroadcaster.HasListeners() { - affectedItems := make(kindAndKeySet) - d.dependencyTracker.addAffectedItems(affectedItems, kindAndKey{kind, key}) + affectedItems := make(toposort.Neighbors) + d.dependencyTracker.addAffectedItems(affectedItems, toposort.NewVertex(kind, key)) d.sendChangeEvents(affectedItems) } } @@ -235,10 +237,10 @@ func (d *DataSourceUpdateSinkImpl) waitFor(desiredState intf.DataSourceState, ti } } -func (d *DataSourceUpdateSinkImpl) sendChangeEvents(affectedItems kindAndKeySet) { +func (d *DataSourceUpdateSinkImpl) sendChangeEvents(affectedItems toposort.Neighbors) { for item := range affectedItems { - if item.kind == datakinds.Features { - d.flagChangeEventBroadcaster.Broadcast(intf.FlagChangeEvent{Key: item.key}) + if item.Kind() == datakinds.Features { + d.flagChangeEventBroadcaster.Broadcast(intf.FlagChangeEvent{Key: item.Key()}) } } } @@ -267,8 +269,8 @@ func fullDataSetToMap(allData []st.Collection) map[st.DataKind]map[string]st.Ite func (d *DataSourceUpdateSinkImpl) computeChangedItemsForFullDataSet( oldDataMap map[st.DataKind]map[string]st.ItemDescriptor, newDataMap map[st.DataKind]map[string]st.ItemDescriptor, -) kindAndKeySet { - affectedItems := make(kindAndKeySet) +) toposort.Neighbors { + affectedItems := make(toposort.Neighbors) for _, kind := range datakinds.AllDataKinds() { oldItems := oldDataMap[kind] newItems := newDataMap[kind] @@ -286,7 +288,7 @@ func (d *DataSourceUpdateSinkImpl) computeChangedItemsForFullDataSet( newItem, haveNew := newItems[key] if haveOld || haveNew { if !haveOld || !haveNew || oldItem.Version < newItem.Version { - d.dependencyTracker.addAffectedItems(affectedItems, kindAndKey{kind, key}) + d.dependencyTracker.addAffectedItems(affectedItems, toposort.NewVertex(kind, key)) } } } diff --git a/internal/toposort/toposort.go b/internal/toposort/toposort.go new file mode 100644 index 00000000..7b5fc70e --- /dev/null +++ b/internal/toposort/toposort.go @@ -0,0 +1,166 @@ +// Package toposort provides a topological sort for segments/flags based on their dependencies. +package toposort + +import ( + "sort" + + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" + "github.com/launchdarkly/go-server-sdk-evaluation/v3/ldmodel" + "github.com/launchdarkly/go-server-sdk/v7/internal/datakinds" + "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoreimpl" + st "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoretypes" +) + +// AdjacencyList is a map of vertices (kind/key) to neighbors (dependencies). +type AdjacencyList map[Vertex]Neighbors + +// Neighbors is a set of vertices. It is used instead of a list for efficient lookup. +type Neighbors map[Vertex]struct{} + +// Add adds a vertex to the set. +func (s Neighbors) Add(value Vertex) { + s[value] = struct{}{} +} + +// Contains returns true if the set contains the vertex. +func (s Neighbors) Contains(value Vertex) bool { + _, ok := s[value] + return ok +} + +// Vertex represents a particular data item, identified by kind + key. +type Vertex struct { + kind st.DataKind + key string +} + +// NewVertex constructs a Vertex. +func NewVertex(kind st.DataKind, key string) Vertex { + return Vertex{kind, key} +} + +// Kind returns the data kind of the vertex. +func (v Vertex) Kind() st.DataKind { + return v.kind +} + +// Key returns the key of the vertex. +func (v Vertex) Key() string { + return v.key +} + +func doesDataKindSupportDependencies(kind st.DataKind) bool { + return kind == datakinds.Features //nolint:megacheck +} + +// Logic for ensuring that segments are processed before features; if we get any other data types that +// haven't been accounted for here, they'll come after those two in an arbitrary order. +func dataKindPriority(kind st.DataKind) int { + switch kind.GetName() { + case "segments": + return 0 + case "features": + return 1 + default: + return len(kind.GetName()) + 2 + } +} + +func addItemsInDependencyOrder( + kind st.DataKind, + itemsIn []st.KeyedItemDescriptor, + out *[]st.KeyedItemDescriptor, +) { + remainingItems := make(map[string]st.ItemDescriptor, len(itemsIn)) + for _, item := range itemsIn { + remainingItems[item.Key] = item.Item + } + for len(remainingItems) > 0 { + // pick a random item that hasn't been visited yet + for firstKey := range remainingItems { + addWithDependenciesFirst(kind, firstKey, remainingItems, out) + break + } + } +} + +func addWithDependenciesFirst( + kind st.DataKind, + startingKey string, + remainingItems map[string]st.ItemDescriptor, + out *[]st.KeyedItemDescriptor, +) { + startItem := remainingItems[startingKey] + delete(remainingItems, startingKey) // we won't need to visit this item again + for dep := range GetNeighbors(kind, startItem) { + if dep.kind == kind { + if _, ok := remainingItems[dep.key]; ok { + addWithDependenciesFirst(kind, dep.key, remainingItems, out) + } + } + } + *out = append(*out, st.KeyedItemDescriptor{Key: startingKey, Item: startItem}) +} + +// GetNeighbors returns all direct neighbors of the given item. +func GetNeighbors(kind st.DataKind, fromItem st.ItemDescriptor) Neighbors { + // For any given flag or segment, find all the flags/segments that it directly references. + // Transitive references are handled by recursive logic at a higher level. + var ret Neighbors + checkClauses := func(clauses []ldmodel.Clause) { + for _, c := range clauses { + if c.Op == ldmodel.OperatorSegmentMatch { + for _, v := range c.Values { + if v.Type() == ldvalue.StringType { + if ret == nil { + ret = make(Neighbors) + } + ret.Add(Vertex{datakinds.Segments, v.StringValue()}) + } + } + } + } + } + switch kind { + case ldstoreimpl.Features(): + if flag, ok := fromItem.Item.(*ldmodel.FeatureFlag); ok { + if len(flag.Prerequisites) > 0 { + ret = make(Neighbors, len(flag.Prerequisites)) + for _, p := range flag.Prerequisites { + ret.Add(Vertex{ldstoreimpl.Features(), p.Key}) + } + } + for _, r := range flag.Rules { + checkClauses(r.Clauses) + } + return ret + } + + case ldstoreimpl.Segments(): + if segment, ok := fromItem.Item.(*ldmodel.Segment); ok { + for _, r := range segment.Rules { + checkClauses(r.Clauses) + } + } + } + return ret +} + +// Sort performs a topological sort on the given data collections, so that the items can be inserted into a +// persistent store to minimize the risk of evaluating a flag before its prerequisites/segments have been stored. +func Sort(allData []st.Collection) []st.Collection { + colls := make([]st.Collection, 0, len(allData)) + for _, coll := range allData { + if doesDataKindSupportDependencies(coll.Kind) { + itemsOut := make([]st.KeyedItemDescriptor, 0, len(coll.Items)) + addItemsInDependencyOrder(coll.Kind, coll.Items, &itemsOut) + colls = append(colls, st.Collection{Kind: coll.Kind, Items: itemsOut}) + } else { + colls = append(colls, coll) + } + } + sort.Slice(colls, func(i, j int) bool { + return dataKindPriority(colls[i].Kind) < dataKindPriority(colls[j].Kind) + }) + return colls +}