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  }