github.com/cilium/cilium@v1.16.2/pkg/envoy/xds/cache.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package xds
     5  
     6  import (
     7  	"sort"
     8  
     9  	"github.com/sirupsen/logrus"
    10  	"google.golang.org/protobuf/proto"
    11  
    12  	"github.com/cilium/cilium/pkg/logging/logfields"
    13  )
    14  
    15  // Cache is a key-value container which allows atomically updating entries and
    16  // incrementing a version number and notifying observers if the cache is actually
    17  // modified.
    18  // Cache implements the ObservableResourceSet interface.
    19  // This cache implementation ignores the proxy node identifiers, i.e. the same
    20  // resources are available under the same names to all nodes.
    21  type Cache struct {
    22  	*BaseObservableResourceSource
    23  
    24  	// resources is the map of cached resource name to resource entry.
    25  	resources map[cacheKey]cacheValue
    26  
    27  	// version is the current version of the resources in the cache.
    28  	// valid version numbers start at 1, which is the version of a cache
    29  	// before any modifications have been made
    30  	version uint64
    31  }
    32  
    33  // cacheKey uniquely identifies a resource.
    34  type cacheKey struct {
    35  	// typeURL is the URL that uniquely identifies the resource's type.
    36  	typeURL string
    37  
    38  	// resourceName is the name of the resource, unique among all the resources
    39  	// of this type.
    40  	resourceName string
    41  }
    42  
    43  // cacheValue is a cached resource.
    44  type cacheValue struct {
    45  	// resource is the resource in this cache entry.
    46  	resource proto.Message
    47  
    48  	// lastModifiedVersion is the version when this resource entry was last
    49  	// modified.
    50  	lastModifiedVersion uint64
    51  }
    52  
    53  // NewCache creates a new, empty cache with 0 as its current version.
    54  func NewCache() *Cache {
    55  	return &Cache{
    56  		BaseObservableResourceSource: NewBaseObservableResourceSource(),
    57  		resources:                    make(map[cacheKey]cacheValue),
    58  		version:                      1,
    59  	}
    60  }
    61  
    62  // tx inserts/updates a set of resources, then deletes a set of resources, then
    63  // increases the cache's version number atomically if the cache is actually
    64  // changed.
    65  // The version after updating the set is returned.
    66  func (c *Cache) tx(typeURL string, upsertedResources map[string]proto.Message, deletedNames []string) (version uint64, updated bool, revert ResourceMutatorRevertFunc) {
    67  	c.locker.Lock()
    68  	defer c.locker.Unlock()
    69  
    70  	cacheIsUpdated := false
    71  	newVersion := c.version + 1
    72  
    73  	cacheLog := log.WithFields(logrus.Fields{
    74  		logfields.XDSTypeURL:       typeURL,
    75  		logfields.XDSCachedVersion: newVersion,
    76  	})
    77  
    78  	cacheLog.Debugf("preparing new cache transaction: upserting %d entries, deleting %d entries",
    79  		len(upsertedResources), len(deletedNames))
    80  
    81  	// The parameters to pass to tx in revertFunc.
    82  	var revertUpsertedResources map[string]proto.Message
    83  	var revertDeletedNames []string
    84  
    85  	k := cacheKey{
    86  		typeURL: typeURL,
    87  	}
    88  
    89  	v := cacheValue{
    90  		lastModifiedVersion: newVersion,
    91  	}
    92  
    93  	for name, value := range upsertedResources {
    94  		k.resourceName = name
    95  		oldV, found := c.resources[k]
    96  		// If the value is unchanged, don't update the entry, to preserve its
    97  		// lastModifiedVersion. This allows minimizing the frequency of
    98  		// responses in GetResources.
    99  		if !found || !proto.Equal(oldV.resource, value) {
   100  			if found {
   101  				cacheLog.WithField(logfields.XDSResourceName, name).Debug("updating resource in cache")
   102  
   103  				if revertUpsertedResources == nil {
   104  					revertUpsertedResources = make(map[string]proto.Message, len(upsertedResources)+len(deletedNames))
   105  				}
   106  				revertUpsertedResources[name] = oldV.resource
   107  			} else {
   108  				cacheLog.WithField(logfields.XDSResourceName, name).Debug("inserting resource into cache")
   109  
   110  				if revertDeletedNames == nil {
   111  					revertDeletedNames = make([]string, 0, len(upsertedResources))
   112  				}
   113  				revertDeletedNames = append(revertDeletedNames, name)
   114  			}
   115  			cacheIsUpdated = true
   116  			v.resource = value
   117  			c.resources[k] = v
   118  		}
   119  	}
   120  
   121  	for _, name := range deletedNames {
   122  		k.resourceName = name
   123  		oldV, found := c.resources[k]
   124  		if found {
   125  			cacheLog.WithField(logfields.XDSResourceName, name).
   126  				Debug("deleting resource from cache")
   127  
   128  			if revertUpsertedResources == nil {
   129  				revertUpsertedResources = make(map[string]proto.Message, len(upsertedResources)+len(deletedNames))
   130  			}
   131  			revertUpsertedResources[name] = oldV.resource
   132  
   133  			cacheIsUpdated = true
   134  			delete(c.resources, k)
   135  		}
   136  	}
   137  
   138  	if cacheIsUpdated {
   139  		cacheLog.Debug("committing cache transaction and notifying of new version")
   140  		c.version = newVersion
   141  		c.NotifyNewResourceVersionRLocked(typeURL, c.version)
   142  
   143  		revert = func() (version uint64, updated bool) {
   144  			version, updated, _ = c.tx(typeURL, revertUpsertedResources, revertDeletedNames)
   145  			return
   146  		}
   147  	} else {
   148  		cacheLog.Debug("cache unmodified by transaction; aborting")
   149  	}
   150  
   151  	return c.version, cacheIsUpdated, revert
   152  }
   153  
   154  func (c *Cache) Upsert(typeURL string, resourceName string, resource proto.Message) (version uint64, updated bool, revert ResourceMutatorRevertFunc) {
   155  	return c.tx(typeURL, map[string]proto.Message{resourceName: resource}, nil)
   156  }
   157  
   158  func (c *Cache) Delete(typeURL string, resourceName string) (version uint64, updated bool, revert ResourceMutatorRevertFunc) {
   159  	return c.tx(typeURL, nil, []string{resourceName})
   160  }
   161  
   162  func (c *Cache) Clear(typeURL string) (version uint64, updated bool) {
   163  	c.locker.Lock()
   164  	defer c.locker.Unlock()
   165  
   166  	cacheIsUpdated := false
   167  	newVersion := c.version + 1
   168  
   169  	cacheLog := log.WithFields(logrus.Fields{
   170  		logfields.XDSTypeURL:       typeURL,
   171  		logfields.XDSCachedVersion: newVersion,
   172  	})
   173  
   174  	cacheLog.Debug("preparing new cache transaction: deleting all entries")
   175  
   176  	for k := range c.resources {
   177  		if k.typeURL == typeURL {
   178  			cacheLog.WithField(logfields.XDSResourceName, k.resourceName).
   179  				Debug("deleting resource from cache")
   180  			cacheIsUpdated = true
   181  			delete(c.resources, k)
   182  		}
   183  	}
   184  
   185  	if cacheIsUpdated {
   186  		cacheLog.Debug("committing cache transaction and notifying of new version")
   187  		c.version = newVersion
   188  		c.NotifyNewResourceVersionRLocked(typeURL, c.version)
   189  	} else {
   190  		cacheLog.Debug("cache unmodified by transaction; aborting")
   191  	}
   192  
   193  	return c.version, cacheIsUpdated
   194  }
   195  
   196  func (c *Cache) GetResources(typeURL string, lastVersion uint64, nodeIP string, resourceNames []string) (*VersionedResources, error) {
   197  	c.locker.RLock()
   198  	defer c.locker.RUnlock()
   199  
   200  	cacheLog := log.WithFields(logrus.Fields{
   201  		logfields.XDSAckedVersion: lastVersion,
   202  		logfields.XDSClientNode:   nodeIP,
   203  		logfields.XDSTypeURL:      typeURL,
   204  	})
   205  
   206  	res := &VersionedResources{
   207  		Version: c.version,
   208  		Canary:  false,
   209  	}
   210  
   211  	// Return all resources.
   212  	// TODO: return nil if no changes since the last version?
   213  	if len(resourceNames) == 0 {
   214  		res.ResourceNames = make([]string, 0, len(c.resources))
   215  		res.Resources = make([]proto.Message, 0, len(c.resources))
   216  		cacheLog.Debugf("no resource names requested, returning all %d resources", len(c.resources))
   217  		for k, v := range c.resources {
   218  			res.ResourceNames = append(res.ResourceNames, k.resourceName)
   219  			res.Resources = append(res.Resources, v.resource)
   220  		}
   221  		return res, nil
   222  	}
   223  
   224  	// Return only the resources with the requested names.
   225  
   226  	// As an optimization, if all the requested resources are found but none of
   227  	// them has been modified since the lastVersion, return no response.
   228  	// If at least one resource is not found, return all the found resources
   229  	// anyway, because we don't know whether the missing resource was deleted
   230  	// after the lastVersion, so we can't optimize in this case.
   231  
   232  	res.ResourceNames = make([]string, 0, len(resourceNames))
   233  	res.Resources = make([]proto.Message, 0, len(resourceNames))
   234  
   235  	k := cacheKey{typeURL: typeURL}
   236  
   237  	allResourcesFound := true
   238  	updatedSinceLastVersion := false
   239  
   240  	cacheLog.Debugf("%d resource names requested, filtering resources", len(resourceNames))
   241  
   242  	for _, name := range resourceNames {
   243  		k.resourceName = name
   244  		v, found := c.resources[k]
   245  		if found {
   246  			cacheLog.WithField(logfields.XDSResourceName, name).
   247  				Debugf("resource found, last modified in version %d", v.lastModifiedVersion)
   248  			if lastVersion == 0 || (lastVersion < v.lastModifiedVersion) {
   249  				updatedSinceLastVersion = true
   250  			}
   251  			res.ResourceNames = append(res.ResourceNames, name)
   252  			res.Resources = append(res.Resources, v.resource)
   253  		} else {
   254  			cacheLog.WithField(logfields.XDSResourceName, name).Debug("resource not found")
   255  			allResourcesFound = false
   256  		}
   257  	}
   258  
   259  	if allResourcesFound && !updatedSinceLastVersion {
   260  		cacheLog.Debug("all requested resources found but not updated since last version, returning no response")
   261  		return nil, nil
   262  	}
   263  
   264  	sort.Strings(res.ResourceNames)
   265  
   266  	cacheLog.Debugf("returning %d resources out of %d requested", len(res.Resources), len(resourceNames))
   267  	return res, nil
   268  }
   269  
   270  func (c *Cache) EnsureVersion(typeURL string, version uint64) {
   271  	c.locker.Lock()
   272  	defer c.locker.Unlock()
   273  
   274  	if c.version < version {
   275  		cacheLog := log.WithFields(logrus.Fields{
   276  			logfields.XDSTypeURL:      typeURL,
   277  			logfields.XDSAckedVersion: version,
   278  		})
   279  		cacheLog.Debug("increasing version to match client and notifying of new version")
   280  
   281  		c.version = version
   282  		c.NotifyNewResourceVersionRLocked(typeURL, c.version)
   283  	}
   284  }
   285  
   286  // Lookup finds the resource corresponding to the specified typeURL and resourceName,
   287  // if available, and returns it. Otherwise, returns nil. If an error occurs while
   288  // fetching the resource, also returns the error.
   289  func (c *Cache) Lookup(typeURL string, resourceName string) (proto.Message, error) {
   290  	res, err := c.GetResources(typeURL, 0, "", []string{resourceName})
   291  	if err != nil || res == nil || len(res.Resources) == 0 {
   292  		return nil, err
   293  	}
   294  	return res.Resources[0], nil
   295  }