github.com/cilium/cilium@v1.16.2/pkg/bgpv1/manager/store/diffstore.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package store 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 11 "github.com/cilium/hive/cell" 12 "github.com/cilium/hive/job" 13 k8sRuntime "k8s.io/apimachinery/pkg/runtime" 14 15 "github.com/cilium/cilium/pkg/bgpv1/agent/signaler" 16 "github.com/cilium/cilium/pkg/k8s/resource" 17 "github.com/cilium/cilium/pkg/lock" 18 "github.com/cilium/cilium/pkg/time" 19 ) 20 21 var ( 22 ErrStoreUninitialized = errors.New("the store has not initialized yet") 23 ErrDiffUninitialized = errors.New("diff not initialized for caller") 24 ) 25 26 // DiffStore is a wrapper around the resource.Store. The diffStore tracks all changes made to it since the 27 // last time the user synced up. This allows a user to get a list of just the changed objects while still being able 28 // to query the full store for a full sync. 29 type DiffStore[T k8sRuntime.Object] interface { 30 // InitDiff initializes tracking io items to Diff for the given callerID. 31 InitDiff(callerID string) 32 33 // Diff returns a list of items that have been upserted (updated or inserted) and deleted 34 // since InitDiff or the last call to Diff with the same callerID. 35 // Init(callerID) has to be called before Diff(callerID). 36 Diff(callerID string) (upserted []T, deleted []resource.Key, err error) 37 38 // CleanupDiff cleans up all caller-specific diff state. 39 CleanupDiff(callerID string) 40 41 // GetByKey returns the latest version of the object with given key. 42 GetByKey(key resource.Key) (item T, exists bool, err error) 43 44 // List returns all items currently in the store. 45 List() (items []T, err error) 46 } 47 48 var _ DiffStore[*k8sRuntime.Unknown] = (*diffStore[*k8sRuntime.Unknown])(nil) 49 50 type diffStoreParams[T k8sRuntime.Object] struct { 51 cell.In 52 53 Lifecycle cell.Lifecycle 54 Health cell.Health 55 JobGroup job.Group 56 Resource resource.Resource[T] 57 Signaler *signaler.BGPCPSignaler 58 } 59 60 // updatedKeysMap is a map of updated resource keys since the last diff against the map. 61 type updatedKeysMap map[resource.Key]bool 62 63 // diffStore takes a resource.Resource[T] and watches for events, it stores all of the keys that have been changed. 64 // diffStore can still be used as a normal store, but adds the Diff function to get a Diff of all changes. 65 // The diffStore also takes in Signaler which it will signal after the initial sync and every update thereafter. 66 type diffStore[T k8sRuntime.Object] struct { 67 store resource.Store[T] 68 69 resource resource.Resource[T] 70 signaler *signaler.BGPCPSignaler 71 72 initialSync bool 73 74 mu lock.Mutex 75 callerUpdatedKeys map[string]updatedKeysMap // updated keys per caller ID 76 } 77 78 func NewDiffStore[T k8sRuntime.Object](params diffStoreParams[T]) DiffStore[T] { 79 if params.Resource == nil { 80 return nil 81 } 82 83 ds := &diffStore[T]{ 84 resource: params.Resource, 85 signaler: params.Signaler, 86 87 callerUpdatedKeys: make(map[string]updatedKeysMap), 88 } 89 90 params.JobGroup.Add( 91 job.OneShot("diffstore-events", 92 func(ctx context.Context, health cell.Health) (err error) { 93 ds.store, err = ds.resource.Store(ctx) 94 if err != nil { 95 return fmt.Errorf("error creating resource store: %w", err) 96 } 97 for event := range ds.resource.Events(ctx) { 98 ds.handleEvent(event) 99 } 100 return nil 101 }, 102 job.WithRetry(3, &job.ExponentialBackoff{Min: 100 * time.Millisecond, Max: time.Second}), 103 job.WithShutdown()), 104 ) 105 106 return ds 107 } 108 109 func (ds *diffStore[T]) handleEvent(event resource.Event[T]) { 110 update := func(k resource.Key) { 111 ds.mu.Lock() 112 for _, updatedKeys := range ds.callerUpdatedKeys { 113 updatedKeys[k] = true 114 } 115 ds.mu.Unlock() 116 117 // Start triggering the signaler after initialization to reduce reconciliation load. 118 if ds.initialSync { 119 ds.signaler.Event(struct{}{}) 120 } 121 } 122 123 switch event.Kind { 124 case resource.Sync: 125 ds.initialSync = true 126 ds.signaler.Event(struct{}{}) 127 case resource.Upsert, resource.Delete: 128 update(event.Key) 129 } 130 131 event.Done(nil) 132 } 133 134 // InitDiff initializes tracking io items to Diff for the given callerID. 135 func (ds *diffStore[T]) InitDiff(callerID string) { 136 ds.mu.Lock() 137 defer ds.mu.Unlock() 138 139 ds.callerUpdatedKeys[callerID] = make(updatedKeysMap) 140 } 141 142 // Diff returns a list of items that have been upserted (updated or inserted) and deleted 143 // since InitDiff or the last call to Diff with the same callerID. 144 // Init(callerID) has to be called before Diff(callerID). 145 func (ds *diffStore[T]) Diff(callerID string) (upserted []T, deleted []resource.Key, err error) { 146 ds.mu.Lock() 147 defer ds.mu.Unlock() 148 149 if ds.store == nil { 150 return nil, nil, ErrStoreUninitialized 151 } 152 153 updatedKeys, ok := ds.callerUpdatedKeys[callerID] 154 if !ok { 155 return nil, nil, ErrDiffUninitialized 156 } 157 158 // Deleting keys doesn't shrink the memory size. So if the size of updateKeys ever reaches above this threshold 159 // we should re-create it to reduce memory usage. Below the threshold, don't bother to avoid unnecessary allocation. 160 // Note: this value is arbitrary, can be changed to tune CPU/Memory tradeoff 161 const shrinkThreshold = 64 162 shrink := len(updatedKeys) > shrinkThreshold 163 164 for k := range updatedKeys { 165 item, found, err := ds.store.GetByKey(k) 166 if err != nil { 167 return nil, nil, err 168 } 169 170 if found { 171 upserted = append(upserted, item) 172 } else { 173 deleted = append(deleted, k) 174 } 175 176 if !shrink { 177 delete(updatedKeys, k) 178 } 179 } 180 181 if shrink { 182 ds.callerUpdatedKeys[callerID] = make(updatedKeysMap, shrinkThreshold) 183 } 184 185 return upserted, deleted, err 186 } 187 188 // CleanupDiff cleans up all caller-specific diff state. 189 func (ds *diffStore[T]) CleanupDiff(callerID string) { 190 ds.mu.Lock() 191 defer ds.mu.Unlock() 192 193 delete(ds.callerUpdatedKeys, callerID) 194 } 195 196 // GetByKey returns the latest version of the object with given key. 197 func (ds *diffStore[T]) GetByKey(key resource.Key) (item T, exists bool, err error) { 198 ds.mu.Lock() 199 defer ds.mu.Unlock() 200 201 if ds.store == nil { 202 var empty T 203 return empty, false, ErrStoreUninitialized 204 } 205 206 return ds.store.GetByKey(key) 207 } 208 209 // List returns all items currently in the store. 210 func (ds *diffStore[T]) List() (items []T, err error) { 211 ds.mu.Lock() 212 defer ds.mu.Unlock() 213 214 if ds.store == nil { 215 return nil, ErrStoreUninitialized 216 } 217 218 return ds.store.List(), nil 219 }