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 }