github.com/cilium/cilium@v1.16.2/pkg/envoy/xds/watcher.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package xds 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "sync" 11 12 "github.com/sirupsen/logrus" 13 14 "github.com/cilium/cilium/pkg/lock" 15 "github.com/cilium/cilium/pkg/logging/logfields" 16 ) 17 18 // ResourceWatcher watches and retrieves new versions of resources from a 19 // resource set. 20 // ResourceWatcher implements ResourceVersionObserver to get notified when new 21 // resource versions are available in the set. 22 type ResourceWatcher struct { 23 // typeURL is the URL that uniquely identifies the resource type. 24 typeURL string 25 26 // resourceSet is the set of resources to watch. 27 resourceSet ResourceSource 28 29 // version is the current version of the resources. Updated in calls to 30 // NotifyNewVersion. 31 // Versioning starts at 1. 32 version uint64 33 34 // versionLocker is used to lock all accesses to version. 35 versionLocker lock.Mutex 36 37 // versionCond is a condition that is broadcast whenever the source's 38 // current version is increased. 39 // versionCond is associated with versionLocker. 40 versionCond *sync.Cond 41 } 42 43 // NewResourceWatcher creates a new ResourceWatcher backed by the given 44 // resource set. 45 func NewResourceWatcher(typeURL string, resourceSet ResourceSource) *ResourceWatcher { 46 w := &ResourceWatcher{ 47 version: 1, 48 typeURL: typeURL, 49 resourceSet: resourceSet, 50 } 51 w.versionCond = sync.NewCond(&w.versionLocker) 52 return w 53 } 54 55 func (w *ResourceWatcher) HandleNewResourceVersion(typeURL string, version uint64) { 56 w.versionLocker.Lock() 57 defer w.versionLocker.Unlock() 58 59 if typeURL != w.typeURL { 60 return 61 } 62 63 if version < w.version { 64 log.WithFields(logrus.Fields{ 65 logfields.XDSCachedVersion: version, 66 logfields.XDSTypeURL: typeURL, 67 }).Panicf(fmt.Sprintf("decreasing version number found for resources of type %s: %d < %d", 68 typeURL, version, w.version)) 69 } 70 w.version = version 71 72 w.versionCond.Broadcast() 73 } 74 75 // WatchResources watches for new versions of specific resources and sends them 76 // into the given out channel. 77 // 78 // A call to this method blocks until a version greater than lastVersion is 79 // available. Therefore, every call must be done in a separate goroutine. 80 // A watch can be canceled by canceling the given context. 81 // 82 // lastVersion is the last version successfully applied by the 83 // client; nil if this is the first request for resources. 84 // This method call must always close the out channel. 85 func (w *ResourceWatcher) WatchResources(ctx context.Context, typeURL string, lastVersion uint64, nodeIP string, 86 resourceNames []string, out chan<- *VersionedResources) { 87 defer close(out) 88 89 watchLog := log.WithFields(logrus.Fields{ 90 logfields.XDSAckedVersion: lastVersion, 91 logfields.XDSClientNode: nodeIP, 92 logfields.XDSTypeURL: typeURL, 93 }) 94 95 var res *VersionedResources 96 97 var waitVersion uint64 98 var waitForVersion bool 99 if lastVersion != 0 { 100 waitForVersion = true 101 waitVersion = lastVersion 102 } 103 104 for ctx.Err() == nil && res == nil { 105 w.versionLocker.Lock() 106 // If the client ACKed a version that we have never sent back, this 107 // indicates that this server restarted but the client survived and had 108 // received a higher version number from the previous server instance. 109 // Bump the resource set's version number to match the client's and 110 // send a response immediately. 111 if waitForVersion && w.version < waitVersion { 112 w.versionLocker.Unlock() 113 // Calling EnsureVersion will increase the version of the resource 114 // set, which in turn will callback w.HandleNewResourceVersion with 115 // that new version number. In order for that callback to not 116 // deadlock, temporarily unlock w.versionLocker. 117 // The w.HandleNewResourceVersion callback will update w.version to 118 // the new resource set version. 119 w.resourceSet.EnsureVersion(typeURL, waitVersion+1) 120 w.versionLocker.Lock() 121 } 122 123 // Re-check w.version, since it may have been modified by calling 124 // EnsureVersion above. 125 for ctx.Err() == nil && waitForVersion && w.version <= waitVersion { 126 watchLog.Debugf("current resource version is %d, waiting for it to become > %d", w.version, waitVersion) 127 w.versionCond.Wait() 128 } 129 // In case we need to loop again, wait for any version more recent than 130 // the current one. 131 waitForVersion = true 132 waitVersion = w.version 133 w.versionLocker.Unlock() 134 135 if ctx.Err() != nil { 136 break 137 } 138 139 watchLog.Debugf("getting %d resources from set", len(resourceNames)) 140 var err error 141 res, err = w.resourceSet.GetResources(typeURL, lastVersion, nodeIP, resourceNames) 142 if err != nil { 143 watchLog.WithError(err).Errorf("failed to query resources named: %v; terminating resource watch", resourceNames) 144 return 145 } 146 } 147 148 if res != nil { 149 // Resources have changed since the last version returned to the 150 // client. Send out the new version. 151 select { 152 case <-ctx.Done(): 153 case out <- res: 154 return 155 } 156 } 157 158 err := ctx.Err() 159 if err != nil { 160 if errors.Is(err, context.Canceled) { 161 watchLog.Debug("context canceled, terminating resource watch") 162 } else { 163 watchLog.WithError(err).Error("context error, terminating resource watch") 164 } 165 } 166 }