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  }