github.com/imran-kn/cilium-fork@v1.6.9/pkg/envoy/xds/watcher.go (about)

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