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 }