google.golang.org/grpc@v1.72.2/xds/internal/xdsclient/channel.go (about)

     1  /*
     2   *
     3   * Copyright 2024 gRPC authors.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  
    18  package xdsclient
    19  
    20  import (
    21  	"errors"
    22  	"fmt"
    23  	"strings"
    24  	"time"
    25  
    26  	"google.golang.org/grpc/grpclog"
    27  	"google.golang.org/grpc/internal/backoff"
    28  	igrpclog "google.golang.org/grpc/internal/grpclog"
    29  	"google.golang.org/grpc/internal/grpcsync"
    30  	"google.golang.org/grpc/internal/xds/bootstrap"
    31  	"google.golang.org/grpc/xds/internal/xdsclient/load"
    32  	"google.golang.org/grpc/xds/internal/xdsclient/transport"
    33  	"google.golang.org/grpc/xds/internal/xdsclient/transport/ads"
    34  	"google.golang.org/grpc/xds/internal/xdsclient/transport/lrs"
    35  	"google.golang.org/grpc/xds/internal/xdsclient/xdsresource"
    36  )
    37  
    38  // xdsChannelEventHandler wraps callbacks used to notify the xDS client about
    39  // events on the xdsChannel. Methods in this interface may be invoked
    40  // concurrently and the xDS client implementation needs to handle them in a
    41  // thread-safe manner.
    42  type xdsChannelEventHandler interface {
    43  	// adsStreamFailure is called when the xdsChannel encounters an ADS stream
    44  	// failure.
    45  	adsStreamFailure(error)
    46  
    47  	// adsResourceUpdate is called when the xdsChannel receives an ADS response
    48  	// from the xDS management server. The callback is provided with the
    49  	// following:
    50  	//   - the resource type of the resources in the response
    51  	//   - a map of resources in the response, keyed by resource name
    52  	//   - the metadata associated with the response
    53  	//   - a callback to be invoked when the updated is processed
    54  	adsResourceUpdate(xdsresource.Type, map[string]ads.DataAndErrTuple, xdsresource.UpdateMetadata, func())
    55  
    56  	// adsResourceDoesNotExist is called when the xdsChannel determines that a
    57  	// requested ADS resource does not exist.
    58  	adsResourceDoesNotExist(xdsresource.Type, string)
    59  }
    60  
    61  // xdsChannelOpts holds the options for creating a new xdsChannel.
    62  type xdsChannelOpts struct {
    63  	transport          transport.Transport           // Takes ownership of this transport.
    64  	serverConfig       *bootstrap.ServerConfig       // Configuration of the server to connect to.
    65  	bootstrapConfig    *bootstrap.Config             // Complete bootstrap configuration, used to decode resources.
    66  	resourceTypeGetter func(string) xdsresource.Type // Function to retrieve resource parsing functionality, based on resource type.
    67  	eventHandler       xdsChannelEventHandler        // Callbacks for ADS stream events.
    68  	backoff            func(int) time.Duration       // Backoff function to use for stream retries. Defaults to exponential backoff, if unset.
    69  	watchExpiryTimeout time.Duration                 // Timeout for ADS resource watch expiry.
    70  	logPrefix          string                        // Prefix to use for logging.
    71  }
    72  
    73  // newXDSChannel creates a new xdsChannel instance with the provided options.
    74  // It performs basic validation on the provided options and initializes the
    75  // xdsChannel with the necessary components.
    76  func newXDSChannel(opts xdsChannelOpts) (*xdsChannel, error) {
    77  	switch {
    78  	case opts.transport == nil:
    79  		return nil, errors.New("xdsChannel: transport is nil")
    80  	case opts.serverConfig == nil:
    81  		return nil, errors.New("xdsChannel: serverConfig is nil")
    82  	case opts.bootstrapConfig == nil:
    83  		return nil, errors.New("xdsChannel: bootstrapConfig is nil")
    84  	case opts.resourceTypeGetter == nil:
    85  		return nil, errors.New("xdsChannel: resourceTypeGetter is nil")
    86  	case opts.eventHandler == nil:
    87  		return nil, errors.New("xdsChannel: eventHandler is nil")
    88  	}
    89  
    90  	xc := &xdsChannel{
    91  		transport:          opts.transport,
    92  		serverConfig:       opts.serverConfig,
    93  		bootstrapConfig:    opts.bootstrapConfig,
    94  		resourceTypeGetter: opts.resourceTypeGetter,
    95  		eventHandler:       opts.eventHandler,
    96  		closed:             grpcsync.NewEvent(),
    97  	}
    98  
    99  	l := grpclog.Component("xds")
   100  	logPrefix := opts.logPrefix + fmt.Sprintf("[xds-channel %p] ", xc)
   101  	xc.logger = igrpclog.NewPrefixLogger(l, logPrefix)
   102  
   103  	if opts.backoff == nil {
   104  		opts.backoff = backoff.DefaultExponential.Backoff
   105  	}
   106  	xc.ads = ads.NewStreamImpl(ads.StreamOpts{
   107  		Transport:          xc.transport,
   108  		EventHandler:       xc,
   109  		Backoff:            opts.backoff,
   110  		NodeProto:          xc.bootstrapConfig.Node(),
   111  		WatchExpiryTimeout: opts.watchExpiryTimeout,
   112  		LogPrefix:          logPrefix,
   113  	})
   114  	xc.lrs = lrs.NewStreamImpl(lrs.StreamOpts{
   115  		Transport: xc.transport,
   116  		Backoff:   opts.backoff,
   117  		NodeProto: xc.bootstrapConfig.Node(),
   118  		LogPrefix: logPrefix,
   119  	})
   120  	return xc, nil
   121  }
   122  
   123  // xdsChannel represents a client channel to a management server, and is
   124  // responsible for managing the lifecycle of the ADS and LRS streams. It invokes
   125  // callbacks on the registered event handler for various ADS stream events.
   126  type xdsChannel struct {
   127  	// The following fields are initialized at creation time and are read-only
   128  	// after that, and hence need not be guarded by a mutex.
   129  	transport          transport.Transport           // Takes ownership of this transport (used to make streaming calls).
   130  	ads                *ads.StreamImpl               // An ADS stream to the management server.
   131  	lrs                *lrs.StreamImpl               // An LRS stream to the management server.
   132  	serverConfig       *bootstrap.ServerConfig       // Configuration of the server to connect to.
   133  	bootstrapConfig    *bootstrap.Config             // Complete bootstrap configuration, used to decode resources.
   134  	resourceTypeGetter func(string) xdsresource.Type // Function to retrieve resource parsing functionality, based on resource type.
   135  	eventHandler       xdsChannelEventHandler        // Callbacks for ADS stream events.
   136  	logger             *igrpclog.PrefixLogger        // Logger to use for logging.
   137  	closed             *grpcsync.Event               // Fired when the channel is closed.
   138  }
   139  
   140  func (xc *xdsChannel) close() {
   141  	xc.closed.Fire()
   142  	xc.ads.Stop()
   143  	xc.lrs.Stop()
   144  	xc.transport.Close()
   145  	xc.logger.Infof("Shutdown")
   146  }
   147  
   148  // reportLoad returns a load.Store that can be used to report load to the LRS, and a
   149  // function that can be called to stop reporting load.
   150  func (xc *xdsChannel) reportLoad() (*load.Store, func()) {
   151  	if xc.closed.HasFired() {
   152  		if xc.logger.V(2) {
   153  			xc.logger.Infof("Attempt to start load reporting on closed channel")
   154  		}
   155  		return nil, func() {}
   156  	}
   157  	return xc.lrs.ReportLoad()
   158  }
   159  
   160  // subscribe adds a subscription for the given resource name of the given
   161  // resource type on the ADS stream.
   162  func (xc *xdsChannel) subscribe(typ xdsresource.Type, name string) {
   163  	if xc.closed.HasFired() {
   164  		if xc.logger.V(2) {
   165  			xc.logger.Infof("Attempt to subscribe to an xDS resource of type %s and name %q on a closed channel", typ.TypeName(), name)
   166  		}
   167  		return
   168  	}
   169  	xc.ads.Subscribe(typ, name)
   170  }
   171  
   172  // unsubscribe removes the subscription for the given resource name of the given
   173  // resource type from the ADS stream.
   174  func (xc *xdsChannel) unsubscribe(typ xdsresource.Type, name string) {
   175  	if xc.closed.HasFired() {
   176  		if xc.logger.V(2) {
   177  			xc.logger.Infof("Attempt to unsubscribe to an xDS resource of type %s and name %q on a closed channel", typ.TypeName(), name)
   178  		}
   179  		return
   180  	}
   181  	xc.ads.Unsubscribe(typ, name)
   182  }
   183  
   184  // The following OnADSXxx() methods implement the ads.StreamEventHandler interface
   185  // and are invoked by the ADS stream implementation.
   186  
   187  // OnADSStreamError is invoked when an error occurs on the ADS stream. It
   188  // propagates the update to the xDS client.
   189  func (xc *xdsChannel) OnADSStreamError(err error) {
   190  	if xc.closed.HasFired() {
   191  		if xc.logger.V(2) {
   192  			xc.logger.Infof("Received ADS stream error on a closed xdsChannel: %v", err)
   193  		}
   194  		return
   195  	}
   196  	xc.eventHandler.adsStreamFailure(err)
   197  }
   198  
   199  // OnADSWatchExpiry is invoked when a watch for a resource expires. It
   200  // propagates the update to the xDS client.
   201  func (xc *xdsChannel) OnADSWatchExpiry(typ xdsresource.Type, name string) {
   202  	if xc.closed.HasFired() {
   203  		if xc.logger.V(2) {
   204  			xc.logger.Infof("Received ADS resource watch expiry for resource %q on a closed xdsChannel", name)
   205  		}
   206  		return
   207  	}
   208  	xc.eventHandler.adsResourceDoesNotExist(typ, name)
   209  }
   210  
   211  // OnADSResponse is invoked when a response is received on the ADS stream. It
   212  // decodes the resources in the response, and propagates the updates to the xDS
   213  // client.
   214  //
   215  // It returns the list of resource names in the response and any errors
   216  // encountered during decoding.
   217  func (xc *xdsChannel) OnADSResponse(resp ads.Response, onDone func()) ([]string, error) {
   218  	if xc.closed.HasFired() {
   219  		if xc.logger.V(2) {
   220  			xc.logger.Infof("Received an update from the ADS stream on closed ADS stream")
   221  		}
   222  		return nil, errors.New("xdsChannel is closed")
   223  	}
   224  
   225  	// Lookup the resource parser based on the resource type.
   226  	rType := xc.resourceTypeGetter(resp.TypeURL)
   227  	if rType == nil {
   228  		return nil, xdsresource.NewErrorf(xdsresource.ErrorTypeResourceTypeUnsupported, "Resource type URL %q unknown in response from server", resp.TypeURL)
   229  	}
   230  
   231  	// Decode the resources and build the list of resource names to return.
   232  	opts := &xdsresource.DecodeOptions{
   233  		BootstrapConfig: xc.bootstrapConfig,
   234  		ServerConfig:    xc.serverConfig,
   235  	}
   236  	updates, md, err := decodeResponse(opts, rType, resp)
   237  	var names []string
   238  	for name := range updates {
   239  		names = append(names, name)
   240  	}
   241  
   242  	xc.eventHandler.adsResourceUpdate(rType, updates, md, onDone)
   243  	return names, err
   244  }
   245  
   246  // decodeResponse decodes the resources in the given ADS response.
   247  //
   248  // The opts parameter provides configuration options for decoding the resources.
   249  // The rType parameter specifies the resource type parser to use for decoding
   250  // the resources.
   251  //
   252  // The returned map contains a key for each resource in the response, with the
   253  // value being either the decoded resource data or an error if decoding failed.
   254  // The returned metadata includes the version of the response, the timestamp of
   255  // the update, and the status of the update (ACKed or NACKed).
   256  //
   257  // If there are any errors decoding the resources, the metadata will indicate
   258  // that the update was NACKed, and the returned error will contain information
   259  // about all errors encountered by this function.
   260  func decodeResponse(opts *xdsresource.DecodeOptions, rType xdsresource.Type, resp ads.Response) (map[string]ads.DataAndErrTuple, xdsresource.UpdateMetadata, error) {
   261  	timestamp := time.Now()
   262  	md := xdsresource.UpdateMetadata{
   263  		Version:   resp.Version,
   264  		Timestamp: timestamp,
   265  	}
   266  
   267  	topLevelErrors := make([]error, 0)          // Tracks deserialization errors, where we don't have a resource name.
   268  	perResourceErrors := make(map[string]error) // Tracks resource validation errors, where we have a resource name.
   269  	ret := make(map[string]ads.DataAndErrTuple) // Return result, a map from resource name to either resource data or error.
   270  	for _, r := range resp.Resources {
   271  		result, err := rType.Decode(opts, r)
   272  
   273  		// Name field of the result is left unpopulated only when resource
   274  		// deserialization fails.
   275  		name := ""
   276  		if result != nil {
   277  			name = xdsresource.ParseName(result.Name).String()
   278  		}
   279  		if err == nil {
   280  			ret[name] = ads.DataAndErrTuple{Resource: result.Resource}
   281  			continue
   282  		}
   283  		if name == "" {
   284  			topLevelErrors = append(topLevelErrors, err)
   285  			continue
   286  		}
   287  		perResourceErrors[name] = err
   288  		// Add place holder in the map so we know this resource name was in
   289  		// the response.
   290  		ret[name] = ads.DataAndErrTuple{Err: xdsresource.NewError(xdsresource.ErrorTypeNACKed, err.Error())}
   291  	}
   292  
   293  	if len(topLevelErrors) == 0 && len(perResourceErrors) == 0 {
   294  		md.Status = xdsresource.ServiceStatusACKed
   295  		return ret, md, nil
   296  	}
   297  
   298  	md.Status = xdsresource.ServiceStatusNACKed
   299  	errRet := combineErrors(rType.TypeName(), topLevelErrors, perResourceErrors)
   300  	md.ErrState = &xdsresource.UpdateErrorMetadata{
   301  		Version:   resp.Version,
   302  		Err:       xdsresource.NewError(xdsresource.ErrorTypeNACKed, errRet.Error()),
   303  		Timestamp: timestamp,
   304  	}
   305  	return ret, md, errRet
   306  }
   307  
   308  func combineErrors(rType string, topLevelErrors []error, perResourceErrors map[string]error) error {
   309  	var errStrB strings.Builder
   310  	errStrB.WriteString(fmt.Sprintf("error parsing %q response: ", rType))
   311  	if len(topLevelErrors) > 0 {
   312  		errStrB.WriteString("top level errors: ")
   313  		for i, err := range topLevelErrors {
   314  			if i != 0 {
   315  				errStrB.WriteString(";\n")
   316  			}
   317  			errStrB.WriteString(err.Error())
   318  		}
   319  	}
   320  	if len(perResourceErrors) > 0 {
   321  		var i int
   322  		for name, err := range perResourceErrors {
   323  			if i != 0 {
   324  				errStrB.WriteString(";\n")
   325  			}
   326  			i++
   327  			errStrB.WriteString(fmt.Sprintf("resource %q: %v", name, err.Error()))
   328  		}
   329  	}
   330  	return errors.New(errStrB.String())
   331  }
   332  
   333  func (xc *xdsChannel) triggerResourceNotFoundForTesting(rType xdsresource.Type, resourceName string) error {
   334  	if xc.closed.HasFired() {
   335  		return fmt.Errorf("triggerResourceNotFoundForTesting() called on a closed channel")
   336  	}
   337  	if xc.logger.V(2) {
   338  		xc.logger.Infof("Triggering resource not found for type: %s, resource name: %s", rType.TypeName(), resourceName)
   339  	}
   340  	xc.ads.TriggerResourceNotFoundForTesting(rType, resourceName)
   341  	return nil
   342  }