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 }