google.golang.org/grpc@v1.62.1/xds/internal/xdsclient/transport/transport.go (about) 1 /* 2 * 3 * Copyright 2022 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 transport implements the xDS transport protocol functionality 19 // required by the xdsclient. 20 package transport 21 22 import ( 23 "context" 24 "errors" 25 "fmt" 26 "sync" 27 "time" 28 29 "google.golang.org/grpc" 30 "google.golang.org/grpc/codes" 31 "google.golang.org/grpc/connectivity" 32 "google.golang.org/grpc/internal/backoff" 33 "google.golang.org/grpc/internal/buffer" 34 "google.golang.org/grpc/internal/grpclog" 35 "google.golang.org/grpc/internal/pretty" 36 "google.golang.org/grpc/keepalive" 37 "google.golang.org/grpc/xds/internal/xdsclient/bootstrap" 38 "google.golang.org/grpc/xds/internal/xdsclient/load" 39 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource" 40 "google.golang.org/protobuf/types/known/anypb" 41 42 v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 43 v3adsgrpc "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 44 v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 45 statuspb "google.golang.org/genproto/googleapis/rpc/status" 46 ) 47 48 // Any per-RPC level logs which print complete request or response messages 49 // should be gated at this verbosity level. Other per-RPC level logs which print 50 // terse output should be at `INFO` and verbosity 2, which corresponds to using 51 // the `Debugf` method on the logger. 52 const perRPCVerbosityLevel = 9 53 54 type adsStream = v3adsgrpc.AggregatedDiscoveryService_StreamAggregatedResourcesClient 55 56 // Transport provides a resource-type agnostic implementation of the xDS 57 // transport protocol. At this layer, resource contents are supposed to be 58 // opaque blobs which should be be meaningful only to the xDS data model layer 59 // which is implemented by the `xdsresource` package. 60 // 61 // Under the hood, it owns the gRPC connection to a single management server and 62 // manages the lifecycle of ADS/LRS streams. It uses the xDS v3 transport 63 // protocol version. 64 type Transport struct { 65 // These fields are initialized at creation time and are read-only afterwards. 66 cc *grpc.ClientConn // ClientConn to the mangement server. 67 serverURI string // URI of the management server. 68 onRecvHandler OnRecvHandlerFunc // Resource update handler. xDS data model layer. 69 onErrorHandler func(error) // To report underlying stream errors. 70 onSendHandler OnSendHandlerFunc // To report resources requested on ADS stream. 71 lrsStore *load.Store // Store returned to user for pushing loads. 72 backoff func(int) time.Duration // Backoff after stream failures. 73 nodeProto *v3corepb.Node // Identifies the gRPC application. 74 logger *grpclog.PrefixLogger // Prefix logger for transport logs. 75 adsRunnerCancel context.CancelFunc // CancelFunc for the ADS goroutine. 76 adsRunnerDoneCh chan struct{} // To notify exit of ADS goroutine. 77 lrsRunnerDoneCh chan struct{} // To notify exit of LRS goroutine. 78 79 // These channels enable synchronization amongst the different goroutines 80 // spawned by the transport, and between asynchorous events resulting from 81 // receipt of responses from the management server. 82 adsStreamCh chan adsStream // New ADS streams are pushed here. 83 adsRequestCh *buffer.Unbounded // Resource and ack requests are pushed here. 84 85 // mu guards the following runtime state maintained by the transport. 86 mu sync.Mutex 87 // resources is map from resource type URL to the set of resource names 88 // being requested for that type. When the ADS stream is restarted, the 89 // transport requests all these resources again from the management server. 90 resources map[string]map[string]bool 91 // versions is a map from resource type URL to the most recently ACKed 92 // version for that resource. Resource versions are a property of the 93 // resource type and not the stream, and will not be reset upon stream 94 // restarts. 95 versions map[string]string 96 // nonces is a map from resource type URL to the most recently received 97 // nonce for that resource type. Nonces are a property of the ADS stream and 98 // will be reset upon stream restarts. 99 nonces map[string]string 100 101 lrsMu sync.Mutex // Protects all LRS state. 102 lrsCancelStream context.CancelFunc // CancelFunc for the LRS stream. 103 lrsRefCount int // Reference count on the load store. 104 } 105 106 // OnRecvHandlerFunc is the implementation at the xDS data model layer, which 107 // determines if the configuration received from the management server can be 108 // applied locally or not. 109 // 110 // A nil error is returned from this function when the data model layer believes 111 // that the received configuration is good and can be applied locally. This will 112 // cause the transport layer to send an ACK to the management server. A non-nil 113 // error is returned from this function when the data model layer believes 114 // otherwise, and this will cause the transport layer to send a NACK. 115 type OnRecvHandlerFunc func(update ResourceUpdate) error 116 117 // OnSendHandlerFunc is the implementation at the authority, which handles state 118 // changes for the resource watch and stop watch timers accordingly. 119 type OnSendHandlerFunc func(update *ResourceSendInfo) 120 121 // ResourceUpdate is a representation of the configuration update received from 122 // the management server. It only contains fields which are useful to the data 123 // model layer, and layers above it. 124 type ResourceUpdate struct { 125 // Resources is the list of resources received from the management server. 126 Resources []*anypb.Any 127 // URL is the resource type URL for the above resources. 128 URL string 129 // Version is the resource version, for the above resources, as specified by 130 // the management server. 131 Version string 132 } 133 134 // Options specifies configuration knobs used when creating a new Transport. 135 type Options struct { 136 // ServerCfg contains all the configuration required to connect to the xDS 137 // management server. 138 ServerCfg bootstrap.ServerConfig 139 // OnRecvHandler is the component which makes ACK/NACK decisions based on 140 // the received resources. 141 // 142 // Invoked inline and implementations must not block. 143 OnRecvHandler OnRecvHandlerFunc 144 // OnErrorHandler provides a way for the transport layer to report 145 // underlying stream errors. These can be bubbled all the way up to the user 146 // of the xdsClient. 147 // 148 // Invoked inline and implementations must not block. 149 OnErrorHandler func(error) 150 // OnSendHandler provides a way for the transport layer to report underlying 151 // resource requests sent on the stream. However, Send() on the ADS stream will 152 // return successfully as long as: 153 // 1. there is enough flow control quota to send the message. 154 // 2. the message is added to the send buffer. 155 // However, the connection may fail after the callback is invoked and before 156 // the message is actually sent on the wire. This is accepted. 157 // 158 // Invoked inline and implementations must not block. 159 OnSendHandler func(*ResourceSendInfo) 160 // Backoff controls the amount of time to backoff before recreating failed 161 // ADS streams. If unspecified, a default exponential backoff implementation 162 // is used. For more details, see: 163 // https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md. 164 Backoff func(retries int) time.Duration 165 // Logger does logging with a prefix. 166 Logger *grpclog.PrefixLogger 167 // NodeProto contains the Node proto to be used in xDS requests. This will be 168 // of type *v3corepb.Node. 169 NodeProto *v3corepb.Node 170 } 171 172 // For overriding in unit tests. 173 var grpcDial = grpc.Dial 174 175 // New creates a new Transport. 176 func New(opts Options) (*Transport, error) { 177 switch { 178 case opts.ServerCfg.ServerURI == "": 179 return nil, errors.New("missing server URI when creating a new transport") 180 case opts.ServerCfg.CredsDialOption() == nil: 181 return nil, errors.New("missing credentials when creating a new transport") 182 case opts.OnRecvHandler == nil: 183 return nil, errors.New("missing OnRecv callback handler when creating a new transport") 184 case opts.OnErrorHandler == nil: 185 return nil, errors.New("missing OnError callback handler when creating a new transport") 186 case opts.OnSendHandler == nil: 187 return nil, errors.New("missing OnSend callback handler when creating a new transport") 188 } 189 190 // Dial the xDS management with the passed in credentials. 191 dopts := []grpc.DialOption{ 192 opts.ServerCfg.CredsDialOption(), 193 grpc.WithKeepaliveParams(keepalive.ClientParameters{ 194 // We decided to use these sane defaults in all languages, and 195 // kicked the can down the road as far making these configurable. 196 Time: 5 * time.Minute, 197 Timeout: 20 * time.Second, 198 }), 199 } 200 cc, err := grpcDial(opts.ServerCfg.ServerURI, dopts...) 201 if err != nil { 202 // An error from a non-blocking dial indicates something serious. 203 return nil, fmt.Errorf("failed to create a transport to the management server %q: %v", opts.ServerCfg.ServerURI, err) 204 } 205 206 boff := opts.Backoff 207 if boff == nil { 208 boff = backoff.DefaultExponential.Backoff 209 } 210 ret := &Transport{ 211 cc: cc, 212 serverURI: opts.ServerCfg.ServerURI, 213 onRecvHandler: opts.OnRecvHandler, 214 onErrorHandler: opts.OnErrorHandler, 215 onSendHandler: opts.OnSendHandler, 216 lrsStore: load.NewStore(), 217 backoff: boff, 218 nodeProto: opts.NodeProto, 219 logger: opts.Logger, 220 221 adsStreamCh: make(chan adsStream, 1), 222 adsRequestCh: buffer.NewUnbounded(), 223 resources: make(map[string]map[string]bool), 224 versions: make(map[string]string), 225 nonces: make(map[string]string), 226 adsRunnerDoneCh: make(chan struct{}), 227 } 228 229 // This context is used for sending and receiving RPC requests and 230 // responses. It is also used by all the goroutines spawned by this 231 // Transport. Therefore, cancelling this context when the transport is 232 // closed will essentially cancel any pending RPCs, and cause the goroutines 233 // to terminate. 234 ctx, cancel := context.WithCancel(context.Background()) 235 ret.adsRunnerCancel = cancel 236 go ret.adsRunner(ctx) 237 238 ret.logger.Infof("Created transport to server %q", ret.serverURI) 239 return ret, nil 240 } 241 242 // resourceRequest wraps the resource type url and the resource names requested 243 // by the user of this transport. 244 type resourceRequest struct { 245 resources []string 246 url string 247 } 248 249 // SendRequest sends out an ADS request for the provided resources of the 250 // specified resource type. 251 // 252 // The request is sent out asynchronously. If no valid stream exists at the time 253 // of processing this request, it is queued and will be sent out once a valid 254 // stream exists. 255 // 256 // If a successful response is received, the update handler callback provided at 257 // creation time is invoked. If an error is encountered, the stream error 258 // handler callback provided at creation time is invoked. 259 func (t *Transport) SendRequest(url string, resources []string) { 260 t.adsRequestCh.Put(&resourceRequest{ 261 url: url, 262 resources: resources, 263 }) 264 } 265 266 func (t *Transport) newAggregatedDiscoveryServiceStream(ctx context.Context, cc *grpc.ClientConn) (adsStream, error) { 267 // The transport retries the stream with an exponential backoff whenever the 268 // stream breaks without ever having seen a response. 269 return v3adsgrpc.NewAggregatedDiscoveryServiceClient(cc).StreamAggregatedResources(ctx) 270 } 271 272 // ResourceSendInfo wraps the names and url of resources sent to the management 273 // server. This is used by the `authority` type to start/stop the watch timer 274 // associated with every resource in the update. 275 type ResourceSendInfo struct { 276 ResourceNames []string 277 URL string 278 } 279 280 func (t *Transport) sendAggregatedDiscoveryServiceRequest(stream adsStream, sendNodeProto bool, resourceNames []string, resourceURL, version, nonce string, nackErr error) error { 281 req := &v3discoverypb.DiscoveryRequest{ 282 TypeUrl: resourceURL, 283 ResourceNames: resourceNames, 284 VersionInfo: version, 285 ResponseNonce: nonce, 286 } 287 if sendNodeProto { 288 req.Node = t.nodeProto 289 } 290 if nackErr != nil { 291 req.ErrorDetail = &statuspb.Status{ 292 Code: int32(codes.InvalidArgument), Message: nackErr.Error(), 293 } 294 } 295 if err := stream.Send(req); err != nil { 296 return err 297 } 298 if t.logger.V(perRPCVerbosityLevel) { 299 t.logger.Infof("ADS request sent: %v", pretty.ToJSON(req)) 300 } else { 301 t.logger.Debugf("ADS request sent for type %q, resources: %v, version %q, nonce %q", resourceURL, resourceNames, version, nonce) 302 } 303 t.onSendHandler(&ResourceSendInfo{URL: resourceURL, ResourceNames: resourceNames}) 304 return nil 305 } 306 307 func (t *Transport) recvAggregatedDiscoveryServiceResponse(stream adsStream) (resources []*anypb.Any, resourceURL, version, nonce string, err error) { 308 resp, err := stream.Recv() 309 if err != nil { 310 return nil, "", "", "", err 311 } 312 if t.logger.V(perRPCVerbosityLevel) { 313 t.logger.Infof("ADS response received: %v", pretty.ToJSON(resp)) 314 } else { 315 t.logger.Debugf("ADS response received for type %q, version %q, nonce %q", resp.GetTypeUrl(), resp.GetVersionInfo(), resp.GetNonce()) 316 } 317 return resp.GetResources(), resp.GetTypeUrl(), resp.GetVersionInfo(), resp.GetNonce(), nil 318 } 319 320 // adsRunner starts an ADS stream (and backs off exponentially, if the previous 321 // stream failed without receiving a single reply) and runs the sender and 322 // receiver routines to send and receive data from the stream respectively. 323 func (t *Transport) adsRunner(ctx context.Context) { 324 defer close(t.adsRunnerDoneCh) 325 326 go t.send(ctx) 327 328 // We reset backoff state when we successfully receive at least one 329 // message from the server. 330 runStreamWithBackoff := func() error { 331 stream, err := t.newAggregatedDiscoveryServiceStream(ctx, t.cc) 332 if err != nil { 333 t.onErrorHandler(err) 334 t.logger.Warningf("Creating new ADS stream failed: %v", err) 335 return nil 336 } 337 t.logger.Infof("ADS stream created") 338 339 select { 340 case <-t.adsStreamCh: 341 default: 342 } 343 t.adsStreamCh <- stream 344 msgReceived := t.recv(stream) 345 if msgReceived { 346 return backoff.ErrResetBackoff 347 } 348 return nil 349 } 350 backoff.RunF(ctx, runStreamWithBackoff, t.backoff) 351 } 352 353 // send is a separate goroutine for sending resource requests on the ADS stream. 354 // 355 // For every new stream received on the stream channel, all existing resources 356 // are re-requested from the management server. 357 // 358 // For every new resource request received on the resources channel, the 359 // resources map is updated (this ensures that resend will pick them up when 360 // there are new streams) and the appropriate request is sent out. 361 func (t *Transport) send(ctx context.Context) { 362 var stream adsStream 363 // The xDS protocol only requires that we send the node proto in the first 364 // discovery request on every stream. Sending the node proto in every 365 // request message wastes CPU resources on the client and the server. 366 sendNodeProto := true 367 for { 368 select { 369 case <-ctx.Done(): 370 return 371 case stream = <-t.adsStreamCh: 372 // We have a new stream and we've to ensure that the node proto gets 373 // sent out in the first request on the stream. At this point, we 374 // might not have any registered watches. Setting this field to true 375 // here will ensure that the node proto gets sent out along with the 376 // discovery request when the first watch is registered. 377 if len(t.resources) == 0 { 378 sendNodeProto = true 379 continue 380 } 381 382 if !t.sendExisting(stream) { 383 // Send failed, clear the current stream. Attempt to resend will 384 // only be made after a new stream is created. 385 stream = nil 386 continue 387 } 388 sendNodeProto = false 389 case u, ok := <-t.adsRequestCh.Get(): 390 if !ok { 391 // No requests will be sent after the adsRequestCh buffer is closed. 392 return 393 } 394 t.adsRequestCh.Load() 395 396 var ( 397 resources []string 398 url, version, nonce string 399 send bool 400 nackErr error 401 ) 402 switch update := u.(type) { 403 case *resourceRequest: 404 resources, url, version, nonce = t.processResourceRequest(update) 405 case *ackRequest: 406 resources, url, version, nonce, send = t.processAckRequest(update, stream) 407 if !send { 408 continue 409 } 410 nackErr = update.nackErr 411 } 412 if stream == nil { 413 // There's no stream yet. Skip the request. This request 414 // will be resent to the new streams. If no stream is 415 // created, the watcher will timeout (same as server not 416 // sending response back). 417 continue 418 } 419 if err := t.sendAggregatedDiscoveryServiceRequest(stream, sendNodeProto, resources, url, version, nonce, nackErr); err != nil { 420 t.logger.Warningf("Sending ADS request for resources: %q, url: %q, version: %q, nonce: %q failed: %v", resources, url, version, nonce, err) 421 // Send failed, clear the current stream. 422 stream = nil 423 } 424 sendNodeProto = false 425 } 426 } 427 } 428 429 // sendExisting sends out xDS requests for existing resources when recovering 430 // from a broken stream. 431 // 432 // We call stream.Send() here with the lock being held. It should be OK to do 433 // that here because the stream has just started and Send() usually returns 434 // quickly (once it pushes the message onto the transport layer) and is only 435 // ever blocked if we don't have enough flow control quota. 436 func (t *Transport) sendExisting(stream adsStream) bool { 437 t.mu.Lock() 438 defer t.mu.Unlock() 439 440 // Reset only the nonces map when the stream restarts. 441 // 442 // xDS spec says the following. See section: 443 // https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol#ack-nack-and-resource-type-instance-version 444 // 445 // Note that the version for a resource type is not a property of an 446 // individual xDS stream but rather a property of the resources themselves. If 447 // the stream becomes broken and the client creates a new stream, the client’s 448 // initial request on the new stream should indicate the most recent version 449 // seen by the client on the previous stream 450 t.nonces = make(map[string]string) 451 452 // Send node proto only in the first request on the stream. 453 sendNodeProto := true 454 for url, resources := range t.resources { 455 if err := t.sendAggregatedDiscoveryServiceRequest(stream, sendNodeProto, mapToSlice(resources), url, t.versions[url], "", nil); err != nil { 456 t.logger.Warningf("Sending ADS request for resources: %q, url: %q, version: %q, nonce: %q failed: %v", resources, url, t.versions[url], "", err) 457 return false 458 } 459 sendNodeProto = false 460 } 461 462 return true 463 } 464 465 // recv receives xDS responses on the provided ADS stream and branches out to 466 // message specific handlers. Returns true if at least one message was 467 // successfully received. 468 func (t *Transport) recv(stream adsStream) bool { 469 msgReceived := false 470 for { 471 resources, url, rVersion, nonce, err := t.recvAggregatedDiscoveryServiceResponse(stream) 472 if err != nil { 473 // Note that we do not consider it an error if the ADS stream was closed 474 // after having received a response on the stream. This is because there 475 // are legitimate reasons why the server may need to close the stream during 476 // normal operations, such as needing to rebalance load or the underlying 477 // connection hitting its max connection age limit. 478 // (see [gRFC A9](https://github.com/grpc/proposal/blob/master/A9-server-side-conn-mgt.md)). 479 if msgReceived { 480 err = xdsresource.NewErrorf(xdsresource.ErrTypeStreamFailedAfterRecv, err.Error()) 481 } 482 t.onErrorHandler(err) 483 t.logger.Warningf("ADS stream closed: %v", err) 484 return msgReceived 485 } 486 msgReceived = true 487 488 err = t.onRecvHandler(ResourceUpdate{ 489 Resources: resources, 490 URL: url, 491 Version: rVersion, 492 }) 493 if xdsresource.ErrType(err) == xdsresource.ErrorTypeResourceTypeUnsupported { 494 t.logger.Warningf("%v", err) 495 continue 496 } 497 // If the data model layer returned an error, we need to NACK the 498 // response in which case we need to set the version to the most 499 // recently accepted version of this resource type. 500 if err != nil { 501 t.mu.Lock() 502 t.adsRequestCh.Put(&ackRequest{ 503 url: url, 504 nonce: nonce, 505 stream: stream, 506 version: t.versions[url], 507 nackErr: err, 508 }) 509 t.mu.Unlock() 510 t.logger.Warningf("Sending NACK for resource type: %q, version: %q, nonce: %q, reason: %v", url, rVersion, nonce, err) 511 continue 512 } 513 t.adsRequestCh.Put(&ackRequest{ 514 url: url, 515 nonce: nonce, 516 stream: stream, 517 version: rVersion, 518 }) 519 t.logger.Debugf("Sending ACK for resource type: %q, version: %q, nonce: %q", url, rVersion, nonce) 520 } 521 } 522 523 func mapToSlice(m map[string]bool) []string { 524 ret := make([]string, 0, len(m)) 525 for i := range m { 526 ret = append(ret, i) 527 } 528 return ret 529 } 530 531 func sliceToMap(ss []string) map[string]bool { 532 ret := make(map[string]bool, len(ss)) 533 for _, s := range ss { 534 ret[s] = true 535 } 536 return ret 537 } 538 539 // processResourceRequest pulls the fields needed to send out an ADS request. 540 // The resource type and the list of resources to request are provided by the 541 // user, while the version and nonce are maintained internally. 542 // 543 // The resources map, which keeps track of the resources being requested, is 544 // updated here. Any subsequent stream failure will re-request resources stored 545 // in this map. 546 // 547 // Returns the list of resources, resource type url, version and nonce. 548 func (t *Transport) processResourceRequest(req *resourceRequest) ([]string, string, string, string) { 549 t.mu.Lock() 550 defer t.mu.Unlock() 551 552 resources := sliceToMap(req.resources) 553 t.resources[req.url] = resources 554 return req.resources, req.url, t.versions[req.url], t.nonces[req.url] 555 } 556 557 type ackRequest struct { 558 url string // Resource type URL. 559 version string // NACK if version is an empty string. 560 nonce string 561 nackErr error // nil for ACK, non-nil for NACK. 562 // ACK/NACK are tagged with the stream it's for. When the stream is down, 563 // all the ACK/NACK for this stream will be dropped, and the version/nonce 564 // won't be updated. 565 stream grpc.ClientStream 566 } 567 568 // processAckRequest pulls the fields needed to send out an ADS ACK. The nonces 569 // and versions map is updated. 570 // 571 // Returns the list of resources, resource type url, version, nonce, and an 572 // indication of whether an ACK should be sent on the wire or not. 573 func (t *Transport) processAckRequest(ack *ackRequest, stream grpc.ClientStream) ([]string, string, string, string, bool) { 574 if ack.stream != stream { 575 // If ACK's stream isn't the current sending stream, this means the ACK 576 // was pushed to queue before the old stream broke, and a new stream has 577 // been started since. Return immediately here so we don't update the 578 // nonce for the new stream. 579 return nil, "", "", "", false 580 } 581 582 t.mu.Lock() 583 defer t.mu.Unlock() 584 585 // Update the nonce irrespective of whether we send the ACK request on wire. 586 // An up-to-date nonce is required for the next request. 587 nonce := ack.nonce 588 t.nonces[ack.url] = nonce 589 590 s, ok := t.resources[ack.url] 591 if !ok || len(s) == 0 { 592 // We don't send the ACK request if there are no resources of this type 593 // in our resources map. This can be either when the server sends 594 // responses before any request, or the resources are removed while the 595 // ackRequest was in queue). If we send a request with an empty 596 // resource name list, the server may treat it as a wild card and send 597 // us everything. 598 return nil, "", "", "", false 599 } 600 resources := mapToSlice(s) 601 602 // Update the versions map only when we plan to send an ACK. 603 if ack.nackErr == nil { 604 t.versions[ack.url] = ack.version 605 } 606 607 return resources, ack.url, ack.version, nonce, true 608 } 609 610 // Close closes the Transport and frees any associated resources. 611 func (t *Transport) Close() { 612 t.adsRunnerCancel() 613 <-t.adsRunnerDoneCh 614 t.adsRequestCh.Close() 615 t.cc.Close() 616 } 617 618 // ChannelConnectivityStateForTesting returns the connectivity state of the gRPC 619 // channel to the management server. 620 // 621 // Only for testing purposes. 622 func (t *Transport) ChannelConnectivityStateForTesting() connectivity.State { 623 return t.cc.GetState() 624 }