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

     1  /*
     2   *
     3   * Copyright 2025 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  
    19  package xdsclient
    20  
    21  import (
    22  	"errors"
    23  	"fmt"
    24  	"strings"
    25  	"time"
    26  
    27  	"google.golang.org/grpc/grpclog"
    28  	igrpclog "google.golang.org/grpc/internal/grpclog"
    29  	"google.golang.org/grpc/xds/internal/clients"
    30  	"google.golang.org/grpc/xds/internal/clients/internal"
    31  	"google.golang.org/grpc/xds/internal/clients/internal/backoff"
    32  	"google.golang.org/grpc/xds/internal/clients/internal/syncutil"
    33  	"google.golang.org/grpc/xds/internal/clients/xdsclient/internal/xdsresource"
    34  )
    35  
    36  const (
    37  	clientFeatureNoOverprovisioning = "envoy.lb.does_not_support_overprovisioning"
    38  	clientFeatureResourceWrapper    = "xds.config.resource-in-sotw"
    39  )
    40  
    41  // xdsChannelEventHandler wraps callbacks used to notify the xDS client about
    42  // events on the xdsChannel. Methods in this interface may be invoked
    43  // concurrently and the xDS client implementation needs to handle them in a
    44  // thread-safe manner.
    45  type xdsChannelEventHandler interface {
    46  	// adsStreamFailure is called when the xdsChannel encounters an ADS stream
    47  	// failure.
    48  	adsStreamFailure(error)
    49  
    50  	// adsResourceUpdate is called when the xdsChannel receives an ADS response
    51  	// from the xDS management server. The callback is provided with the
    52  	// following:
    53  	//   - the resource type of the resources in the response
    54  	//   - a map of resources in the response, keyed by resource name
    55  	//   - the metadata associated with the response
    56  	//   - a callback to be invoked when the updated is processed
    57  	adsResourceUpdate(ResourceType, map[string]dataAndErrTuple, xdsresource.UpdateMetadata, func())
    58  
    59  	// adsResourceDoesNotExist is called when the xdsChannel determines that a
    60  	// requested ADS resource does not exist.
    61  	adsResourceDoesNotExist(ResourceType, string)
    62  }
    63  
    64  // xdsChannelOpts holds the options for creating a new xdsChannel.
    65  type xdsChannelOpts struct {
    66  	transport          clients.Transport       // Takes ownership of this transport.
    67  	serverConfig       *ServerConfig           // Configuration of the server to connect to.
    68  	clientConfig       *Config                 // Complete xDS client configuration, used to decode resources.
    69  	eventHandler       xdsChannelEventHandler  // Callbacks for ADS stream events.
    70  	backoff            func(int) time.Duration // Backoff function to use for stream retries. Defaults to exponential backoff, if unset.
    71  	watchExpiryTimeout time.Duration           // Timeout for ADS resource watch expiry.
    72  	logPrefix          string                  // Prefix to use for logging.
    73  }
    74  
    75  // newXDSChannel creates a new xdsChannel instance with the provided options.
    76  // It performs basic validation on the provided options and initializes the
    77  // xdsChannel with the necessary components.
    78  func newXDSChannel(opts xdsChannelOpts) (*xdsChannel, error) {
    79  	switch {
    80  	case opts.transport == nil:
    81  		return nil, errors.New("xdsclient: transport is nil")
    82  	case opts.serverConfig == nil:
    83  		return nil, errors.New("xdsclient: serverConfig is nil")
    84  	case opts.clientConfig == nil:
    85  		return nil, errors.New("xdsclient: clientConfig is nil")
    86  	case opts.eventHandler == nil:
    87  		return nil, errors.New("xdsclient: eventHandler is nil")
    88  	}
    89  
    90  	xc := &xdsChannel{
    91  		transport:    opts.transport,
    92  		serverConfig: opts.serverConfig,
    93  		clientConfig: opts.clientConfig,
    94  		eventHandler: opts.eventHandler,
    95  		closed:       syncutil.NewEvent(),
    96  	}
    97  
    98  	l := grpclog.Component("xds")
    99  	logPrefix := opts.logPrefix + fmt.Sprintf("[xds-channel %p] ", xc)
   100  	xc.logger = igrpclog.NewPrefixLogger(l, logPrefix)
   101  
   102  	if opts.backoff == nil {
   103  		opts.backoff = backoff.DefaultExponential.Backoff
   104  	}
   105  	np := internal.NodeProto(opts.clientConfig.Node)
   106  	np.ClientFeatures = []string{clientFeatureNoOverprovisioning, clientFeatureResourceWrapper}
   107  	xc.ads = newADSStreamImpl(adsStreamOpts{
   108  		transport:          opts.transport,
   109  		eventHandler:       xc,
   110  		backoff:            opts.backoff,
   111  		nodeProto:          np,
   112  		watchExpiryTimeout: opts.watchExpiryTimeout,
   113  		logPrefix:          logPrefix,
   114  	})
   115  	if xc.logger.V(2) {
   116  		xc.logger.Infof("xdsChannel is created for ServerConfig %v", opts.serverConfig)
   117  	}
   118  	return xc, nil
   119  }
   120  
   121  // xdsChannel represents a client channel to a management server, and is
   122  // responsible for managing the lifecycle of the ADS and LRS streams. It invokes
   123  // callbacks on the registered event handler for various ADS stream events.
   124  //
   125  // It is safe for concurrent use.
   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    clients.Transport      // Takes ownership of this transport (used to make streaming calls).
   130  	ads          *adsStreamImpl         // An ADS stream to the management server.
   131  	serverConfig *ServerConfig          // Configuration of the server to connect to.
   132  	clientConfig *Config                // Complete xDS client configuration, used to decode resources.
   133  	eventHandler xdsChannelEventHandler // Callbacks for ADS stream events.
   134  	logger       *igrpclog.PrefixLogger // Logger to use for logging.
   135  	closed       *syncutil.Event        // Fired when the channel is closed.
   136  }
   137  
   138  func (xc *xdsChannel) close() {
   139  	xc.closed.Fire()
   140  	xc.ads.Stop()
   141  	xc.transport.Close()
   142  	xc.logger.Infof("Shutdown")
   143  }
   144  
   145  // subscribe adds a subscription for the given resource name of the given
   146  // resource type on the ADS stream.
   147  func (xc *xdsChannel) subscribe(typ ResourceType, name string) {
   148  	if xc.closed.HasFired() {
   149  		if xc.logger.V(2) {
   150  			xc.logger.Infof("Attempt to subscribe to an xDS resource of type %s and name %q on a closed channel", typ.TypeName, name)
   151  		}
   152  		return
   153  	}
   154  	xc.ads.subscribe(typ, name)
   155  }
   156  
   157  // unsubscribe removes the subscription for the given resource name of the given
   158  // resource type from the ADS stream.
   159  func (xc *xdsChannel) unsubscribe(typ ResourceType, name string) {
   160  	if xc.closed.HasFired() {
   161  		if xc.logger.V(2) {
   162  			xc.logger.Infof("Attempt to unsubscribe to an xDS resource of type %s and name %q on a closed channel", typ.TypeName, name)
   163  		}
   164  		return
   165  	}
   166  	xc.ads.Unsubscribe(typ, name)
   167  }
   168  
   169  // The following onADSXxx() methods implement the StreamEventHandler interface
   170  // and are invoked by the ADS stream implementation.
   171  
   172  // onStreamError is invoked when an error occurs on the ADS stream. It
   173  // propagates the update to the xDS client.
   174  func (xc *xdsChannel) onStreamError(err error) {
   175  	if xc.closed.HasFired() {
   176  		if xc.logger.V(2) {
   177  			xc.logger.Infof("Received ADS stream error on a closed xdsChannel: %v", err)
   178  		}
   179  		return
   180  	}
   181  	xc.eventHandler.adsStreamFailure(err)
   182  }
   183  
   184  // onWatchExpiry is invoked when a watch for a resource expires. It
   185  // propagates the update to the xDS client.
   186  func (xc *xdsChannel) onWatchExpiry(typ ResourceType, name string) {
   187  	if xc.closed.HasFired() {
   188  		if xc.logger.V(2) {
   189  			xc.logger.Infof("Received ADS resource watch expiry for resource %q on a closed xdsChannel", name)
   190  		}
   191  		return
   192  	}
   193  	xc.eventHandler.adsResourceDoesNotExist(typ, name)
   194  }
   195  
   196  // onResponse is invoked when a response is received on the ADS stream. It
   197  // decodes the resources in the response, and propagates the updates to the xDS
   198  // client.
   199  //
   200  // It returns the list of resource names in the response and any errors
   201  // encountered during decoding.
   202  func (xc *xdsChannel) onResponse(resp response, onDone func()) ([]string, error) {
   203  	if xc.closed.HasFired() {
   204  		if xc.logger.V(2) {
   205  			xc.logger.Infof("Received an update from the ADS stream on closed ADS stream")
   206  		}
   207  		return nil, errors.New("xdsChannel is closed")
   208  	}
   209  
   210  	// Lookup the resource parser based on the resource type.
   211  	rType, ok := xc.clientConfig.ResourceTypes[resp.typeURL]
   212  	if !ok {
   213  		return nil, xdsresource.NewErrorf(xdsresource.ErrorTypeResourceTypeUnsupported, "Resource type URL %q unknown in response from server", resp.typeURL)
   214  	}
   215  
   216  	// Decode the resources and build the list of resource names to return.
   217  	opts := &DecodeOptions{
   218  		Config:       xc.clientConfig,
   219  		ServerConfig: xc.serverConfig,
   220  	}
   221  	updates, md, err := decodeResponse(opts, &rType, resp)
   222  	var names []string
   223  	for name := range updates {
   224  		names = append(names, name)
   225  	}
   226  
   227  	xc.eventHandler.adsResourceUpdate(rType, updates, md, onDone)
   228  	return names, err
   229  }
   230  
   231  // decodeResponse decodes the resources in the given ADS response.
   232  //
   233  // The opts parameter provides configuration options for decoding the resources.
   234  // The rType parameter specifies the resource type parser to use for decoding
   235  // the resources.
   236  //
   237  // The returned map contains a key for each resource in the response, with the
   238  // value being either the decoded resource data or an error if decoding failed.
   239  // The returned metadata includes the version of the response, the timestamp of
   240  // the update, and the status of the update (ACKed or NACKed).
   241  //
   242  // If there are any errors decoding the resources, the metadata will indicate
   243  // that the update was NACKed, and the returned error will contain information
   244  // about all errors encountered by this function.
   245  func decodeResponse(opts *DecodeOptions, rType *ResourceType, resp response) (map[string]dataAndErrTuple, xdsresource.UpdateMetadata, error) {
   246  	timestamp := time.Now()
   247  	md := xdsresource.UpdateMetadata{
   248  		Version:   resp.version,
   249  		Timestamp: timestamp,
   250  	}
   251  
   252  	topLevelErrors := make([]error, 0)          // Tracks deserialization errors, where we don't have a resource name.
   253  	perResourceErrors := make(map[string]error) // Tracks resource validation errors, where we have a resource name.
   254  	ret := make(map[string]dataAndErrTuple)     // Return result, a map from resource name to either resource data or error.
   255  	for _, r := range resp.resources {
   256  		result, err := rType.Decoder.Decode(r.GetValue(), *opts)
   257  
   258  		// Name field of the result is left unpopulated only when resource
   259  		// deserialization fails.
   260  		name := ""
   261  		if result != nil {
   262  			name = xdsresource.ParseName(result.Name).String()
   263  		}
   264  		if err == nil {
   265  			ret[name] = dataAndErrTuple{Resource: result.Resource}
   266  			continue
   267  		}
   268  		if name == "" {
   269  			topLevelErrors = append(topLevelErrors, err)
   270  			continue
   271  		}
   272  		perResourceErrors[name] = err
   273  		// Add place holder in the map so we know this resource name was in
   274  		// the response.
   275  		ret[name] = dataAndErrTuple{Err: xdsresource.NewError(xdsresource.ErrorTypeNACKed, err.Error())}
   276  	}
   277  
   278  	if len(topLevelErrors) == 0 && len(perResourceErrors) == 0 {
   279  		md.Status = xdsresource.ServiceStatusACKed
   280  		return ret, md, nil
   281  	}
   282  
   283  	md.Status = xdsresource.ServiceStatusNACKed
   284  	errRet := combineErrors(rType.TypeName, topLevelErrors, perResourceErrors)
   285  	md.ErrState = &xdsresource.UpdateErrorMetadata{
   286  		Version:   resp.version,
   287  		Err:       xdsresource.NewError(xdsresource.ErrorTypeNACKed, errRet.Error()),
   288  		Timestamp: timestamp,
   289  	}
   290  	return ret, md, errRet
   291  }
   292  
   293  func combineErrors(rType string, topLevelErrors []error, perResourceErrors map[string]error) error {
   294  	var errStrB strings.Builder
   295  	errStrB.WriteString(fmt.Sprintf("error parsing %q response: ", rType))
   296  	if len(topLevelErrors) > 0 {
   297  		errStrB.WriteString("top level errors: ")
   298  		for i, err := range topLevelErrors {
   299  			if i != 0 {
   300  				errStrB.WriteString(";\n")
   301  			}
   302  			errStrB.WriteString(err.Error())
   303  		}
   304  	}
   305  	if len(perResourceErrors) > 0 {
   306  		var i int
   307  		for name, err := range perResourceErrors {
   308  			if i != 0 {
   309  				errStrB.WriteString(";\n")
   310  			}
   311  			i++
   312  			errStrB.WriteString(fmt.Sprintf("resource %q: %v", name, err.Error()))
   313  		}
   314  	}
   315  	return errors.New(errStrB.String())
   316  }