From 253b197aeb2ae0ea1e090b8d44606479dc0f2f4b Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Tue, 2 Apr 2024 17:01:17 -0400 Subject: [PATCH 01/23] update --- contrib/pkg/sets/v2/sets.go | 249 ++++++++++++++++------------- contrib/tests/set_v2_test.go | 292 ++++++++++++++++++++++++----------- 2 files changed, 343 insertions(+), 198 deletions(-) diff --git a/contrib/pkg/sets/v2/sets.go b/contrib/pkg/sets/v2/sets.go index a88dac62c..7261eab06 100644 --- a/contrib/pkg/sets/v2/sets.go +++ b/contrib/pkg/sets/v2/sets.go @@ -1,6 +1,7 @@ package sets_v2 import ( + "slices" "sync" sk_sets "github.com/solo-io/skv2/contrib/pkg/sets" @@ -12,21 +13,20 @@ import ( type ResourceSet[T client.Object] interface { // Get the set stored keys - Keys() sets.String - // List of resources stored in the set. Pass an optional filter function to filter on the list. - List(filterResource ...func(T) bool) []T - // Unsorted list of resources stored in the set. Pass an optional filter function to filter on the list. - UnsortedList(filterResource ...func(T) bool) []T + Keys() sets.Set[string] + // List returns an iterator for the set. + // Pass an optional filter function to skip iteration on specific entries; Note: index will still progress. + List(filterResource ...func(T) bool) func(yield func(int, T) bool) // Return the Set as a map of key to resource. Map() map[string]T // Insert a resource into the set. Insert(resource ...T) // Compare the equality of the keys in two sets (not the resources themselves) Equal(set ResourceSet[T]) bool - // Check if the set contains a key matching the resource (not the resource itself) + // Check if the set contains the resource. Has(resource T) bool - // Delete the key matching the resource - Delete(resource T) + // Delete the matching resource. + Delete(resource ezkube.ResourceId) // Return the union with the provided set Union(set ResourceSet[T]) ResourceSet[T] // Return the difference with the provided set @@ -34,15 +34,17 @@ type ResourceSet[T client.Object] interface { // Return the intersection with the provided set Intersection(set ResourceSet[T]) ResourceSet[T] // Find the resource with the given ID - Find(id ezkube.ResourceId) (T, error) + Find(resource ezkube.ResourceId) (T, error) // Get the length of the set - Length() int + Len() int // returns the generic implementation of the set Generic() sk_sets.ResourceSet // returns the delta between this and and another ResourceSet[T] Delta(newSet ResourceSet[T]) sk_sets.ResourceDelta // Clone returns a deep copy of the set Clone() ResourceSet[T] + // Get the compare function used by the set + GetCompareFunc() func(a, b ezkube.ResourceId) int } // ResourceDelta represents the set of changes between two ResourceSets. @@ -61,93 +63,91 @@ func (r *ResourceDelta[T]) DeltaV1() sk_sets.ResourceDelta { } type resourceSet[T client.Object] struct { - lock sync.RWMutex - set sets.String - mapping map[string]T + lock sync.RWMutex + set []T + compareFunc func(a, b ezkube.ResourceId) int } -func NewResourceSet[T client.Object](resources ...T) ResourceSet[T] { - set := sets.NewString() - mapping := map[string]T{} - for _, resource := range resources { - key := sk_sets.Key(resource) - set.Insert(key) - mapping[key] = resource +func NewResourceSet[T client.Object]( + compareFunc func(a, b ezkube.ResourceId) int, + resources ...T, +) ResourceSet[T] { + rs := &resourceSet[T]{ + set: []T{}, + compareFunc: compareFunc, } - return &resourceSet[T]{set: set, mapping: mapping} + rs.Insert(resources...) + return rs } -func (s *resourceSet[T]) Keys() sets.String { - return sets.NewString(s.set.List()...) -} - -func (s *resourceSet[T]) List(filterResource ...func(T) bool) []T { +func (s *resourceSet[T]) Keys() sets.Set[string] { s.lock.RLock() defer s.lock.RUnlock() - var resources []T - for _, key := range s.set.List() { - var filtered bool - for _, filter := range filterResource { - if filter(s.mapping[key]) { - filtered = true - break - } - } - if !filtered { - resources = append(resources, s.mapping[key]) - } + keys := sets.Set[string]{} + for _, resource := range s.set { + keys.Insert(sk_sets.Key(resource)) } - return resources + return sets.Set[string]{} } -func (s *resourceSet[T]) UnsortedList(filterResource ...func(T) bool) []T { +func (s *resourceSet[T]) List(filterResource ...func(T) bool) func(yield func(int, T) bool) { s.lock.RLock() defer s.lock.RUnlock() - - keys := s.set.UnsortedList() - resources := make([]T, 0, len(keys)) - - for _, key := range keys { - var filtered bool - for _, filter := range filterResource { - if filter(s.mapping[key]) { - filtered = true + return func(yield func(int, T) bool) { + OUTER: + for i, resource := range s.set { + for _, filter := range filterResource { + if filter(resource) { + continue OUTER + } + } + if !yield(i, resource) { break } } - if !filtered { - resources = append(resources, s.mapping[key]) - } } - return resources } func (s *resourceSet[T]) Map() map[string]T { s.lock.RLock() defer s.lock.RUnlock() newMap := map[string]T{} - for k, v := range s.mapping { - newMap[k] = v + for _, resource := range s.set { + newMap[sk_sets.Key(resource)] = resource } return newMap } -func (s *resourceSet[T]) Insert( - resources ...T, -) { - s.lock.Lock() - defer s.lock.Unlock() +// Insert adds items to the set. +// If an item is already in the set, it is overwritten. +// The set is sorted based on the compare func. +func (s *resourceSet[T]) Insert(resources ...T) { + s.lock.RLock() + defer s.lock.RUnlock() for _, resource := range resources { - key := sk_sets.Key(resource) - s.mapping[key] = resource - s.set.Insert(key) + insertIndex, found := slices.BinarySearchFunc( + s.set, + resource, + func(a, b T) int { return s.compareFunc(a, b) }, + ) + if found { + s.set[insertIndex] = resource + continue + } + // insert the resource at the determined index + s.set = slices.Insert(s.set, insertIndex, resource) } } func (s *resourceSet[T]) Has(resource T) bool { s.lock.RLock() defer s.lock.RUnlock() - return s.set.Has(sk_sets.Key(resource)) + _, found := slices.BinarySearchFunc( + s.set, + resource, + func(a, b T) int { return s.compareFunc(a, b) }, + ) + return found } func (s *resourceSet[T]) Equal( @@ -155,94 +155,117 @@ func (s *resourceSet[T]) Equal( ) bool { s.lock.RLock() defer s.lock.RUnlock() - return s.set.Equal(set.Keys()) + return s.Generic().Equal(set.Generic()) } -func (s *resourceSet[T]) Delete(resource T) { +func (s *resourceSet[T]) Delete(resource ezkube.ResourceId) { s.lock.Lock() defer s.lock.Unlock() - key := sk_sets.Key(resource) - delete(s.mapping, key) - s.set.Delete(key) + + i, found := slices.BinarySearchFunc( + s.set, + resource, + func(a T, b ezkube.ResourceId) int { return s.compareFunc(a, b) }, + ) + if found { + s.set = slices.Delete(s.set, i, i+1) + } } func (s *resourceSet[T]) Union(set ResourceSet[T]) ResourceSet[T] { - return NewResourceSet[T](append(s.List(), set.List()...)...) + list := []T{} + for _, resource := range s.Generic().Union(set.Generic()).List() { + list = append(list, resource.(T)) + } + return NewResourceSet[T]( + s.compareFunc, + list..., + ) } func (s *resourceSet[T]) Difference(set ResourceSet[T]) ResourceSet[T] { s.lock.RLock() defer s.lock.RUnlock() - newSet := s.set.Difference(set.Keys()) - var newResources []T - for key, _ := range newSet { - val, _ := s.mapping[key] - newResources = append(newResources, val) + result := NewResourceSet[T](s.compareFunc) + for _, resource := range s.set { + if !set.Has(resource) { + result.Insert(resource) + } } - return NewResourceSet[T](newResources...) + return result } func (s *resourceSet[T]) Intersection(set ResourceSet[T]) ResourceSet[T] { s.lock.RLock() defer s.lock.RUnlock() - newSet := s.set.Intersection(set.Keys()) - var newResources []T - for key, _ := range newSet { - val, _ := s.mapping[key] - newResources = append(newResources, val) + var walk, other ResourceSet[T] + result := NewResourceSet[T](s.compareFunc) + if len(s.set) < set.Len() { + walk = NewResourceSet(s.compareFunc, s.set...) + other = set + } else { + walk = set + other = NewResourceSet(s.compareFunc, s.set...) } - return NewResourceSet[T](newResources...) + walk.List()(func(_ int, key T) bool { + if other.Has(key) { + result.Insert(key) + } + return true + }) + return result } -func (s *resourceSet[T]) Find( - id ezkube.ResourceId, -) (T, error) { +func (s *resourceSet[T]) Find(resource ezkube.ResourceId) (T, error) { s.lock.RLock() defer s.lock.RUnlock() - key := sk_sets.Key(id) - resource, ok := s.mapping[key] - if !ok { - return resource, sk_sets.NotFoundErr(resource, id) + + insertIndex, found := slices.BinarySearchFunc( + s.set, + resource, + func(a T, b ezkube.ResourceId) int { return s.compareFunc(a, b) }, + ) + if found { + return s.set[insertIndex], nil } - return resource, nil + var r T + return r, sk_sets.NotFoundErr(r, resource) } -func (s *resourceSet[T]) Length() int { - +func (s *resourceSet[T]) Len() int { s.lock.RLock() defer s.lock.RUnlock() - return len(s.mapping) + return len(s.set) } // note that this function will currently panic if called for a ResourceSet[T] containing non-runtime.Objects func (oldSet *resourceSet[T]) Delta(newSet ResourceSet[T]) sk_sets.ResourceDelta { - updated, removed := NewResourceSet[T](), NewResourceSet[T]() + updated, removed := NewResourceSet[T](oldSet.compareFunc), NewResourceSet[T](oldSet.compareFunc) // find objects updated or removed - oldSet.List(func(oldObj T) bool { - newObj, err := newSet.Find(oldObj) + for _, resource := range oldSet.set { + newObj, err := newSet.Find(resource) switch { case err != nil: // obj removed - removed.Insert(oldObj) - case !controllerutils.ObjectsEqual(oldObj, newObj): + removed.Insert(resource) + case !controllerutils.ObjectsEqual(resource, newObj): // obj updated updated.Insert(newObj) default: // obj the same } - return true // return value ignored - }) + } // find objects added - newSet.List(func(newObj T) bool { - if _, err := oldSet.Find(newObj); err != nil { + for _, newObj := range newSet.Generic().List() { + if _, err := oldSet.Find(newObj.(T)); err != nil { // obj added - updated.Insert(newObj) + updated.Insert(newObj.(T)) } - return true // return value ignored - }) + } + delta := &ResourceDelta[T]{ Inserted: updated, Removed: removed, @@ -253,8 +276,9 @@ func (oldSet *resourceSet[T]) Delta(newSet ResourceSet[T]) sk_sets.ResourceDelta // Create a clone of the current set // note that this function will currently panic if called for a ResourceSet[T] containing non-runtime.Objects func (oldSet *resourceSet[T]) Clone() ResourceSet[T] { - new := NewResourceSet[T]() - oldSet.List(func(oldObj T) bool { + new := NewResourceSet[T](oldSet.compareFunc) + + oldSet.List()(func(_ int, oldObj T) bool { copy := oldObj.DeepCopyObject().(T) new.Insert(copy) return true @@ -263,9 +287,14 @@ func (oldSet *resourceSet[T]) Clone() ResourceSet[T] { } func (s *resourceSet[T]) Generic() sk_sets.ResourceSet { - set := sk_sets.NewResourceSet() - for _, v := range s.List() { + set := sk_sets.NewResourceSet(nil) + s.List()(func(_ int, v T) bool { set.Insert(v) - } + return true + }) return set } + +func (s *resourceSet[T]) GetCompareFunc() func(a, b ezkube.ResourceId) int { + return s.compareFunc +} diff --git a/contrib/tests/set_v2_test.go b/contrib/tests/set_v2_test.go index 752494f8a..28d79a121 100644 --- a/contrib/tests/set_v2_test.go +++ b/contrib/tests/set_v2_test.go @@ -6,22 +6,23 @@ import ( v1 "github.com/solo-io/skv2/codegen/test/api/things.test.io/v1" "github.com/solo-io/skv2/contrib/pkg/sets" sets_v2 "github.com/solo-io/skv2/contrib/pkg/sets/v2" + "github.com/solo-io/skv2/pkg/ezkube" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -var _ = Describe("PaintSetV2", func() { +var _ = FDescribe("PaintSetV2", func() { var ( - setA, setB sets_v2.ResourceSet[*v1.Paint] - paintA, paintB, paintC *v1.Paint + setA, setB sets_v2.ResourceSet[*v1.Paint] + paintA, paintBCluster2, paintC *v1.Paint ) BeforeEach(func() { - setA = sets_v2.NewResourceSet[*v1.Paint]() - setB = sets_v2.NewResourceSet[*v1.Paint]() + setA = sets_v2.NewResourceSet[*v1.Paint](ezkube.ResourceIdsCompare) + setB = sets_v2.NewResourceSet[*v1.Paint](ezkube.ResourceIdsCompare) paintA = &v1.Paint{ ObjectMeta: metav1.ObjectMeta{Name: "nameA", Namespace: "nsA"}, } - paintB = &v1.Paint{ + paintBCluster2 = &v1.Paint{ ObjectMeta: metav1.ObjectMeta{Name: "nameB", Namespace: "nsB"}, } paintC = &v1.Paint{ @@ -31,157 +32,272 @@ var _ = Describe("PaintSetV2", func() { It("should insert", func() { setA.Insert(paintA) - list := setA.List() - Expect(list).To(ConsistOf(paintA)) - setA.Insert(paintB, paintC) - list = setA.List() - Expect(list).To(ConsistOf(paintA, paintB, paintC)) + Expect(setA.Has(paintA)).To(BeTrue()) + setA.Insert(paintBCluster2, paintC) + Expect(setA.Has(paintBCluster2)).To(BeTrue()) + Expect(setA.Has(paintC)).To(BeTrue()) + Expect(setA.Len()).To(Equal(3)) + }) + + When("inserting an existing resource", func() { + It("should overwrite the existing resource", func() { + setA.Insert(paintA) + setA.Insert(paintBCluster2) + setA.Insert(paintA) + Expect(setA.Len()).To(Equal(2)) + }) }) It("should return set existence", func() { setA.Insert(paintA) Expect(setA.Has(paintA)).To(BeTrue()) - Expect(setA.Has(paintB)).To(BeFalse()) - setA.Insert(paintB, paintC) + Expect(setA.Has(paintBCluster2)).To(BeFalse()) + setA.Insert(paintBCluster2, paintC) Expect(setA.Has(paintA)).To(BeTrue()) - Expect(setA.Has(paintB)).To(BeTrue()) + Expect(setA.Has(paintBCluster2)).To(BeTrue()) Expect(setA.Has(paintC)).To(BeTrue()) }) It("should return set equality", func() { - setB.Insert(paintA, paintB, paintC) + setB.Insert(paintA, paintBCluster2, paintC) setA.Insert(paintA) Expect(setA.Equal(setB)).To(BeFalse()) - setA.Insert(paintC, paintB) + setA.Insert(paintC, paintBCluster2) Expect(setA.Equal(setB)).To(BeTrue()) }) It("should delete", func() { - setA.Insert(paintA, paintB, paintC) + setA.Insert(paintA, paintBCluster2, paintC) Expect(setA.Has(paintA)).To(BeTrue()) setA.Delete(paintA) + Expect(setA.Len()).To(Equal(2)) Expect(setA.Has(paintA)).To(BeFalse()) }) - It("should filter UnsortedList", func() { - setA.Insert(paintA, paintB, paintC) - Expect(setA.Has(paintA)).To(BeTrue()) - filtered := setA.UnsortedList(func(p *v1.Paint) bool { - return p.Name == "nameA" - }) - Expect(filtered).To(HaveLen(2)) - Expect(filtered).To(ContainElements(paintB, paintC)) - }) - - It("should double filter UnsortedList", func() { - setA.Insert(paintA, paintB, paintC) - Expect(setA.Has(paintA)).To(BeTrue()) - filtered := setA.UnsortedList(func(p *v1.Paint) bool { - return p.Name == "nameA" - }, func(p *v1.Paint) bool { - return p.Name == "nameB" - }) - Expect(filtered).To(HaveLen(1)) - Expect(filtered).To(ContainElement(paintC)) - }) It("should filter List", func() { - setA.Insert(paintA, paintB, paintC) + setA.Insert(paintA, paintBCluster2, paintC) Expect(setA.Has(paintA)).To(BeTrue()) - filtered := setA.List(func(p *v1.Paint) bool { - return p.Name == "nameA" - }) - Expect(filtered).To(HaveLen(2)) - Expect(filtered).To(ContainElements(paintB, paintC)) + + for i, filtered := range setA.List(func(p *v1.Paint) bool { return p.GetName() == "nameA" }) { + if i == 1 { + Expect(filtered).To(Equal(paintBCluster2)) + } + if i == 2 { + Expect(filtered).To(Equal(paintC)) + } + } }) It("should double filter List", func() { - setA.Insert(paintA, paintB, paintC) + setA.Insert(paintA, paintBCluster2, paintC) Expect(setA.Has(paintA)).To(BeTrue()) - filtered := setA.List(func(p *v1.Paint) bool { + for _, filtered := range setA.List(func(p *v1.Paint) bool { return p.Name == "nameA" }, func(p *v1.Paint) bool { return p.Name == "nameB" - }) - Expect(filtered).To(HaveLen(1)) - Expect(filtered).To(ContainElement(paintC)) + }) { + Expect(filtered).To(Equal(paintC)) + } }) It("should union two sets and return new set", func() { - setA.Insert(paintA, paintB) - setB.Insert(paintA, paintB, paintC) + setA.Insert(paintA, paintBCluster2) + setB.Insert(paintA, paintBCluster2, paintC) unionSet := setA.Union(setB) - Expect(unionSet.List()).To(ConsistOf(paintA, paintB, paintC)) + Expect(unionSet.Len()).To(Equal(3)) + Expect(unionSet.Has(paintA)).To(BeTrue()) + Expect(unionSet.Has(paintBCluster2)).To(BeTrue()) + Expect(unionSet.Has(paintC)).To(BeTrue()) Expect(unionSet).ToNot(BeIdenticalTo(setA)) Expect(unionSet).ToNot(BeIdenticalTo(setB)) }) It("should take the difference of two sets and return new set", func() { - setA.Insert(paintA, paintB) - setB.Insert(paintA, paintB, paintC) + setA.Insert(paintA, paintBCluster2) + setB.Insert(paintA, paintBCluster2, paintC) differenceA := setA.Difference(setB) - Expect(differenceA.List()).To(BeEmpty()) + Expect(differenceA.Len()).To(Equal(0)) Expect(differenceA.Map()).To(BeEmpty()) Expect(differenceA).ToNot(BeIdenticalTo(setA)) differenceB := setB.Difference(setA) - Expect(differenceB.List()).To(ConsistOf(paintC)) + Expect(differenceB.Has(paintC)).To(BeTrue()) Expect(differenceB.Map()).To(HaveKeyWithValue(sets.Key(paintC), paintC)) Expect(differenceB).ToNot(BeIdenticalTo(setB)) }) It("should take the intersection of two sets and return new set", func() { - setA.Insert(paintA, paintB) - setB.Insert(paintA, paintB, paintC) + setA.Insert(paintA, paintBCluster2) + setB.Insert(paintA, paintBCluster2, paintC) intersectionA := setA.Intersection(setB) - Expect(intersectionA.List()).To(ConsistOf(paintA, paintB)) + Expect(intersectionA.Has(paintA)).To(BeTrue()) + Expect(intersectionA.Has(paintBCluster2)).To(BeTrue()) + Expect(intersectionA.Len()).To(Equal(2)) Expect(intersectionA.Map()).To(HaveKeyWithValue(sets.Key(paintA), paintA)) - Expect(intersectionA.Map()).To(HaveKeyWithValue(sets.Key(paintB), paintB)) + Expect(intersectionA.Map()).To(HaveKeyWithValue(sets.Key(paintBCluster2), paintBCluster2)) Expect(intersectionA).ToNot(BeIdenticalTo(setA)) }) - It("should correctly match two sets", func() { - setA.Insert(paintA, paintB) - setB.Insert(paintA, paintB) - Expect(setA).To(Equal(setB)) - setB.Insert(paintC) - Expect(setA).ToNot(Equal(setB)) - }) + // It("should correctly match two sets", func() { + // setA.Insert(paintA, paintBCluster2) + // setB.Insert(paintA, paintBCluster2) + // Expect(setA).To(Equal(setB)) + // setB.Insert(paintC) + // Expect(setA).ToNot(Equal(setB)) + // }) It("should return corrent length", func() { - setA.Insert(paintA, paintB) - Expect(setA.Length()).To(Equal(2)) + setA.Insert(paintA, paintBCluster2) + Expect(setA.Len()).To(Equal(2)) }) - It("should return set deltas", func() { - // update + // It("should return set deltas", func() { + // // update + // oldPaintA := &v1.Paint{ + // ObjectMeta: metav1.ObjectMeta{Name: "background", Namespace: "color"}, + // Spec: v1.PaintSpec{Color: &v1.PaintColor{Hue: "orange"}}, + // } + // newPaintA := &v1.Paint{ + // ObjectMeta: metav1.ObjectMeta{Name: "background", Namespace: "color"}, + // Spec: v1.PaintSpec{Color: &v1.PaintColor{Hue: "green"}}, + // } + // // remove + // oldPaintB := &v1.Paint{ + // ObjectMeta: metav1.ObjectMeta{Name: "ugly", Namespace: "color"}, + // } + // // add + // newPaintC := &v1.Paint{ + // ObjectMeta: metav1.ObjectMeta{Name: "beautiful", Namespace: "color"}, + // } + // // no change + // oldPaintD := &v1.Paint{ + // ObjectMeta: metav1.ObjectMeta{Name: "decent", Namespace: "color"}, + // } + // newPaintD := &v1.Paint{ + // ObjectMeta: metav1.ObjectMeta{Name: "decent", Namespace: "color"}, + // } + // setA.Insert(oldPaintA, oldPaintB, oldPaintD) + // setB.Insert(newPaintA, newPaintC, newPaintD) + + // expectedDelta := sets_v2.ResourceDelta[*v1.Paint]{ + // Inserted: sets_v2.NewResourceSet(ezkube.ObjectsAscending, ezkube.CompareObjects, newPaintA, newPaintC), + // Removed: sets_v2.NewResourceSet(ezkube.ObjectsAscending, ezkube.CompareObjects, oldPaintB), + // } + + // actualDelta := setA.Delta(setB) + + // Expect(actualDelta.Removed.Length()).To(Equal(expectedDelta.DeltaV1().Removed.Length())) + // for _, removed := range actualDelta.Removed.List() { + // Expect(removed).To(Equal(oldPaintB)) + // } + + // Expect(actualDelta.Inserted.Length()).To(Equal(expectedDelta.DeltaV1().Inserted.Length())) + // for i, inserted := range actualDelta.Inserted.List() { + // if i == 0 { + // Expect(inserted).To(Equal(newPaintA)) + // } + // if i == 1 { + // Expect(inserted).To(Equal(newPaintC)) + // } + // } + // }) + + It("should find resources", func() { oldPaintA := &v1.Paint{ - ObjectMeta: metav1.ObjectMeta{Name: "background", Namespace: "color"}, - Spec: v1.PaintSpec{Color: &v1.PaintColor{Hue: "orange"}}, + ObjectMeta: metav1.ObjectMeta{ + Name: "background", + Namespace: "color", + Annotations: map[string]string{ezkube.ClusterAnnotation: "orange"}, + }, } newPaintA := &v1.Paint{ - ObjectMeta: metav1.ObjectMeta{Name: "background", Namespace: "color"}, - Spec: v1.PaintSpec{Color: &v1.PaintColor{Hue: "green"}}, + ObjectMeta: metav1.ObjectMeta{ + Name: "background", + Namespace: "color", + Annotations: map[string]string{ezkube.ClusterAnnotation: "orange"}, + }, } - // remove oldPaintB := &v1.Paint{ ObjectMeta: metav1.ObjectMeta{Name: "ugly", Namespace: "color"}, } - // add newPaintC := &v1.Paint{ ObjectMeta: metav1.ObjectMeta{Name: "beautiful", Namespace: "color"}, } - // no change - oldPaintD := &v1.Paint{ - ObjectMeta: metav1.ObjectMeta{Name: "decent", Namespace: "color"}, + paintList := []*v1.Paint{oldPaintA, newPaintA, oldPaintB, newPaintC, newPaintC} + expectedPaintList := []*v1.Paint{newPaintC, oldPaintB, newPaintA} + setA.Insert(paintList...) + + Expect(setA.Len()).To(Equal(3)) + + for _, paint := range expectedPaintList { + found, err := setA.Find(paint) + Expect(Expect(err).NotTo(HaveOccurred())) + Expect(found).To(Equal(paint)) } - newPaintD := &v1.Paint{ - ObjectMeta: metav1.ObjectMeta{Name: "decent", Namespace: "color"}, + // find based on something that implemented ClusterObjectRef + found, err := setA.Find(ezkube.MakeClusterObjectRef(newPaintC)) + Expect(Expect(err).NotTo(HaveOccurred())) + Expect(found).To(Equal(newPaintC)) + }) + + It("should sort resources by name.ns.cluster", func() { + paintAA1 := &v1.Paint{ + ObjectMeta: metav1.ObjectMeta{ + Name: "a", + Namespace: "a", + Annotations: map[string]string{ezkube.ClusterAnnotation: "1"}, + }, + } + paintAB1 := &v1.Paint{ + ObjectMeta: metav1.ObjectMeta{ + Name: "a", + Namespace: "b", + Annotations: map[string]string{ezkube.ClusterAnnotation: "1"}, + }, + } + paintBB1 := &v1.Paint{ + ObjectMeta: metav1.ObjectMeta{ + Name: "b", + Namespace: "b", + Annotations: map[string]string{ezkube.ClusterAnnotation: "1"}, + }, + } + paintAC2 := &v1.Paint{ + ObjectMeta: metav1.ObjectMeta{ + Name: "a", + Namespace: "c", + Annotations: map[string]string{ezkube.ClusterAnnotation: "2"}, + }, + } + paintBC2 := &v1.Paint{ + ObjectMeta: metav1.ObjectMeta{ + Name: "b", + Namespace: "c", + Annotations: map[string]string{ezkube.ClusterAnnotation: "2"}, + }, + } + paintAC := &v1.Paint{ + ObjectMeta: metav1.ObjectMeta{ + Name: "a", + Namespace: "c", + }, + } + paintBC := &v1.Paint{ + ObjectMeta: metav1.ObjectMeta{ + Name: "b", + Namespace: "c", + }, + } + expectedOrder := []*v1.Paint{paintAA1, paintAB1, paintAC, paintAC2, paintBB1, paintBC, paintBC2} + setA.Insert(expectedOrder...) + + var paintList []*v1.Paint + for _, paint := range setA.List() { + paintList = append(paintList, paint) + } + + for i, paint := range expectedOrder { + Expect(paintList[i]).To(Equal(paint)) } - setA.Insert(oldPaintA, oldPaintB, oldPaintD) - setB.Insert(newPaintA, newPaintC, newPaintD) - Expect(setA.Delta(setB)).To(Equal(sets.ResourceDelta{ - Inserted: sets.NewResourceSet(newPaintA, newPaintC), - Removed: sets.NewResourceSet(oldPaintB), - })) }) }) From e9c1450b66da617d35e93bd18f7e71618aeb0a87 Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Tue, 2 Apr 2024 17:25:34 -0400 Subject: [PATCH 02/23] remove compare resource ids --- contrib/pkg/sets/v2/sets.go | 16 +++++------ contrib/tests/set_v2_test.go | 4 +-- pkg/ezkube/creationtimestamp.go | 15 ++++++++++ pkg/ezkube/resource_id.go | 50 +++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 pkg/ezkube/creationtimestamp.go diff --git a/contrib/pkg/sets/v2/sets.go b/contrib/pkg/sets/v2/sets.go index 7261eab06..eabb4867f 100644 --- a/contrib/pkg/sets/v2/sets.go +++ b/contrib/pkg/sets/v2/sets.go @@ -69,12 +69,11 @@ type resourceSet[T client.Object] struct { } func NewResourceSet[T client.Object]( - compareFunc func(a, b ezkube.ResourceId) int, resources ...T, ) ResourceSet[T] { rs := &resourceSet[T]{ set: []T{}, - compareFunc: compareFunc, + compareFunc: ezkube.ResourceIdsCompare, } rs.Insert(resources...) return rs @@ -178,7 +177,6 @@ func (s *resourceSet[T]) Union(set ResourceSet[T]) ResourceSet[T] { list = append(list, resource.(T)) } return NewResourceSet[T]( - s.compareFunc, list..., ) } @@ -186,7 +184,7 @@ func (s *resourceSet[T]) Union(set ResourceSet[T]) ResourceSet[T] { func (s *resourceSet[T]) Difference(set ResourceSet[T]) ResourceSet[T] { s.lock.RLock() defer s.lock.RUnlock() - result := NewResourceSet[T](s.compareFunc) + result := NewResourceSet[T]() for _, resource := range s.set { if !set.Has(resource) { result.Insert(resource) @@ -199,13 +197,13 @@ func (s *resourceSet[T]) Intersection(set ResourceSet[T]) ResourceSet[T] { s.lock.RLock() defer s.lock.RUnlock() var walk, other ResourceSet[T] - result := NewResourceSet[T](s.compareFunc) + result := NewResourceSet[T]() if len(s.set) < set.Len() { - walk = NewResourceSet(s.compareFunc, s.set...) + walk = NewResourceSet(s.set...) other = set } else { walk = set - other = NewResourceSet(s.compareFunc, s.set...) + other = NewResourceSet(s.set...) } walk.List()(func(_ int, key T) bool { if other.Has(key) { @@ -241,7 +239,7 @@ func (s *resourceSet[T]) Len() int { // note that this function will currently panic if called for a ResourceSet[T] containing non-runtime.Objects func (oldSet *resourceSet[T]) Delta(newSet ResourceSet[T]) sk_sets.ResourceDelta { - updated, removed := NewResourceSet[T](oldSet.compareFunc), NewResourceSet[T](oldSet.compareFunc) + updated, removed := NewResourceSet[T](), NewResourceSet[T]() // find objects updated or removed for _, resource := range oldSet.set { @@ -276,7 +274,7 @@ func (oldSet *resourceSet[T]) Delta(newSet ResourceSet[T]) sk_sets.ResourceDelta // Create a clone of the current set // note that this function will currently panic if called for a ResourceSet[T] containing non-runtime.Objects func (oldSet *resourceSet[T]) Clone() ResourceSet[T] { - new := NewResourceSet[T](oldSet.compareFunc) + new := NewResourceSet[T]() oldSet.List()(func(_ int, oldObj T) bool { copy := oldObj.DeepCopyObject().(T) diff --git a/contrib/tests/set_v2_test.go b/contrib/tests/set_v2_test.go index 28d79a121..e31d1078a 100644 --- a/contrib/tests/set_v2_test.go +++ b/contrib/tests/set_v2_test.go @@ -17,8 +17,8 @@ var _ = FDescribe("PaintSetV2", func() { ) BeforeEach(func() { - setA = sets_v2.NewResourceSet[*v1.Paint](ezkube.ResourceIdsCompare) - setB = sets_v2.NewResourceSet[*v1.Paint](ezkube.ResourceIdsCompare) + setA = sets_v2.NewResourceSet[*v1.Paint]() + setB = sets_v2.NewResourceSet[*v1.Paint]() paintA = &v1.Paint{ ObjectMeta: metav1.ObjectMeta{Name: "nameA", Namespace: "nsA"}, } diff --git a/pkg/ezkube/creationtimestamp.go b/pkg/ezkube/creationtimestamp.go new file mode 100644 index 000000000..2723c6a65 --- /dev/null +++ b/pkg/ezkube/creationtimestamp.go @@ -0,0 +1,15 @@ +package ezkube + +import "sigs.k8s.io/controller-runtime/pkg/client" + +// CreationTimestampCompare returns an integer comparing two resources lexicographically. +// The result will be 0 if a == b, -1 if a < b, and +1 if a > b. +func CreationTimestampCompare(a, b client.Object) int { + if a.GetCreationTimestamp().Time.Equal(b.GetCreationTimestamp().Time) { + return 0 + } + if a.GetCreationTimestamp().Time.Before(b.GetCreationTimestamp().Time) { + return -1 + } + return 1 +} diff --git a/pkg/ezkube/resource_id.go b/pkg/ezkube/resource_id.go index b070e006e..6248fbc63 100644 --- a/pkg/ezkube/resource_id.go +++ b/pkg/ezkube/resource_id.go @@ -173,3 +173,53 @@ func ResourceIdFromKeyWithSeparator(key string, separator string) (ResourceId, e return nil, eris.Errorf("could not convert key %s with separator %s into resource id; unexpected number of parts: %d", key, separator, len(parts)) } } + +// ResourceIdsCompare returns an integer comparing two ResourceIds lexicographically. +// The comparison is based on name, then namespace, then cluster name. +// The result will be 0 if a == b, -1 if a < b, and +1 if a > b. +func ResourceIdsCompare(a, b ResourceId) int { + // compare names + if a.GetName() < b.GetName() { + return -1 + } + if a.GetName() > b.GetName() { + return 1 + } + + // compare namespaces + if a.GetNamespace() < b.GetNamespace() { + return -1 + } + if a.GetNamespace() > b.GetNamespace() { + return 1 + } + + // compare cluster names + // attempt to cast to ClusterResourceId + // if fails, attempt cast to deprecatedClusterResourceId since we might be working with a ClusterObjectRef + var ( + aCluster, bCluster string + ) + + if a_cri, ok := a.(ClusterResourceId); ok { + aCluster = GetClusterName(a_cri) + } else if a_dcri, ok := a.(deprecatedClusterResourceId); ok { + aCluster = a_dcri.GetClusterName() + } + + if b_cri, ok := b.(ClusterResourceId); ok { + bCluster = GetClusterName(b_cri) + } else if b_dcri, ok := b.(deprecatedClusterResourceId); ok { + bCluster = b_dcri.GetClusterName() + } + + if aCluster < bCluster { + return -1 + } + if aCluster > bCluster { + return 1 + } + + // they are equal + return 0 +} From 7da95afc7353582da0a8c40da85c41d25d9341ea Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Tue, 2 Apr 2024 18:02:04 -0400 Subject: [PATCH 03/23] add iter --- contrib/pkg/sets/v2/sets.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contrib/pkg/sets/v2/sets.go b/contrib/pkg/sets/v2/sets.go index eabb4867f..d97635a8f 100644 --- a/contrib/pkg/sets/v2/sets.go +++ b/contrib/pkg/sets/v2/sets.go @@ -17,6 +17,10 @@ type ResourceSet[T client.Object] interface { // List returns an iterator for the set. // Pass an optional filter function to skip iteration on specific entries; Note: index will still progress. List(filterResource ...func(T) bool) func(yield func(int, T) bool) + // Iterate over the set, passing the index and resource to the provided function. + // The iteration can be stopped by returning false from the function. + // Returning true will continue the iteration. + Iter(func(yield func(int, T) bool)) // Return the Set as a map of key to resource. Map() map[string]T // Insert a resource into the set. From 57dbf78866922a4e68d0e0162746ff66b0374e88 Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Tue, 2 Apr 2024 18:03:53 -0400 Subject: [PATCH 04/23] fix iter --- contrib/pkg/sets/v2/sets.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/pkg/sets/v2/sets.go b/contrib/pkg/sets/v2/sets.go index d97635a8f..3ca7857da 100644 --- a/contrib/pkg/sets/v2/sets.go +++ b/contrib/pkg/sets/v2/sets.go @@ -20,7 +20,7 @@ type ResourceSet[T client.Object] interface { // Iterate over the set, passing the index and resource to the provided function. // The iteration can be stopped by returning false from the function. // Returning true will continue the iteration. - Iter(func(yield func(int, T) bool)) + Iter(func(int, T) bool) // Return the Set as a map of key to resource. Map() map[string]T // Insert a resource into the set. From 4f8e21a7ea8d78ef0944ab96238656a7aa15f21e Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Tue, 2 Apr 2024 18:04:34 -0400 Subject: [PATCH 05/23] add iter impl --- contrib/pkg/sets/v2/sets.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/contrib/pkg/sets/v2/sets.go b/contrib/pkg/sets/v2/sets.go index 3ca7857da..8b6f6abb6 100644 --- a/contrib/pkg/sets/v2/sets.go +++ b/contrib/pkg/sets/v2/sets.go @@ -111,6 +111,16 @@ func (s *resourceSet[T]) List(filterResource ...func(T) bool) func(yield func(in } } +func (s *resourceSet[T]) Iter(yield func(int, T) bool) { + s.lock.RLock() + defer s.lock.RUnlock() + for i, resource := range s.set { + if !yield(i, resource) { + break + } + } +} + func (s *resourceSet[T]) Map() map[string]T { s.lock.RLock() defer s.lock.RUnlock() From 6d680071305c9b4ae492da0a69d385c89a0fbecd Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Tue, 2 Apr 2024 18:12:36 -0400 Subject: [PATCH 06/23] add inefficient list as cop out --- contrib/pkg/sets/v2/sets.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/contrib/pkg/sets/v2/sets.go b/contrib/pkg/sets/v2/sets.go index 8b6f6abb6..2b63cccc6 100644 --- a/contrib/pkg/sets/v2/sets.go +++ b/contrib/pkg/sets/v2/sets.go @@ -21,6 +21,8 @@ type ResourceSet[T client.Object] interface { // The iteration can be stopped by returning false from the function. // Returning true will continue the iteration. Iter(func(int, T) bool) + + InefficientList() []T // Return the Set as a map of key to resource. Map() map[string]T // Insert a resource into the set. @@ -111,6 +113,14 @@ func (s *resourceSet[T]) List(filterResource ...func(T) bool) func(yield func(in } } +// Deprecated: use Iter instead +// InefficientList returns the set as a slice. +func (s *resourceSet[T]) InefficientList() []T { + s.lock.RLock() + defer s.lock.RUnlock() + return s.set[:] +} + func (s *resourceSet[T]) Iter(yield func(int, T) bool) { s.lock.RLock() defer s.lock.RUnlock() From 79c043668044aedba20d5beee7917b68254d8357 Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Tue, 2 Apr 2024 19:32:36 -0400 Subject: [PATCH 07/23] add filters to inefficient list --- contrib/pkg/sets/v2/sets.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/contrib/pkg/sets/v2/sets.go b/contrib/pkg/sets/v2/sets.go index 2b63cccc6..6d7e0b660 100644 --- a/contrib/pkg/sets/v2/sets.go +++ b/contrib/pkg/sets/v2/sets.go @@ -22,7 +22,7 @@ type ResourceSet[T client.Object] interface { // Returning true will continue the iteration. Iter(func(int, T) bool) - InefficientList() []T + InefficientList(filterResource ...func(T) bool) []T // Return the Set as a map of key to resource. Map() map[string]T // Insert a resource into the set. @@ -115,10 +115,19 @@ func (s *resourceSet[T]) List(filterResource ...func(T) bool) func(yield func(in // Deprecated: use Iter instead // InefficientList returns the set as a slice. -func (s *resourceSet[T]) InefficientList() []T { +func (s *resourceSet[T]) InefficientList(filterResource ...func(T) bool) []T { s.lock.RLock() defer s.lock.RUnlock() - return s.set[:] + var ret []T + for _, resource := range s.set { + for _, filter := range filterResource { + if filter(resource) { + ret = append(ret, resource) + break + } + } + } + return ret } func (s *resourceSet[T]) Iter(yield func(int, T) bool) { From d55edfa2368792bf8b41421cad3b6eb437a3a944 Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Wed, 3 Apr 2024 10:54:42 -0400 Subject: [PATCH 08/23] fix inefficient list --- contrib/pkg/sets/v2/sets.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/contrib/pkg/sets/v2/sets.go b/contrib/pkg/sets/v2/sets.go index 6d7e0b660..99e1706ef 100644 --- a/contrib/pkg/sets/v2/sets.go +++ b/contrib/pkg/sets/v2/sets.go @@ -121,10 +121,12 @@ func (s *resourceSet[T]) InefficientList(filterResource ...func(T) bool) []T { var ret []T for _, resource := range s.set { for _, filter := range filterResource { - if filter(resource) { - ret = append(ret, resource) - break + if len(filterResource) > 0 && !filter(resource) { + continue } + ret = append(ret, resource) + break + } } return ret From 6dbaac015ee1e64c58af0cbbabde2b872cf5e7fe Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Wed, 3 Apr 2024 10:55:57 -0400 Subject: [PATCH 09/23] fix inefficient list --- contrib/pkg/sets/v2/sets.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/contrib/pkg/sets/v2/sets.go b/contrib/pkg/sets/v2/sets.go index 99e1706ef..abd398784 100644 --- a/contrib/pkg/sets/v2/sets.go +++ b/contrib/pkg/sets/v2/sets.go @@ -120,13 +120,15 @@ func (s *resourceSet[T]) InefficientList(filterResource ...func(T) bool) []T { defer s.lock.RUnlock() var ret []T for _, resource := range s.set { + if len(filterResource) == 0 { + ret = append(ret, resource) + continue + } for _, filter := range filterResource { - if len(filterResource) > 0 && !filter(resource) { - continue + if filter(resource) { + ret = append(ret, resource) + break } - ret = append(ret, resource) - break - } } return ret From 3d22b84355d33a5e2e5ebb0003ed5542b2dc11c0 Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Wed, 3 Apr 2024 12:51:22 -0400 Subject: [PATCH 10/23] inefficient list inverted --- contrib/pkg/sets/v2/sets.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/contrib/pkg/sets/v2/sets.go b/contrib/pkg/sets/v2/sets.go index abd398784..850242cb3 100644 --- a/contrib/pkg/sets/v2/sets.go +++ b/contrib/pkg/sets/v2/sets.go @@ -23,6 +23,8 @@ type ResourceSet[T client.Object] interface { Iter(func(int, T) bool) InefficientList(filterResource ...func(T) bool) []T + + InefficientListInverted(filterResource ...func(T) bool) []T // Return the Set as a map of key to resource. Map() map[string]T // Insert a resource into the set. @@ -134,6 +136,25 @@ func (s *resourceSet[T]) InefficientList(filterResource ...func(T) bool) []T { return ret } +func (s *resourceSet[T]) InefficientListInverted(filterResource ...func(T) bool) []T { + s.lock.RLock() + defer s.lock.RUnlock() + var ret []T + for _, resource := range s.set { + filtered := false + for _, filter := range filterResource { + if filter(resource) { + filtered = true + break + } + } + if !filtered { + ret = append(ret, resource) + } + } + return ret +} + func (s *resourceSet[T]) Iter(yield func(int, T) bool) { s.lock.RLock() defer s.lock.RUnlock() From 755af87013abb4f77ecc4edfbf66f992861848f8 Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Wed, 3 Apr 2024 14:36:38 -0400 Subject: [PATCH 11/23] remove inefficient list --- contrib/pkg/sets/v2/sets.go | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/contrib/pkg/sets/v2/sets.go b/contrib/pkg/sets/v2/sets.go index 850242cb3..1b4fe505c 100644 --- a/contrib/pkg/sets/v2/sets.go +++ b/contrib/pkg/sets/v2/sets.go @@ -22,8 +22,6 @@ type ResourceSet[T client.Object] interface { // Returning true will continue the iteration. Iter(func(int, T) bool) - InefficientList(filterResource ...func(T) bool) []T - InefficientListInverted(filterResource ...func(T) bool) []T // Return the Set as a map of key to resource. Map() map[string]T @@ -115,26 +113,6 @@ func (s *resourceSet[T]) List(filterResource ...func(T) bool) func(yield func(in } } -// Deprecated: use Iter instead -// InefficientList returns the set as a slice. -func (s *resourceSet[T]) InefficientList(filterResource ...func(T) bool) []T { - s.lock.RLock() - defer s.lock.RUnlock() - var ret []T - for _, resource := range s.set { - if len(filterResource) == 0 { - ret = append(ret, resource) - continue - } - for _, filter := range filterResource { - if filter(resource) { - ret = append(ret, resource) - break - } - } - } - return ret -} func (s *resourceSet[T]) InefficientListInverted(filterResource ...func(T) bool) []T { s.lock.RLock() From 6b23bffcd1db09dbb51e53dc36aa84efe3aeafdb Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Wed, 3 Apr 2024 17:41:56 -0400 Subject: [PATCH 12/23] add length method to set/ --- contrib/pkg/sets/v2/sets.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/contrib/pkg/sets/v2/sets.go b/contrib/pkg/sets/v2/sets.go index 1b4fe505c..99e4a0b8f 100644 --- a/contrib/pkg/sets/v2/sets.go +++ b/contrib/pkg/sets/v2/sets.go @@ -27,6 +27,9 @@ type ResourceSet[T client.Object] interface { Map() map[string]T // Insert a resource into the set. Insert(resource ...T) + + Length() int + // Compare the equality of the keys in two sets (not the resources themselves) Equal(set ResourceSet[T]) bool // Check if the set contains the resource. @@ -113,7 +116,6 @@ func (s *resourceSet[T]) List(filterResource ...func(T) bool) func(yield func(in } } - func (s *resourceSet[T]) InefficientListInverted(filterResource ...func(T) bool) []T { s.lock.RLock() defer s.lock.RUnlock() @@ -273,6 +275,12 @@ func (s *resourceSet[T]) Len() int { return len(s.set) } +func (s *resourceSet[T]) Length() int { + s.lock.RLock() + defer s.lock.RUnlock() + return len(s.set) +} + // note that this function will currently panic if called for a ResourceSet[T] containing non-runtime.Objects func (oldSet *resourceSet[T]) Delta(newSet ResourceSet[T]) sk_sets.ResourceDelta { updated, removed := NewResourceSet[T](), NewResourceSet[T]() From b322aadb06101d8cf425a28832d35ab8e8e0b28b Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Thu, 4 Apr 2024 13:10:10 -0400 Subject: [PATCH 13/23] update --- contrib/pkg/sets/v2/sets.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/pkg/sets/v2/sets.go b/contrib/pkg/sets/v2/sets.go index 99e4a0b8f..327be6a67 100644 --- a/contrib/pkg/sets/v2/sets.go +++ b/contrib/pkg/sets/v2/sets.go @@ -16,7 +16,7 @@ type ResourceSet[T client.Object] interface { Keys() sets.Set[string] // List returns an iterator for the set. // Pass an optional filter function to skip iteration on specific entries; Note: index will still progress. - List(filterResource ...func(T) bool) func(yield func(int, T) bool) + InvertedFilterIter(filterResource ...func(T) bool) func(yield func(int, T) bool) // Iterate over the set, passing the index and resource to the provided function. // The iteration can be stopped by returning false from the function. // Returning true will continue the iteration. @@ -98,7 +98,7 @@ func (s *resourceSet[T]) Keys() sets.Set[string] { return sets.Set[string]{} } -func (s *resourceSet[T]) List(filterResource ...func(T) bool) func(yield func(int, T) bool) { +func (s *resourceSet[T]) FilterIter(filterResource ...func(T) bool) func(yield func(int, T) bool) { s.lock.RLock() defer s.lock.RUnlock() return func(yield func(int, T) bool) { From dd56372257bb0993c7838fcdfa4b1272284b6dfe Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Thu, 4 Apr 2024 13:48:46 -0400 Subject: [PATCH 14/23] update --- contrib/pkg/sets/v2/sets.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contrib/pkg/sets/v2/sets.go b/contrib/pkg/sets/v2/sets.go index 327be6a67..526da3b6f 100644 --- a/contrib/pkg/sets/v2/sets.go +++ b/contrib/pkg/sets/v2/sets.go @@ -98,7 +98,7 @@ func (s *resourceSet[T]) Keys() sets.Set[string] { return sets.Set[string]{} } -func (s *resourceSet[T]) FilterIter(filterResource ...func(T) bool) func(yield func(int, T) bool) { +func (s *resourceSet[T]) InvertedFilterIter(filterResource ...func(T) bool) func(yield func(int, T) bool) { s.lock.RLock() defer s.lock.RUnlock() return func(yield func(int, T) bool) { @@ -243,7 +243,7 @@ func (s *resourceSet[T]) Intersection(set ResourceSet[T]) ResourceSet[T] { walk = set other = NewResourceSet(s.set...) } - walk.List()(func(_ int, key T) bool { + walk.Iter(func(_ int, key T) bool { if other.Has(key) { result.Insert(key) } @@ -320,7 +320,7 @@ func (oldSet *resourceSet[T]) Delta(newSet ResourceSet[T]) sk_sets.ResourceDelta func (oldSet *resourceSet[T]) Clone() ResourceSet[T] { new := NewResourceSet[T]() - oldSet.List()(func(_ int, oldObj T) bool { + oldSet.Iter(func(_ int, oldObj T) bool { copy := oldObj.DeepCopyObject().(T) new.Insert(copy) return true @@ -330,7 +330,7 @@ func (oldSet *resourceSet[T]) Clone() ResourceSet[T] { func (s *resourceSet[T]) Generic() sk_sets.ResourceSet { set := sk_sets.NewResourceSet(nil) - s.List()(func(_ int, v T) bool { + s.Iter(func(_ int, v T) bool { set.Insert(v) return true }) From ced1f6cf48b635aafe0d94f000bb2d0e447850c0 Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Mon, 8 Apr 2024 12:16:44 -0400 Subject: [PATCH 15/23] update v2 sets with better funciton names --- contrib/pkg/sets/v2/sets.go | 102 +++++++++++++++++------------------- 1 file changed, 47 insertions(+), 55 deletions(-) diff --git a/contrib/pkg/sets/v2/sets.go b/contrib/pkg/sets/v2/sets.go index 526da3b6f..c63116e4e 100644 --- a/contrib/pkg/sets/v2/sets.go +++ b/contrib/pkg/sets/v2/sets.go @@ -5,7 +5,6 @@ import ( "sync" sk_sets "github.com/solo-io/skv2/contrib/pkg/sets" - "github.com/solo-io/skv2/pkg/controllerutils" "github.com/solo-io/skv2/pkg/ezkube" "k8s.io/apimachinery/pkg/util/sets" "sigs.k8s.io/controller-runtime/pkg/client" @@ -14,22 +13,38 @@ import ( type ResourceSet[T client.Object] interface { // Get the set stored keys Keys() sets.Set[string] - // List returns an iterator for the set. - // Pass an optional filter function to skip iteration on specific entries; Note: index will still progress. - InvertedFilterIter(filterResource ...func(T) bool) func(yield func(int, T) bool) - // Iterate over the set, passing the index and resource to the provided function. - // The iteration can be stopped by returning false from the function. - // Returning true will continue the iteration. + // FilterOutAndIterate returns an iterator that will iterate over the set of elements + // that *do not match any of the provided filters*. If any of the filters returns true, the resource will be skipped. + // The index and resource are passed to the provided function for every element in the set, + // where the index is the index of the resource in the *filtered* set. + // The iteration can be stopped by returning false from the function. This can be thought of as a "break" statement in a loop. + // Returning true will continue the iteration. This can be thought of as a "continue" statement in a loop. + // For iteration that does not need to be filtered, use Iter. + FilterOutAndIterate(filterResource ...func(T) bool) func(yield func(int, T) bool) + + // Filter returns an iterator that will iterate over the set of elements + // that match the provided filter. If the filter returns true, the resource will be included in the iteration. + // The index and resource are passed to the provided function for every element in the *filtered set*. + // The index is the index of the resource in the *filtered* set. + // The iteration can be stopped by returning false from the function. This can be thought of as a "break" statement in a loop. + // Returning true will continue the iteration. This can be thought of as a "continue" statement in a loop. + // For iteration that does not need to be filtered, use Iter. + Filter(filterResource func(T) bool) func(yield func(int, T) bool) + + // Iter iterates over the set, passing the index and resource to the provided function for every element in the set. + // The iteration can be stopped by returning false from the function. This can be thought of as a "break" statement in a loop. + // Returning true will continue the iteration. This can be thought of as a "continue" statement in a loop. Iter(func(int, T) bool) - InefficientListInverted(filterResource ...func(T) bool) []T + // FilterOutAndCreateList constructs a list of resource that do not match any of the provided filters. + // Use of this function should be limited to only when a filtered list is needed. + // For iteration that does not require creating a new list, use FilterOutAndIterate. + FilterOutAndCreateList(filterResource ...func(T) bool) []T // Return the Set as a map of key to resource. Map() map[string]T // Insert a resource into the set. Insert(resource ...T) - Length() int - // Compare the equality of the keys in two sets (not the resources themselves) Equal(set ResourceSet[T]) bool // Check if the set contains the resource. @@ -46,14 +61,11 @@ type ResourceSet[T client.Object] interface { Find(resource ezkube.ResourceId) (T, error) // Get the length of the set Len() int + Length() int // returns the generic implementation of the set Generic() sk_sets.ResourceSet - // returns the delta between this and and another ResourceSet[T] - Delta(newSet ResourceSet[T]) sk_sets.ResourceDelta // Clone returns a deep copy of the set Clone() ResourceSet[T] - // Get the compare function used by the set - GetCompareFunc() func(a, b ezkube.ResourceId) int } // ResourceDelta represents the set of changes between two ResourceSets. @@ -98,12 +110,13 @@ func (s *resourceSet[T]) Keys() sets.Set[string] { return sets.Set[string]{} } -func (s *resourceSet[T]) InvertedFilterIter(filterResource ...func(T) bool) func(yield func(int, T) bool) { +func (s *resourceSet[T]) FilterOutAndIterate(filterResource ...func(T) bool) func(yield func(int, T) bool) { s.lock.RLock() defer s.lock.RUnlock() return func(yield func(int, T) bool) { + i := 0 OUTER: - for i, resource := range s.set { + for _, resource := range s.set { for _, filter := range filterResource { if filter(resource) { continue OUTER @@ -112,11 +125,28 @@ func (s *resourceSet[T]) InvertedFilterIter(filterResource ...func(T) bool) func if !yield(i, resource) { break } + i += 1 } } } -func (s *resourceSet[T]) InefficientListInverted(filterResource ...func(T) bool) []T { +func (s *resourceSet[T]) Filter(filterResource func(T) bool) func(yield func(int, T) bool) { + s.lock.RLock() + defer s.lock.RUnlock() + return func(yield func(int, T) bool) { + i := 0 + for _, resource := range s.set { + if filterResource(resource) { + if !yield(i, resource) { + break + } + i += 1 + } + } + } +} + +func (s *resourceSet[T]) FilterOutAndCreateList(filterResource ...func(T) bool) []T { s.lock.RLock() defer s.lock.RUnlock() var ret []T @@ -281,40 +311,6 @@ func (s *resourceSet[T]) Length() int { return len(s.set) } -// note that this function will currently panic if called for a ResourceSet[T] containing non-runtime.Objects -func (oldSet *resourceSet[T]) Delta(newSet ResourceSet[T]) sk_sets.ResourceDelta { - updated, removed := NewResourceSet[T](), NewResourceSet[T]() - - // find objects updated or removed - for _, resource := range oldSet.set { - newObj, err := newSet.Find(resource) - switch { - case err != nil: - // obj removed - removed.Insert(resource) - case !controllerutils.ObjectsEqual(resource, newObj): - // obj updated - updated.Insert(newObj) - default: - // obj the same - } - } - - // find objects added - for _, newObj := range newSet.Generic().List() { - if _, err := oldSet.Find(newObj.(T)); err != nil { - // obj added - updated.Insert(newObj.(T)) - } - } - - delta := &ResourceDelta[T]{ - Inserted: updated, - Removed: removed, - } - return delta.DeltaV1() -} - // Create a clone of the current set // note that this function will currently panic if called for a ResourceSet[T] containing non-runtime.Objects func (oldSet *resourceSet[T]) Clone() ResourceSet[T] { @@ -336,7 +332,3 @@ func (s *resourceSet[T]) Generic() sk_sets.ResourceSet { }) return set } - -func (s *resourceSet[T]) GetCompareFunc() func(a, b ezkube.ResourceId) int { - return s.compareFunc -} From 05eb58acdfdac051bf6f44f37d7fbee8de53f1ce Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Mon, 8 Apr 2024 13:27:10 -0400 Subject: [PATCH 16/23] update v2 sets with better funciton names --- changelog/v0.36.6/sets-v2-iter.yaml | 6 ++ contrib/pkg/sets/v2/sets.go | 12 +-- contrib/tests/set_v2_test.go | 115 ++++++++++++---------------- 3 files changed, 59 insertions(+), 74 deletions(-) create mode 100644 changelog/v0.36.6/sets-v2-iter.yaml diff --git a/changelog/v0.36.6/sets-v2-iter.yaml b/changelog/v0.36.6/sets-v2-iter.yaml new file mode 100644 index 000000000..f9b5f08d4 --- /dev/null +++ b/changelog/v0.36.6/sets-v2-iter.yaml @@ -0,0 +1,6 @@ +changelog: + - type: NON_USER_FACING + issueLink: + description: > + "" + skipCI: "false" diff --git a/contrib/pkg/sets/v2/sets.go b/contrib/pkg/sets/v2/sets.go index c63116e4e..527385188 100644 --- a/contrib/pkg/sets/v2/sets.go +++ b/contrib/pkg/sets/v2/sets.go @@ -13,15 +13,7 @@ import ( type ResourceSet[T client.Object] interface { // Get the set stored keys Keys() sets.Set[string] - // FilterOutAndIterate returns an iterator that will iterate over the set of elements - // that *do not match any of the provided filters*. If any of the filters returns true, the resource will be skipped. - // The index and resource are passed to the provided function for every element in the set, - // where the index is the index of the resource in the *filtered* set. - // The iteration can be stopped by returning false from the function. This can be thought of as a "break" statement in a loop. - // Returning true will continue the iteration. This can be thought of as a "continue" statement in a loop. - // For iteration that does not need to be filtered, use Iter. - FilterOutAndIterate(filterResource ...func(T) bool) func(yield func(int, T) bool) - + // Filter returns an iterator that will iterate over the set of elements // that match the provided filter. If the filter returns true, the resource will be included in the iteration. // The index and resource are passed to the provided function for every element in the *filtered set*. @@ -30,7 +22,7 @@ type ResourceSet[T client.Object] interface { // Returning true will continue the iteration. This can be thought of as a "continue" statement in a loop. // For iteration that does not need to be filtered, use Iter. Filter(filterResource func(T) bool) func(yield func(int, T) bool) - + // Iter iterates over the set, passing the index and resource to the provided function for every element in the set. // The iteration can be stopped by returning false from the function. This can be thought of as a "break" statement in a loop. // Returning true will continue the iteration. This can be thought of as a "continue" statement in a loop. diff --git a/contrib/tests/set_v2_test.go b/contrib/tests/set_v2_test.go index e31d1078a..4026e9822 100644 --- a/contrib/tests/set_v2_test.go +++ b/contrib/tests/set_v2_test.go @@ -74,24 +74,55 @@ var _ = FDescribe("PaintSetV2", func() { Expect(setA.Has(paintA)).To(BeFalse()) }) - It("should filter List", func() { + It("should filter out", func() { setA.Insert(paintA, paintBCluster2, paintC) Expect(setA.Has(paintA)).To(BeTrue()) + Expect(setA.Len()).To(Equal(3)) - for i, filtered := range setA.List(func(p *v1.Paint) bool { return p.GetName() == "nameA" }) { - if i == 1 { + filterFn := func(p *v1.Paint) bool { return p.GetName() == "nameA" } + + for i, filtered := range setA.FilterOutAndCreateList(filterFn) { + if i == 0 { Expect(filtered).To(Equal(paintBCluster2)) } - if i == 2 { + if i == 1 { Expect(filtered).To(Equal(paintC)) } } + + setA.FilterOutAndIterate(filterFn)(func(i int, p *v1.Paint) bool { + if i == 0 { + Expect(p).To(Equal(paintBCluster2)) + } + if i == 1 { + Expect(p).To(Equal(paintC)) + } + return true + }) + }) + + It("should filter", func() { + setA.Insert(paintA, paintBCluster2, paintC) + Expect(setA.Has(paintA)).To(BeTrue()) + Expect(setA.Len()).To(Equal(3)) + + filterFn := func(p *v1.Paint) bool { return p.GetName() == "nameA" } + + setA.Filter(filterFn)(func(i int, p *v1.Paint) bool { + if i == 0 { + Expect(p).To(Equal(paintA)) + } + if i != 0 { + Fail("only one item should be in the filtered set") + } + return true + }) }) It("should double filter List", func() { setA.Insert(paintA, paintBCluster2, paintC) Expect(setA.Has(paintA)).To(BeTrue()) - for _, filtered := range setA.List(func(p *v1.Paint) bool { + for _, filtered := range setA.FilterOutAndCreateList(func(p *v1.Paint) bool { return p.Name == "nameA" }, func(p *v1.Paint) bool { return p.Name == "nameB" @@ -138,70 +169,19 @@ var _ = FDescribe("PaintSetV2", func() { Expect(intersectionA).ToNot(BeIdenticalTo(setA)) }) - // It("should correctly match two sets", func() { - // setA.Insert(paintA, paintBCluster2) - // setB.Insert(paintA, paintBCluster2) - // Expect(setA).To(Equal(setB)) - // setB.Insert(paintC) - // Expect(setA).ToNot(Equal(setB)) - // }) + It("should correctly match two sets", func() { + setA.Insert(paintA, paintBCluster2) + setB.Insert(paintA, paintBCluster2) + Expect(setA.Equal(setB)).To(BeTrue()) + setB.Insert(paintC) + Expect(setA.Equal(setB)).To(BeFalse()) + }) It("should return corrent length", func() { setA.Insert(paintA, paintBCluster2) Expect(setA.Len()).To(Equal(2)) }) - // It("should return set deltas", func() { - // // update - // oldPaintA := &v1.Paint{ - // ObjectMeta: metav1.ObjectMeta{Name: "background", Namespace: "color"}, - // Spec: v1.PaintSpec{Color: &v1.PaintColor{Hue: "orange"}}, - // } - // newPaintA := &v1.Paint{ - // ObjectMeta: metav1.ObjectMeta{Name: "background", Namespace: "color"}, - // Spec: v1.PaintSpec{Color: &v1.PaintColor{Hue: "green"}}, - // } - // // remove - // oldPaintB := &v1.Paint{ - // ObjectMeta: metav1.ObjectMeta{Name: "ugly", Namespace: "color"}, - // } - // // add - // newPaintC := &v1.Paint{ - // ObjectMeta: metav1.ObjectMeta{Name: "beautiful", Namespace: "color"}, - // } - // // no change - // oldPaintD := &v1.Paint{ - // ObjectMeta: metav1.ObjectMeta{Name: "decent", Namespace: "color"}, - // } - // newPaintD := &v1.Paint{ - // ObjectMeta: metav1.ObjectMeta{Name: "decent", Namespace: "color"}, - // } - // setA.Insert(oldPaintA, oldPaintB, oldPaintD) - // setB.Insert(newPaintA, newPaintC, newPaintD) - - // expectedDelta := sets_v2.ResourceDelta[*v1.Paint]{ - // Inserted: sets_v2.NewResourceSet(ezkube.ObjectsAscending, ezkube.CompareObjects, newPaintA, newPaintC), - // Removed: sets_v2.NewResourceSet(ezkube.ObjectsAscending, ezkube.CompareObjects, oldPaintB), - // } - - // actualDelta := setA.Delta(setB) - - // Expect(actualDelta.Removed.Length()).To(Equal(expectedDelta.DeltaV1().Removed.Length())) - // for _, removed := range actualDelta.Removed.List() { - // Expect(removed).To(Equal(oldPaintB)) - // } - - // Expect(actualDelta.Inserted.Length()).To(Equal(expectedDelta.DeltaV1().Inserted.Length())) - // for i, inserted := range actualDelta.Inserted.List() { - // if i == 0 { - // Expect(inserted).To(Equal(newPaintA)) - // } - // if i == 1 { - // Expect(inserted).To(Equal(newPaintC)) - // } - // } - // }) - It("should find resources", func() { oldPaintA := &v1.Paint{ ObjectMeta: metav1.ObjectMeta{ @@ -292,12 +272,19 @@ var _ = FDescribe("PaintSetV2", func() { setA.Insert(expectedOrder...) var paintList []*v1.Paint - for _, paint := range setA.List() { + var paintList2 []*v1.Paint + for _, paint := range setA.FilterOutAndCreateList() { paintList = append(paintList, paint) } + setA.Iter(func(_ int, paint *v1.Paint) bool { + paintList2 = append(paintList2, paint) + return true + }) + for i, paint := range expectedOrder { Expect(paintList[i]).To(Equal(paint)) + Expect(paintList2[i]).To(Equal(paint)) } }) }) From eb0dcc91f7cc682f9d27ef6251631a1c3bcb2774 Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Mon, 8 Apr 2024 13:27:35 -0400 Subject: [PATCH 17/23] remove FilterOutAndIterate method --- contrib/tests/set_v2_test.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/contrib/tests/set_v2_test.go b/contrib/tests/set_v2_test.go index 4026e9822..3889e3188 100644 --- a/contrib/tests/set_v2_test.go +++ b/contrib/tests/set_v2_test.go @@ -90,15 +90,6 @@ var _ = FDescribe("PaintSetV2", func() { } } - setA.FilterOutAndIterate(filterFn)(func(i int, p *v1.Paint) bool { - if i == 0 { - Expect(p).To(Equal(paintBCluster2)) - } - if i == 1 { - Expect(p).To(Equal(paintC)) - } - return true - }) }) It("should filter", func() { From 2edf415f6ced74cf4a04abb15994f74bcf540efa Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Mon, 8 Apr 2024 13:49:28 -0400 Subject: [PATCH 18/23] add Get --- contrib/pkg/sets/v2/sets.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/contrib/pkg/sets/v2/sets.go b/contrib/pkg/sets/v2/sets.go index 527385188..f531d1de8 100644 --- a/contrib/pkg/sets/v2/sets.go +++ b/contrib/pkg/sets/v2/sets.go @@ -49,8 +49,12 @@ type ResourceSet[T client.Object] interface { Difference(set ResourceSet[T]) ResourceSet[T] // Return the intersection with the provided set Intersection(set ResourceSet[T]) ResourceSet[T] - // Find the resource with the given ID + // Find the resource with the given ID. + // Returns a NotFoundErr error if the resource is not found. Find(resource ezkube.ResourceId) (T, error) + // Find the resource with the given ID. + // Returns nil if the resource is not found. + Get(resource ezkube.ResourceId) T // Get the length of the set Len() int Length() int @@ -274,6 +278,22 @@ func (s *resourceSet[T]) Intersection(set ResourceSet[T]) ResourceSet[T] { return result } +func (s *resourceSet[T]) Get(resource ezkube.ResourceId) T { + s.lock.RLock() + defer s.lock.RUnlock() + + insertIndex, found := slices.BinarySearchFunc( + s.set, + resource, + func(a T, b ezkube.ResourceId) int { return s.compareFunc(a, b) }, + ) + if found { + return s.set[insertIndex] + } + var r T + return r +} + func (s *resourceSet[T]) Find(resource ezkube.ResourceId) (T, error) { s.lock.RLock() defer s.lock.RUnlock() From 8ff5b6b935b295c41c3e438766273e9798a4a45f Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Mon, 8 Apr 2024 14:00:07 -0400 Subject: [PATCH 19/23] add comments --- contrib/pkg/sets/v2/sets.go | 65 ++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/contrib/pkg/sets/v2/sets.go b/contrib/pkg/sets/v2/sets.go index f531d1de8..b86ad6319 100644 --- a/contrib/pkg/sets/v2/sets.go +++ b/contrib/pkg/sets/v2/sets.go @@ -10,6 +10,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +// ResourceSet is a thread-safe container for a set of resources. +// It provides a set of operations for working with the set of resources, typically used for managing Kubernetes resources. +// The ResourceSet is a generic interface that can be used with any type that satisfies the client.Object interface. type ResourceSet[T client.Object] interface { // Get the set stored keys Keys() sets.Set[string] @@ -30,7 +33,9 @@ type ResourceSet[T client.Object] interface { // FilterOutAndCreateList constructs a list of resource that do not match any of the provided filters. // Use of this function should be limited to only when a filtered list is needed. - // For iteration that does not require creating a new list, use FilterOutAndIterate. + // For iteration that does not require creating a new list, use Iter. + // For iteration that requires typical filtering semantics (i.e. filters that return true for resources that should be included), + // use FilterOutAndCreateList(filterResource ...func(T) bool) []T // Return the Set as a map of key to resource. Map() map[string]T @@ -80,17 +85,15 @@ func (r *ResourceDelta[T]) DeltaV1() sk_sets.ResourceDelta { } type resourceSet[T client.Object] struct { - lock sync.RWMutex - set []T - compareFunc func(a, b ezkube.ResourceId) int + lock sync.RWMutex + set []T } func NewResourceSet[T client.Object]( resources ...T, ) ResourceSet[T] { rs := &resourceSet[T]{ - set: []T{}, - compareFunc: ezkube.ResourceIdsCompare, + set: []T{}, } rs.Insert(resources...) return rs @@ -106,26 +109,8 @@ func (s *resourceSet[T]) Keys() sets.Set[string] { return sets.Set[string]{} } -func (s *resourceSet[T]) FilterOutAndIterate(filterResource ...func(T) bool) func(yield func(int, T) bool) { - s.lock.RLock() - defer s.lock.RUnlock() - return func(yield func(int, T) bool) { - i := 0 - OUTER: - for _, resource := range s.set { - for _, filter := range filterResource { - if filter(resource) { - continue OUTER - } - } - if !yield(i, resource) { - break - } - i += 1 - } - } -} - +// Filter returns an iterator that will iterate over the set of elements +// that match the provided filter. If the filter returns true, the resource will be included in the iteration. func (s *resourceSet[T]) Filter(filterResource func(T) bool) func(yield func(int, T) bool) { s.lock.RLock() defer s.lock.RUnlock() @@ -142,6 +127,8 @@ func (s *resourceSet[T]) Filter(filterResource func(T) bool) func(yield func(int } } +// This is a convenience function that constructs a list of resource that do not match any of the provided filters. +// Use of this function should be limited to only when a filtered list is needed, as this allocates a new list. func (s *resourceSet[T]) FilterOutAndCreateList(filterResource ...func(T) bool) []T { s.lock.RLock() defer s.lock.RUnlock() @@ -161,6 +148,9 @@ func (s *resourceSet[T]) FilterOutAndCreateList(filterResource ...func(T) bool) return ret } +// Iter iterates over the set, passing the index and resource to the provided function for every element in the set. +// The iteration can be stopped by returning false from the function. This can be thought of as a "break" statement in a loop. +// Returning true will continue the iteration. This can be thought of as a "continue" statement in a loop. func (s *resourceSet[T]) Iter(yield func(int, T) bool) { s.lock.RLock() defer s.lock.RUnlock() @@ -181,9 +171,9 @@ func (s *resourceSet[T]) Map() map[string]T { return newMap } -// Insert adds items to the set. +// Insert adds items to the set in inserted order. // If an item is already in the set, it is overwritten. -// The set is sorted based on the compare func. +// The set is sorted based on the ResourceId of the resources. func (s *resourceSet[T]) Insert(resources ...T) { s.lock.RLock() defer s.lock.RUnlock() @@ -191,7 +181,7 @@ func (s *resourceSet[T]) Insert(resources ...T) { insertIndex, found := slices.BinarySearchFunc( s.set, resource, - func(a, b T) int { return s.compareFunc(a, b) }, + func(a, b T) int { return ezkube.ResourceIdsCompare(a, b) }, ) if found { s.set[insertIndex] = resource @@ -202,13 +192,14 @@ func (s *resourceSet[T]) Insert(resources ...T) { } } +// Uses binary search to check if the set contains the resource. func (s *resourceSet[T]) Has(resource T) bool { s.lock.RLock() defer s.lock.RUnlock() _, found := slices.BinarySearchFunc( s.set, resource, - func(a, b T) int { return s.compareFunc(a, b) }, + func(a, b T) int { return ezkube.ResourceIdsCompare(a, b) }, ) return found } @@ -228,7 +219,7 @@ func (s *resourceSet[T]) Delete(resource ezkube.ResourceId) { i, found := slices.BinarySearchFunc( s.set, resource, - func(a T, b ezkube.ResourceId) int { return s.compareFunc(a, b) }, + func(a T, b ezkube.ResourceId) int { return ezkube.ResourceIdsCompare(a, b) }, ) if found { s.set = slices.Delete(s.set, i, i+1) @@ -278,6 +269,9 @@ func (s *resourceSet[T]) Intersection(set ResourceSet[T]) ResourceSet[T] { return result } +// Get the resource with the given ID. +// Returns nil if the resource is not found. +// Uses binary search to find the resource. func (s *resourceSet[T]) Get(resource ezkube.ResourceId) T { s.lock.RLock() defer s.lock.RUnlock() @@ -285,7 +279,7 @@ func (s *resourceSet[T]) Get(resource ezkube.ResourceId) T { insertIndex, found := slices.BinarySearchFunc( s.set, resource, - func(a T, b ezkube.ResourceId) int { return s.compareFunc(a, b) }, + func(a T, b ezkube.ResourceId) int { return ezkube.ResourceIdsCompare(a, b) }, ) if found { return s.set[insertIndex] @@ -294,6 +288,11 @@ func (s *resourceSet[T]) Get(resource ezkube.ResourceId) T { return r } +// Find the resource with the given ID. +// Returns a NotFoundErr error if the resource is not found. +// Uses binary search to find the resource. +// This function is useful when you need to distinguish between a resource not found and a resource that is found but is nil, +// but it is less efficient than Get as it allocates an error object. func (s *resourceSet[T]) Find(resource ezkube.ResourceId) (T, error) { s.lock.RLock() defer s.lock.RUnlock() @@ -301,7 +300,7 @@ func (s *resourceSet[T]) Find(resource ezkube.ResourceId) (T, error) { insertIndex, found := slices.BinarySearchFunc( s.set, resource, - func(a T, b ezkube.ResourceId) int { return s.compareFunc(a, b) }, + func(a T, b ezkube.ResourceId) int { return ezkube.ResourceIdsCompare(a, b) }, ) if found { return s.set[insertIndex], nil From 874e6df53a06eeb1034aaf98d49fc05e1b42bb3c Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Mon, 8 Apr 2024 16:25:07 -0400 Subject: [PATCH 20/23] add changelog and remove test focus --- changelog/v0.36.6/sets-v2-iter.yaml | 8 +++++--- contrib/tests/set_v2_test.go | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/changelog/v0.36.6/sets-v2-iter.yaml b/changelog/v0.36.6/sets-v2-iter.yaml index f9b5f08d4..2cf272319 100644 --- a/changelog/v0.36.6/sets-v2-iter.yaml +++ b/changelog/v0.36.6/sets-v2-iter.yaml @@ -1,6 +1,8 @@ changelog: - - type: NON_USER_FACING - issueLink: + - type: BREAKING_CHANGE + issueLink: https://github.com/solo-io/skv2/issues/543 description: > - "" + v2 sets have been refactored to use a slice as the backing data structure, allowing for faster GC time of entire sets, + and more efficient iteration over the set. A few methods have been removed, namely `List` and `UnsortedList`, both which have been + replaced with a more accurate name: "FilterOutAndCreateList". In addition, a `Filter` and `Iter` method have been added to v2 sets. skipCI: "false" diff --git a/contrib/tests/set_v2_test.go b/contrib/tests/set_v2_test.go index 3889e3188..7c700e555 100644 --- a/contrib/tests/set_v2_test.go +++ b/contrib/tests/set_v2_test.go @@ -10,7 +10,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -var _ = FDescribe("PaintSetV2", func() { +var _ = Describe("PaintSetV2", func() { var ( setA, setB sets_v2.ResourceSet[*v1.Paint] paintA, paintBCluster2, paintC *v1.Paint From 9f948eb5ae87a928d3a1397ffa80b2dda93cb2f9 Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Mon, 8 Apr 2024 16:27:15 -0400 Subject: [PATCH 21/23] move changelog/ --- changelog/{v0.36.6 => v0.37.2}/sets-v2-iter.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog/{v0.36.6 => v0.37.2}/sets-v2-iter.yaml (100%) diff --git a/changelog/v0.36.6/sets-v2-iter.yaml b/changelog/v0.37.2/sets-v2-iter.yaml similarity index 100% rename from changelog/v0.36.6/sets-v2-iter.yaml rename to changelog/v0.37.2/sets-v2-iter.yaml From 9ddd0729080c919ca149b6a2f50a744d169467a7 Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Tue, 9 Apr 2024 08:11:55 -0400 Subject: [PATCH 22/23] move changelog --- changelog/{v0.37.2 => v0.38.0}/sets-v2-iter.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog/{v0.37.2 => v0.38.0}/sets-v2-iter.yaml (100%) diff --git a/changelog/v0.37.2/sets-v2-iter.yaml b/changelog/v0.38.0/sets-v2-iter.yaml similarity index 100% rename from changelog/v0.37.2/sets-v2-iter.yaml rename to changelog/v0.38.0/sets-v2-iter.yaml From 3661d26bedda8c48ba2183719da70c54894926a2 Mon Sep 17 00:00:00 2001 From: Keerthan Ekbote Date: Tue, 9 Apr 2024 11:10:03 -0400 Subject: [PATCH 23/23] fixes for PR review --- pkg/ezkube/resource_id.go | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/pkg/ezkube/resource_id.go b/pkg/ezkube/resource_id.go index 6248fbc63..3b1d8207d 100644 --- a/pkg/ezkube/resource_id.go +++ b/pkg/ezkube/resource_id.go @@ -179,19 +179,13 @@ func ResourceIdFromKeyWithSeparator(key string, separator string) (ResourceId, e // The result will be 0 if a == b, -1 if a < b, and +1 if a > b. func ResourceIdsCompare(a, b ResourceId) int { // compare names - if a.GetName() < b.GetName() { - return -1 - } - if a.GetName() > b.GetName() { - return 1 + if cmp := strings.Compare(a.GetName(), b.GetName()); cmp != 0 { + return cmp } // compare namespaces - if a.GetNamespace() < b.GetNamespace() { - return -1 - } - if a.GetNamespace() > b.GetNamespace() { - return 1 + if cmp := strings.Compare(a.GetNamespace(), b.GetNamespace()); cmp != 0 { + return cmp } // compare cluster names @@ -203,23 +197,15 @@ func ResourceIdsCompare(a, b ResourceId) int { if a_cri, ok := a.(ClusterResourceId); ok { aCluster = GetClusterName(a_cri) - } else if a_dcri, ok := a.(deprecatedClusterResourceId); ok { - aCluster = a_dcri.GetClusterName() + } else { + aCluster = getDeprecatedClusterName(a) } if b_cri, ok := b.(ClusterResourceId); ok { bCluster = GetClusterName(b_cri) - } else if b_dcri, ok := b.(deprecatedClusterResourceId); ok { - bCluster = b_dcri.GetClusterName() - } - - if aCluster < bCluster { - return -1 - } - if aCluster > bCluster { - return 1 + } else { + bCluster = getDeprecatedClusterName(b) } - // they are equal - return 0 + return strings.Compare(aCluster, bCluster) }