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 }