google.golang.org/grpc@v1.72.2/xds/internal/resolver/serviceconfig.go (about)

     1  /*
     2   *
     3   * Copyright 2020 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 resolver
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"math/bits"
    26  	rand "math/rand/v2"
    27  	"strings"
    28  	"sync/atomic"
    29  	"time"
    30  
    31  	xxhash "github.com/cespare/xxhash/v2"
    32  	"google.golang.org/grpc/codes"
    33  	"google.golang.org/grpc/internal/grpcutil"
    34  	iresolver "google.golang.org/grpc/internal/resolver"
    35  	"google.golang.org/grpc/internal/serviceconfig"
    36  	"google.golang.org/grpc/internal/wrr"
    37  	"google.golang.org/grpc/metadata"
    38  	"google.golang.org/grpc/status"
    39  	"google.golang.org/grpc/xds/internal/balancer/clustermanager"
    40  	"google.golang.org/grpc/xds/internal/balancer/ringhash"
    41  	"google.golang.org/grpc/xds/internal/httpfilter"
    42  	"google.golang.org/grpc/xds/internal/xdsclient/xdsresource"
    43  )
    44  
    45  const (
    46  	cdsName                      = "cds_experimental"
    47  	xdsClusterManagerName        = "xds_cluster_manager_experimental"
    48  	clusterPrefix                = "cluster:"
    49  	clusterSpecifierPluginPrefix = "cluster_specifier_plugin:"
    50  )
    51  
    52  type serviceConfig struct {
    53  	LoadBalancingConfig balancerConfig `json:"loadBalancingConfig"`
    54  }
    55  
    56  type balancerConfig []map[string]any
    57  
    58  func newBalancerConfig(name string, config any) balancerConfig {
    59  	return []map[string]any{{name: config}}
    60  }
    61  
    62  type cdsBalancerConfig struct {
    63  	Cluster string `json:"cluster"`
    64  }
    65  
    66  type xdsChildConfig struct {
    67  	ChildPolicy balancerConfig `json:"childPolicy"`
    68  }
    69  
    70  type xdsClusterManagerConfig struct {
    71  	Children map[string]xdsChildConfig `json:"children"`
    72  }
    73  
    74  // serviceConfigJSON produces a service config in JSON format that contains LB
    75  // policy config for the "xds_cluster_manager" LB policy, with entries in the
    76  // children map for all active clusters.
    77  func serviceConfigJSON(activeClusters map[string]*clusterInfo) []byte {
    78  	// Generate children (all entries in activeClusters).
    79  	children := make(map[string]xdsChildConfig)
    80  	for cluster, ci := range activeClusters {
    81  		children[cluster] = ci.cfg
    82  	}
    83  
    84  	sc := serviceConfig{
    85  		LoadBalancingConfig: newBalancerConfig(
    86  			xdsClusterManagerName, xdsClusterManagerConfig{Children: children},
    87  		),
    88  	}
    89  
    90  	// This is not expected to fail as we have constructed the service config by
    91  	// hand right above, and therefore ok to panic.
    92  	bs, err := json.Marshal(sc)
    93  	if err != nil {
    94  		panic(fmt.Sprintf("failed to marshal service config %+v: %v", sc, err))
    95  	}
    96  	return bs
    97  }
    98  
    99  type virtualHost struct {
   100  	// map from filter name to its config
   101  	httpFilterConfigOverride map[string]httpfilter.FilterConfig
   102  	// retry policy present in virtual host
   103  	retryConfig *xdsresource.RetryConfig
   104  }
   105  
   106  // routeCluster holds information about a cluster as referenced by a route.
   107  type routeCluster struct {
   108  	name string
   109  	// map from filter name to its config
   110  	httpFilterConfigOverride map[string]httpfilter.FilterConfig
   111  }
   112  
   113  type route struct {
   114  	m                 *xdsresource.CompositeMatcher // converted from route matchers
   115  	actionType        xdsresource.RouteActionType   // holds route action type
   116  	clusters          wrr.WRR                       // holds *routeCluster entries
   117  	maxStreamDuration time.Duration
   118  	// map from filter name to its config
   119  	httpFilterConfigOverride map[string]httpfilter.FilterConfig
   120  	retryConfig              *xdsresource.RetryConfig
   121  	hashPolicies             []*xdsresource.HashPolicy
   122  }
   123  
   124  func (r route) String() string {
   125  	return fmt.Sprintf("%s -> { clusters: %v, maxStreamDuration: %v }", r.m.String(), r.clusters, r.maxStreamDuration)
   126  }
   127  
   128  // stoppableConfigSelector extends the iresolver.ConfigSelector interface with a
   129  // stop() method. This makes it possible to swap the current config selector
   130  // with an erroring config selector when the LDS or RDS resource is not found on
   131  // the management server.
   132  type stoppableConfigSelector interface {
   133  	iresolver.ConfigSelector
   134  	stop()
   135  }
   136  
   137  // erroringConfigSelector always returns an error, with the xDS node ID included
   138  // in the error message. It is used to swap out the current config selector
   139  // when the LDS or RDS resource is not found on the management server.
   140  type erroringConfigSelector struct {
   141  	err error
   142  }
   143  
   144  func newErroringConfigSelector(xdsNodeID string) *erroringConfigSelector {
   145  	return &erroringConfigSelector{err: annotateErrorWithNodeID(status.Errorf(codes.Unavailable, "no valid clusters"), xdsNodeID)}
   146  }
   147  
   148  func (cs *erroringConfigSelector) SelectConfig(iresolver.RPCInfo) (*iresolver.RPCConfig, error) {
   149  	return nil, cs.err
   150  }
   151  func (cs *erroringConfigSelector) stop() {}
   152  
   153  type configSelector struct {
   154  	r                *xdsResolver
   155  	xdsNodeID        string
   156  	virtualHost      virtualHost
   157  	routes           []route
   158  	clusters         map[string]*clusterInfo
   159  	httpFilterConfig []xdsresource.HTTPFilter
   160  }
   161  
   162  var errNoMatchedRouteFound = status.Errorf(codes.Unavailable, "no matched route was found")
   163  var errUnsupportedClientRouteAction = status.Errorf(codes.Unavailable, "matched route does not have a supported route action type")
   164  
   165  // annotateErrorWithNodeID annotates the given error with the provided xDS node
   166  // ID. This is used by the real config selector when it runs into errors, and
   167  // also by the erroring config selector.
   168  func annotateErrorWithNodeID(err error, nodeID string) error {
   169  	return fmt.Errorf("[xDS node id: %s]: %w", nodeID, err)
   170  }
   171  
   172  func (cs *configSelector) SelectConfig(rpcInfo iresolver.RPCInfo) (*iresolver.RPCConfig, error) {
   173  	var rt *route
   174  	// Loop through routes in order and select first match.
   175  	for _, r := range cs.routes {
   176  		if r.m.Match(rpcInfo) {
   177  			rt = &r
   178  			break
   179  		}
   180  	}
   181  
   182  	if rt == nil || rt.clusters == nil {
   183  		return nil, annotateErrorWithNodeID(errNoMatchedRouteFound, cs.xdsNodeID)
   184  	}
   185  
   186  	if rt.actionType != xdsresource.RouteActionRoute {
   187  		return nil, annotateErrorWithNodeID(errUnsupportedClientRouteAction, cs.xdsNodeID)
   188  	}
   189  
   190  	cluster, ok := rt.clusters.Next().(*routeCluster)
   191  	if !ok {
   192  		return nil, annotateErrorWithNodeID(status.Errorf(codes.Internal, "error retrieving cluster for match: %v (%T)", cluster, cluster), cs.xdsNodeID)
   193  	}
   194  
   195  	// Add a ref to the selected cluster, as this RPC needs this cluster until
   196  	// it is committed.
   197  	ref := &cs.clusters[cluster.name].refCount
   198  	atomic.AddInt32(ref, 1)
   199  
   200  	interceptor, err := cs.newInterceptor(rt, cluster)
   201  	if err != nil {
   202  		return nil, annotateErrorWithNodeID(err, cs.xdsNodeID)
   203  	}
   204  
   205  	lbCtx := clustermanager.SetPickedCluster(rpcInfo.Context, cluster.name)
   206  	lbCtx = ringhash.SetXDSRequestHash(lbCtx, cs.generateHash(rpcInfo, rt.hashPolicies))
   207  
   208  	config := &iresolver.RPCConfig{
   209  		// Communicate to the LB policy the chosen cluster and request hash, if Ring Hash LB policy.
   210  		Context: lbCtx,
   211  		OnCommitted: func() {
   212  			// When the RPC is committed, the cluster is no longer required.
   213  			// Decrease its ref.
   214  			if v := atomic.AddInt32(ref, -1); v == 0 {
   215  				// This entry will be removed from activeClusters when
   216  				// producing the service config for the empty update.
   217  				cs.r.serializer.TrySchedule(func(context.Context) {
   218  					cs.r.onClusterRefDownToZero()
   219  				})
   220  			}
   221  		},
   222  		Interceptor: interceptor,
   223  	}
   224  
   225  	if rt.maxStreamDuration != 0 {
   226  		config.MethodConfig.Timeout = &rt.maxStreamDuration
   227  	}
   228  	if rt.retryConfig != nil {
   229  		config.MethodConfig.RetryPolicy = retryConfigToPolicy(rt.retryConfig)
   230  	} else if cs.virtualHost.retryConfig != nil {
   231  		config.MethodConfig.RetryPolicy = retryConfigToPolicy(cs.virtualHost.retryConfig)
   232  	}
   233  
   234  	return config, nil
   235  }
   236  
   237  func retryConfigToPolicy(config *xdsresource.RetryConfig) *serviceconfig.RetryPolicy {
   238  	return &serviceconfig.RetryPolicy{
   239  		MaxAttempts:          int(config.NumRetries) + 1,
   240  		InitialBackoff:       config.RetryBackoff.BaseInterval,
   241  		MaxBackoff:           config.RetryBackoff.MaxInterval,
   242  		BackoffMultiplier:    2,
   243  		RetryableStatusCodes: config.RetryOn,
   244  	}
   245  }
   246  
   247  func (cs *configSelector) generateHash(rpcInfo iresolver.RPCInfo, hashPolicies []*xdsresource.HashPolicy) uint64 {
   248  	var hash uint64
   249  	var generatedHash bool
   250  	var md, emd metadata.MD
   251  	var mdRead bool
   252  	for _, policy := range hashPolicies {
   253  		var policyHash uint64
   254  		var generatedPolicyHash bool
   255  		switch policy.HashPolicyType {
   256  		case xdsresource.HashPolicyTypeHeader:
   257  			if strings.HasSuffix(policy.HeaderName, "-bin") {
   258  				continue
   259  			}
   260  			if !mdRead {
   261  				md, _ = metadata.FromOutgoingContext(rpcInfo.Context)
   262  				emd, _ = grpcutil.ExtraMetadata(rpcInfo.Context)
   263  				mdRead = true
   264  			}
   265  			values := emd.Get(policy.HeaderName)
   266  			if len(values) == 0 {
   267  				// Extra metadata (e.g. the "content-type" header) takes
   268  				// precedence over the user's metadata.
   269  				values = md.Get(policy.HeaderName)
   270  				if len(values) == 0 {
   271  					// If the header isn't present at all, this policy is a no-op.
   272  					continue
   273  				}
   274  			}
   275  			joinedValues := strings.Join(values, ",")
   276  			if policy.Regex != nil {
   277  				joinedValues = policy.Regex.ReplaceAllString(joinedValues, policy.RegexSubstitution)
   278  			}
   279  			policyHash = xxhash.Sum64String(joinedValues)
   280  			generatedHash = true
   281  			generatedPolicyHash = true
   282  		case xdsresource.HashPolicyTypeChannelID:
   283  			// Use the static channel ID as the hash for this policy.
   284  			policyHash = cs.r.channelID
   285  			generatedHash = true
   286  			generatedPolicyHash = true
   287  		}
   288  
   289  		// Deterministically combine the hash policies. Rotating prevents
   290  		// duplicate hash policies from cancelling each other out and preserves
   291  		// the 64 bits of entropy.
   292  		if generatedPolicyHash {
   293  			hash = bits.RotateLeft64(hash, 1)
   294  			hash = hash ^ policyHash
   295  		}
   296  
   297  		// If terminal policy and a hash has already been generated, ignore the
   298  		// rest of the policies and use that hash already generated.
   299  		if policy.Terminal && generatedHash {
   300  			break
   301  		}
   302  	}
   303  
   304  	if generatedHash {
   305  		return hash
   306  	}
   307  	// If no generated hash return a random long. In the grand scheme of things
   308  	// this logically will map to choosing a random backend to route request to.
   309  	return rand.Uint64()
   310  }
   311  
   312  func (cs *configSelector) newInterceptor(rt *route, cluster *routeCluster) (iresolver.ClientInterceptor, error) {
   313  	if len(cs.httpFilterConfig) == 0 {
   314  		return nil, nil
   315  	}
   316  	interceptors := make([]iresolver.ClientInterceptor, 0, len(cs.httpFilterConfig))
   317  	for _, filter := range cs.httpFilterConfig {
   318  		override := cluster.httpFilterConfigOverride[filter.Name] // cluster is highest priority
   319  		if override == nil {
   320  			override = rt.httpFilterConfigOverride[filter.Name] // route is second priority
   321  		}
   322  		if override == nil {
   323  			override = cs.virtualHost.httpFilterConfigOverride[filter.Name] // VH is third & lowest priority
   324  		}
   325  		ib, ok := filter.Filter.(httpfilter.ClientInterceptorBuilder)
   326  		if !ok {
   327  			// Should not happen if it passed xdsClient validation.
   328  			return nil, fmt.Errorf("filter does not support use in client")
   329  		}
   330  		i, err := ib.BuildClientInterceptor(filter.Config, override)
   331  		if err != nil {
   332  			return nil, fmt.Errorf("error constructing filter: %v", err)
   333  		}
   334  		if i != nil {
   335  			interceptors = append(interceptors, i)
   336  		}
   337  	}
   338  	return &interceptorList{interceptors: interceptors}, nil
   339  }
   340  
   341  // stop decrements refs of all clusters referenced by this config selector.
   342  func (cs *configSelector) stop() {
   343  	// The resolver's old configSelector may be nil.  Handle that here.
   344  	if cs == nil {
   345  		return
   346  	}
   347  	// If any refs drop to zero, we'll need a service config update to delete
   348  	// the cluster.
   349  	needUpdate := false
   350  	// Loops over cs.clusters, but these are pointers to entries in
   351  	// activeClusters.
   352  	for _, ci := range cs.clusters {
   353  		if v := atomic.AddInt32(&ci.refCount, -1); v == 0 {
   354  			needUpdate = true
   355  		}
   356  	}
   357  	// We stop the old config selector immediately after sending a new config
   358  	// selector; we need another update to delete clusters from the config (if
   359  	// we don't have another update pending already).
   360  	if needUpdate {
   361  		cs.r.serializer.TrySchedule(func(context.Context) {
   362  			cs.r.onClusterRefDownToZero()
   363  		})
   364  	}
   365  }
   366  
   367  type interceptorList struct {
   368  	interceptors []iresolver.ClientInterceptor
   369  }
   370  
   371  func (il *interceptorList) NewStream(ctx context.Context, ri iresolver.RPCInfo, _ func(), newStream func(ctx context.Context, _ func()) (iresolver.ClientStream, error)) (iresolver.ClientStream, error) {
   372  	for i := len(il.interceptors) - 1; i >= 0; i-- {
   373  		ns := newStream
   374  		interceptor := il.interceptors[i]
   375  		newStream = func(ctx context.Context, done func()) (iresolver.ClientStream, error) {
   376  			return interceptor.NewStream(ctx, ri, done, ns)
   377  		}
   378  	}
   379  	return newStream(ctx, func() {})
   380  }