Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 0 additions & 67 deletions internal/datasystem/data_model_dependencies.go

This file was deleted.

14 changes: 7 additions & 7 deletions internal/datasystem/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ type Store struct {

// Used to track dependencies between items in the store. This helps ensure
// we trigger the correct flag change notifications.
dependencyTracker *dependencyTracker
dependencyTracker *toposort.DependencyTracker

// Broadcaster for flag change events.
flagChangeEvent *internal.Broadcaster[interfaces.FlagChangeEvent]
Expand Down Expand Up @@ -128,7 +128,7 @@ func NewStore(
s := &Store{
persistentStore: nil,
memoryStore: memorystorev2.New(loggers),
dependencyTracker: newDependencyTracker(),
dependencyTracker: toposort.NewDependencyTracker(),
flagChangeEvent: flagChangeEvent,
changeSetBroadcaster: changeSet,
loggers: loggers,
Expand Down Expand Up @@ -270,9 +270,9 @@ func (s *Store) applyDelta(collections []ldstoretypes.Collection, selector subsy
affectedItems := make(toposort.Neighbors)
for _, collection := range collections {
for _, item := range collection.Items {
s.dependencyTracker.updateDependenciesFrom(collection.Kind, item.Key, item.Item)
s.dependencyTracker.UpdateDependenciesFrom(collection.Kind, item.Key, item.Item)
if hasListeners {
s.dependencyTracker.addAffectedItems(affectedItems, toposort.NewVertex(collection.Kind, item.Key))
s.dependencyTracker.AddAffectedItems(affectedItems, toposort.NewVertex(collection.Kind, item.Key))
}
}
}
Expand Down Expand Up @@ -377,10 +377,10 @@ func (s *Store) IsInitialized() bool {
// internal.datasource. This duplication will be removed when FDv1 support is
// removed.
func (s *Store) updateDependencyTrackerFromFullDataSet(allData []ldstoretypes.Collection) {
s.dependencyTracker.reset()
s.dependencyTracker.Reset()
for _, coll := range allData {
for _, item := range coll.Items {
s.dependencyTracker.updateDependenciesFrom(coll.Kind, item.Key, item.Item)
s.dependencyTracker.UpdateDependenciesFrom(coll.Kind, item.Key, item.Item)
}
}
}
Expand Down Expand Up @@ -421,7 +421,7 @@ func (s *Store) computeChangedItemsForFullDataSet(

if haveOld || haveNew {
if !haveOld || !haveNew || oldItem.Version < newItem.Version {
s.dependencyTracker.addAffectedItems(affectedItems, toposort.NewVertex(kind, key))
s.dependencyTracker.AddAffectedItems(affectedItems, toposort.NewVertex(kind, key))
}
}
}
Expand Down
69 changes: 69 additions & 0 deletions internal/toposort/dependency_tracker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package toposort

import (
st "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoretypes"
)

// DependencyTracker maintains a bidirectional dependency graph that can be updated whenever an
// item has changed.
//
// A nearly identical unexported implementation exists in internal/datasource for the FDv1 code
// path; that copy will go away when FDv1 is removed.
type DependencyTracker struct {
dependenciesFrom AdjacencyList
dependenciesTo AdjacencyList
}

// NewDependencyTracker creates a DependencyTracker with an empty dependency graph.
func NewDependencyTracker() *DependencyTracker {
return &DependencyTracker{
make(AdjacencyList),
make(AdjacencyList),
}
}

// UpdateDependenciesFrom updates the dependency graph when an item has changed.
func (d *DependencyTracker) UpdateDependenciesFrom(
kind st.DataKind,
fromKey string,
fromItem st.ItemDescriptor,
) {
fromWhat := NewVertex(kind, fromKey)
updatedDependencies := GetNeighbors(kind, fromItem)

oldDependencySet := d.dependenciesFrom[fromWhat]
for oldDep := range oldDependencySet {
depsToThisOldDep := d.dependenciesTo[oldDep]
if depsToThisOldDep != nil {
delete(depsToThisOldDep, fromWhat)
}
}

d.dependenciesFrom[fromWhat] = updatedDependencies
for newDep := range updatedDependencies {
depsToThisNewDep := d.dependenciesTo[newDep]
if depsToThisNewDep == nil {
depsToThisNewDep = make(Neighbors)
d.dependenciesTo[newDep] = depsToThisNewDep
}
depsToThisNewDep.Add(fromWhat)
}
}

// Reset clears the dependency graph.
func (d *DependencyTracker) Reset() {
d.dependenciesFrom = make(AdjacencyList)
d.dependenciesTo = make(AdjacencyList)
}

// AddAffectedItems 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 Neighbors, initialModifiedItem Vertex) {
if !itemsOut.Contains(initialModifiedItem) {
itemsOut.Add(initialModifiedItem)
affectedItems := d.dependenciesTo[initialModifiedItem]
for affectedItem := range affectedItems {
d.AddAffectedItems(itemsOut, affectedItem)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package datasystem
package toposort_test

import (
"strings"
Expand Down Expand Up @@ -145,14 +145,14 @@ func TestSortCollectionsLeavesItemsOfUnknownDataKindUnchanged(t *testing.T) {
}

func TestDependencyTrackerReturnsSingleValueResultForUnknownItem(t *testing.T) {
dt := newDependencyTracker()
dt := toposort.NewDependencyTracker()

// a change to any item with no known depenencies affects only itself
verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag1", toposort.NewVertex(datakinds.Features, "flag1"))
}

func TestDependencyTrackerBuildsGraph(t *testing.T) {
dt := newDependencyTracker()
dt := toposort.NewDependencyTracker()

segment3 := ldbuilders.NewSegmentBuilder("segment3").Build()
segment2 := ldbuilders.NewSegmentBuilder("segment2").
Expand Down Expand Up @@ -182,10 +182,10 @@ func TestDependencyTrackerBuildsGraph(t *testing.T) {
Build()

for _, s := range []ldmodel.Segment{segment1, segment2, segment3} {
dt.updateDependenciesFrom(datakinds.Segments, s.Key, sharedtest.SegmentDescriptor(s))
dt.UpdateDependenciesFrom(datakinds.Segments, s.Key, sharedtest.SegmentDescriptor(s))
}
for _, f := range []ldmodel.FeatureFlag{flag1, flag2} {
dt.updateDependenciesFrom(datakinds.Features, f.Key, sharedtest.FlagDescriptor(f))
dt.UpdateDependenciesFrom(datakinds.Features, f.Key, sharedtest.FlagDescriptor(f))
}

// a change to flag1 affects only flag1
Expand Down Expand Up @@ -228,17 +228,17 @@ func TestDependencyTrackerBuildsGraph(t *testing.T) {
}

func TestDependencyTrackerUpdatesGraph(t *testing.T) {
dt := newDependencyTracker()
dt := toposort.NewDependencyTracker()

flag1 := ldbuilders.NewFlagBuilder("flag1").
AddPrerequisite("flag3", 0).
Build()
dt.updateDependenciesFrom(datakinds.Features, flag1.Key, st.ItemDescriptor{Version: flag1.Version, Item: &flag1})
dt.UpdateDependenciesFrom(datakinds.Features, flag1.Key, st.ItemDescriptor{Version: flag1.Version, Item: &flag1})

flag2 := ldbuilders.NewFlagBuilder("flag2").
AddPrerequisite("flag3", 0).
Build()
dt.updateDependenciesFrom(datakinds.Features, flag2.Key, st.ItemDescriptor{Version: flag2.Version, Item: &flag2})
dt.UpdateDependenciesFrom(datakinds.Features, flag2.Key, st.ItemDescriptor{Version: flag2.Version, Item: &flag2})

// at this point, a change to flag3 affects flag3, flag2, and flag1
verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag3",
Expand All @@ -251,7 +251,7 @@ func TestDependencyTrackerUpdatesGraph(t *testing.T) {
flag1v2 := ldbuilders.NewFlagBuilder("flag1").
AddPrerequisite("flag4", 0).
Build()
dt.updateDependenciesFrom(datakinds.Features, flag1.Key, st.ItemDescriptor{Version: flag1v2.Version, Item: &flag1v2})
dt.UpdateDependenciesFrom(datakinds.Features, flag1.Key, st.ItemDescriptor{Version: flag1v2.Version, Item: &flag1v2})

// now, a change to flag3 affects flag3 and flag2
verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag3",
Expand All @@ -267,19 +267,19 @@ func TestDependencyTrackerUpdatesGraph(t *testing.T) {
}

func TestDependencyTrackerResetsGraph(t *testing.T) {
dt := newDependencyTracker()
dt := toposort.NewDependencyTracker()

flag1 := ldbuilders.NewFlagBuilder("flag1").
AddPrerequisite("flag3", 0).
Build()
dt.updateDependenciesFrom(datakinds.Features, flag1.Key, st.ItemDescriptor{Version: flag1.Version, Item: &flag1})
dt.UpdateDependenciesFrom(datakinds.Features, flag1.Key, st.ItemDescriptor{Version: flag1.Version, Item: &flag1})

verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag3",
toposort.NewVertex(datakinds.Features, "flag3"),
toposort.NewVertex(datakinds.Features, "flag1"),
)

dt.reset()
dt.Reset()

verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag3",
toposort.NewVertex(datakinds.Features, "flag3"),
Expand All @@ -288,7 +288,7 @@ func TestDependencyTrackerResetsGraph(t *testing.T) {

func verifyDependencyAffectedItems(
t *testing.T,
dt *dependencyTracker,
dt *toposort.DependencyTracker,
kind st.DataKind,
key string,
expected ...toposort.Vertex,
Expand All @@ -298,7 +298,7 @@ func verifyDependencyAffectedItems(
expectedSet.Add(value)
}
result := make(toposort.Neighbors)
dt.addAffectedItems(result, toposort.NewVertex(kind, key))
dt.AddAffectedItems(result, toposort.NewVertex(kind, key))
assert.Equal(t, expectedSet, result)
}

Expand Down Expand Up @@ -355,3 +355,15 @@ func verifySortedData(t *testing.T, sortedData []st.Collection, inputData []st.C
}
}
}

func fullDataSetToMap(allData []st.Collection) map[st.DataKind]map[string]st.ItemDescriptor {
ret := make(map[st.DataKind]map[string]st.ItemDescriptor, len(allData))
for _, coll := range allData {
m := make(map[string]st.ItemDescriptor, len(coll.Items))
for _, item := range coll.Items {
m[item.Key] = item.Item
}
ret[coll.Kind] = m
}
return ret
}
Loading