dubbo.apache.org/dubbo-go/v3@v3.1.1/remoting/xds/client.go (about) 1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. 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 xds 19 20 import ( 21 "errors" 22 "sync" 23 "time" 24 ) 25 26 import ( 27 "github.com/dubbogo/gost/log/logger" 28 29 perrors "github.com/pkg/errors" 30 ) 31 32 import ( 33 "dubbo.apache.org/dubbo-go/v3/common/constant" 34 "dubbo.apache.org/dubbo-go/v3/protocol" 35 "dubbo.apache.org/dubbo-go/v3/registry" 36 xdsCommon "dubbo.apache.org/dubbo-go/v3/remoting/xds/common" 37 "dubbo.apache.org/dubbo-go/v3/remoting/xds/ewatcher" 38 "dubbo.apache.org/dubbo-go/v3/remoting/xds/mapping" 39 "dubbo.apache.org/dubbo-go/v3/xds/client" 40 "dubbo.apache.org/dubbo-go/v3/xds/client/resource" 41 "dubbo.apache.org/dubbo-go/v3/xds/utils/resolver" 42 ) 43 44 const ( 45 // todo make istiodTokenPath configurable 46 defaultIstiodTokenPath = "/var/run/secrets/token/istio-token" 47 defaultIstiodDebugPort = "8080" 48 gRPCUserAgentName = "gRPC Go" 49 clientFeatureNoOverprovisioning = "envoy.lb.does_not_support_overprovisioning" 50 ) 51 52 // xdsWrappedClient should only init once 53 var xdsWrappedClient *WrappedClientImpl 54 55 type WrappedClientImpl struct { 56 /* 57 local info 58 */ 59 podName string 60 namespace string 61 62 /* 63 localIP is to find local pod's cluster and hostAddr by cds and eds 64 */ 65 localIP string 66 67 /* 68 hostAddr is local pod's cluster and hostAddr, like dubbo-go-app.default.svc.cluster.local:20000 69 */ 70 hostAddr xdsCommon.HostAddr 71 72 /* 73 istiod info 74 istiodAddr is istio $(istioSeviceFullName):$(xds-grpc-port) like istiod.istio-system.svc.cluster.local:15010 75 istiodPodIP is to call istiod unexposed debug port 8080 76 */ 77 istiodAddr xdsCommon.HostAddr 78 istiodPodIP string 79 80 /* 81 grpc xdsClient sdk 82 */ 83 xdsClient client.XDSClient 84 85 /* 86 interfaceMapHandler manages dubbogo metadata containing service key -> hostAddr map 87 */ 88 interfaceMapHandler mapping.InterfaceMapHandler 89 90 /* 91 rdsMap cache router config 92 mesh router would read config from it 93 */ 94 rdsMap map[string]resource.RouteConfigUpdate 95 rdsMapLock sync.RWMutex 96 97 /* 98 cdsMap cache full clusterID -> clusterUpdate map of this istiod 99 */ 100 cdsMap map[string]resource.ClusterUpdate 101 cdsMapLock sync.RWMutex 102 103 /* 104 cdsUpdateEventChan transfer cds update event from xdsClient 105 if update event got, we will refresh cds watcher, stopping endPointWatcherCtx related to deleted cluster, and starting 106 to watch new-coming cluster with endPointWatcherCtx 107 108 cdsUpdateEventHandlers stores handlers to recv refresh event, refresh event is only a call without param, 109 after the calling event, we can read cdsMap to get latest and full cluster info, and handle the difference. 110 */ 111 cdsUpdateEventChan chan struct{} 112 cdsUpdateEventHandlers []func() 113 cdsUpdateEventHandlersLock sync.RWMutex 114 115 /* 116 hostAddrListenerMap[hostAddr][serviceUniqueKey] -> registry.NotifyListener 117 stores all directory listener, which receives events and refresh invokers 118 */ 119 hostAddrListenerMap map[string]map[string]registry.NotifyListener 120 hostAddrListenerMapLock sync.RWMutex 121 122 /* 123 hostAddrClusterCtxMap[hostAddr][clusterName] -> endPointWatcherCtx 124 */ 125 hostAddrClusterCtxMap map[string]map[string]ewatcher.EWatcher 126 hostAddrClusterCtxMapLock sync.RWMutex 127 128 /* 129 subscribeStopChMap stores subscription stop chan 130 */ 131 subscribeStopChMap sync.Map 132 133 /* 134 xdsSniffingTimeout stores xds sniffing timeout duration 135 */ 136 xdsSniffingTimeout time.Duration 137 } 138 139 func GetXDSWrappedClient() *WrappedClientImpl { 140 return xdsWrappedClient 141 } 142 143 // NewXDSWrappedClient create or get singleton xdsWrappedClient 144 func NewXDSWrappedClient(config Config) (XDSWrapperClient, error) { 145 // todo @(laurence) safety problem? what if to concurrent 'new' both create new client? 146 if xdsWrappedClient != nil { 147 return xdsWrappedClient, nil 148 } 149 if config.SniffingTimeout == 0 { 150 config.SniffingTimeout, _ = time.ParseDuration(constant.DefaultRegTimeout) 151 } 152 if config.DebugPort == "" { 153 config.DebugPort = "8080" 154 } 155 156 // write param 157 newClient := &WrappedClientImpl{ 158 podName: config.PodName, 159 namespace: config.Namespace, 160 localIP: config.LocalIP, 161 istiodAddr: config.IstioAddr, 162 163 rdsMap: make(map[string]resource.RouteConfigUpdate), 164 cdsMap: make(map[string]resource.ClusterUpdate), 165 166 hostAddrListenerMap: make(map[string]map[string]registry.NotifyListener), 167 hostAddrClusterCtxMap: make(map[string]map[string]ewatcher.EWatcher), 168 169 cdsUpdateEventChan: make(chan struct{}), 170 cdsUpdateEventHandlers: make([]func(), 0), 171 172 xdsSniffingTimeout: config.SniffingTimeout, 173 } 174 175 // 1. init xdsclient 176 if err := newClient.initXDSClient(); err != nil { 177 return nil, err 178 } 179 // 2. watching cds update event 180 // todo @(laurence) gr control 181 go newClient.runWatchingCdsUpdateEvent() 182 183 // 3. load basic info from istiod and start listening cds 184 if err := newClient.startWatchingAllClusterAndLoadLocalHostAddrAndIstioPodIP(config.LocalDebugMode); err != nil { 185 return nil, err 186 } 187 188 // 4. init interface map handler 189 newClient.interfaceMapHandler = mapping.NewInterfaceMapHandlerImpl( 190 newClient.xdsClient, 191 defaultIstiodTokenPath, 192 xdsCommon.NewHostNameOrIPAddr(newClient.istiodPodIP+":"+config.DebugPort), 193 newClient.hostAddr, config.LocalDebugMode) 194 195 xdsWrappedClient = newClient 196 return newClient, nil 197 } 198 199 // GetHostAddrByServiceUniqueKey todo 1. timeout 2. hostAddr change? 200 func (w *WrappedClientImpl) GetHostAddrByServiceUniqueKey(serviceUniqueKey string) (string, error) { 201 return w.interfaceMapHandler.GetHostAddrMap(serviceUniqueKey) 202 } 203 204 // GetDubboGoMetadata get all registered metadata of dubbogo 205 func (w *WrappedClientImpl) GetDubboGoMetadata() (map[string]string, error) { 206 return w.interfaceMapHandler.GetDubboGoMetadata() 207 } 208 209 // ChangeInterfaceMap change the map of serviceUniqueKey -> appname, if add is true, register, else unregister 210 func (w *WrappedClientImpl) ChangeInterfaceMap(serviceUniqueKey string, add bool) error { 211 if add { 212 return w.interfaceMapHandler.Register(serviceUniqueKey) 213 } 214 return w.interfaceMapHandler.UnRegister(serviceUniqueKey) 215 } 216 217 func (w *WrappedClientImpl) GetRouterConfig(hostAddr string) resource.RouteConfigUpdate { 218 w.rdsMapLock.RLock() 219 defer w.rdsMapLock.RUnlock() 220 routeConfig, ok := w.rdsMap[hostAddr] 221 if ok { 222 return routeConfig 223 } 224 return resource.RouteConfigUpdate{} 225 } 226 227 func (w *WrappedClientImpl) GetClusterUpdateIgnoreVersion(hostAddr string) resource.ClusterUpdate { 228 addr := xdsCommon.NewHostNameOrIPAddr(hostAddr) 229 w.cdsMapLock.RLock() 230 defer w.cdsMapLock.Unlock() 231 for clusterName, v := range w.cdsMap { 232 cluster := xdsCommon.NewCluster(clusterName) 233 if cluster.Addr.Port == addr.Port && cluster.Addr.HostnameOrIP == addr.HostnameOrIP { 234 return v 235 } 236 } 237 return resource.ClusterUpdate{} 238 } 239 240 func (w *WrappedClientImpl) Subscribe(svcUniqueName, interfaceName, hostAddr string, lst registry.NotifyListener) error { 241 _, ok := w.subscribeStopChMap.Load(svcUniqueName) 242 if ok { 243 return perrors.Errorf("XDS WrappedClientImpl subscribe interface %s failed, subscription already exist.", interfaceName) 244 } 245 stopCh := make(chan struct{}) 246 w.subscribeStopChMap.Store(svcUniqueName, stopCh) 247 w.registerHostLevelSubscription(hostAddr, interfaceName, svcUniqueName, lst) 248 <-stopCh 249 w.unregisterHostLevelSubscription(hostAddr, svcUniqueName) 250 return nil 251 } 252 253 func (w *WrappedClientImpl) UnSubscribe(svcUniqueName string) { 254 if stopCh, ok := w.subscribeStopChMap.Load(svcUniqueName); ok { 255 close(stopCh.(chan struct{})) 256 } 257 w.subscribeStopChMap.Delete(svcUniqueName) 258 } 259 260 func (w *WrappedClientImpl) GetHostAddress() xdsCommon.HostAddr { 261 return w.hostAddr 262 } 263 264 func (w *WrappedClientImpl) GetIstioPodIP() string { 265 return w.istiodPodIP 266 } 267 268 // registerHostLevelSubscription register: 1. all related cluster, 2. router config 269 func (w *WrappedClientImpl) registerHostLevelSubscription(hostAddr, interfaceName, svcUniqueName string, lst registry.NotifyListener) { 270 // 1. listen all cluster related endpoint 271 w.hostAddrListenerMapLock.Lock() 272 if _, ok := w.hostAddrListenerMap[hostAddr]; ok { 273 // if subscription exist, register listener directly 274 w.hostAddrListenerMap[hostAddr][svcUniqueName] = lst 275 w.hostAddrListenerMapLock.Unlock() 276 return 277 } 278 // host HostAddr key must not exist in map, create one 279 w.hostAddrListenerMap[hostAddr] = make(map[string]registry.NotifyListener) 280 281 w.hostAddrClusterCtxMapLock.Lock() 282 w.hostAddrClusterCtxMap[hostAddr] = make(map[string]ewatcher.EWatcher) 283 w.hostAddrClusterCtxMapLock.Unlock() 284 285 w.hostAddrListenerMap[hostAddr][svcUniqueName] = lst 286 w.hostAddrListenerMapLock.Unlock() 287 288 // watch cluster change, and start listening newcoming cluster 289 w.cdsUpdateEventHandlersLock.Lock() 290 w.cdsUpdateEventHandlers = append(w.cdsUpdateEventHandlers, func() { 291 // todo @(laurnece) now this event would be called if any cluster is change, but not only this hostAddr's 292 updatedAllVersionedClusterName := w.getAllVersionClusterName(hostAddr) 293 // do patch 294 w.hostAddrClusterCtxMapLock.RLock() 295 listeningClustersCancelMap := w.hostAddrClusterCtxMap[hostAddr] 296 w.hostAddrClusterCtxMapLock.RUnlock() 297 298 oldlisteningClusterMap := make(map[string]bool) 299 for cluster := range listeningClustersCancelMap { 300 oldlisteningClusterMap[cluster] = false 301 } 302 for _, updatedClusterName := range updatedAllVersionedClusterName { 303 if _, ok := listeningClustersCancelMap[updatedClusterName]; ok { 304 // already listening 305 oldlisteningClusterMap[updatedClusterName] = true 306 continue 307 } 308 // new cluster 309 watcher := ewatcher.NewEndpointWatcherCtxImpl( 310 updatedClusterName, hostAddr, interfaceName, &w.hostAddrListenerMapLock, w.hostAddrListenerMap) 311 cancel := w.xdsClient.WatchEndpoints(updatedClusterName, watcher.Handle) 312 watcher.SetCancelFunction(cancel) 313 w.hostAddrClusterCtxMapLock.Lock() 314 w.hostAddrClusterCtxMap[hostAddr][updatedClusterName] = watcher 315 w.hostAddrClusterCtxMapLock.Unlock() 316 } 317 318 // cancel not exist cluster 319 for cluster, v := range oldlisteningClusterMap { 320 if !v { 321 // this cluster not exist in update cluster list 322 w.hostAddrClusterCtxMapLock.Lock() 323 if watchCtx, ok := w.hostAddrClusterCtxMap[hostAddr][cluster]; ok { 324 delete(w.hostAddrClusterCtxMap[hostAddr], cluster) 325 watchCtx.Destroy() 326 } 327 w.hostAddrClusterCtxMapLock.Unlock() 328 } 329 } 330 }) 331 w.cdsUpdateEventHandlersLock.Unlock() 332 333 // update cluster of now 334 allVersionedClusterName := w.getAllVersionClusterName(hostAddr) 335 for _, c := range allVersionedClusterName { 336 watcher := ewatcher.NewEndpointWatcherCtxImpl( 337 c, hostAddr, interfaceName, &w.hostAddrListenerMapLock, w.hostAddrListenerMap) 338 watcher.SetCancelFunction(w.xdsClient.WatchEndpoints(c, watcher.Handle)) 339 340 w.hostAddrClusterCtxMapLock.Lock() 341 w.hostAddrClusterCtxMap[hostAddr][c] = watcher 342 w.hostAddrClusterCtxMapLock.Unlock() 343 } 344 345 // 2. cache route config 346 // todo @(laurnece) cancel watching of this addr's rds 347 _ = w.xdsClient.WatchRouteConfig(hostAddr, func(update resource.RouteConfigUpdate, err error) { 348 if update.VirtualHosts == nil { 349 return 350 } 351 w.rdsMapLock.Lock() 352 defer w.rdsMapLock.Unlock() 353 w.rdsMap[hostAddr] = update 354 }) 355 } 356 357 func (w *WrappedClientImpl) unregisterHostLevelSubscription(hostAddr, svcUniqueName string) { 358 w.hostAddrListenerMapLock.Lock() 359 defer w.hostAddrListenerMapLock.Unlock() 360 if _, ok := w.hostAddrListenerMap[hostAddr]; ok { 361 // if subscription exist, register listener directly 362 if _, exist := w.hostAddrListenerMap[hostAddr][svcUniqueName]; exist { 363 delete(w.hostAddrListenerMap[hostAddr], svcUniqueName) 364 } 365 if (len(w.hostAddrListenerMap[hostAddr])) == 0 { 366 // if no subscription of this host cancel all cds subscription of this hostAddr 367 keys := make([]string, 0) 368 w.hostAddrClusterCtxMapLock.Lock() 369 for k, c := range w.hostAddrClusterCtxMap[hostAddr] { 370 c.Destroy() 371 keys = append(keys, k) 372 } 373 for _, v := range keys { 374 delete(w.hostAddrClusterCtxMap, v) 375 } 376 w.hostAddrClusterCtxMapLock.Unlock() 377 } 378 } 379 } 380 381 func (w *WrappedClientImpl) initXDSClient() error { 382 xdsClient, err := xdsClientFactoryFunction(w.localIP, w.podName, w.namespace, w.istiodAddr) 383 if err != nil { 384 return err 385 } 386 w.xdsClient = xdsClient 387 return nil 388 } 389 390 // startWatchingAllClusterAndLoadLocalHostAddrAndIstioPodIP is blocking function 391 // 1. start watching all cluster by cds 392 // 2. discovery local pod's hostAddr by cds and eds 393 // 3. discovery istiod pod ip by cds and eds 394 func (w *WrappedClientImpl) startWatchingAllClusterAndLoadLocalHostAddrAndIstioPodIP(localDebugMode bool) error { 395 // call watch and refresh istiod debug interface 396 foundLocalStopCh := make(chan struct{}) 397 foundIstiodStopCh := make(chan struct{}) 398 discoveryFinishedStopCh := make(chan struct{}) 399 // todo timeout configure 400 timeoutCh := time.After(w.xdsSniffingTimeout) 401 foundLocal := false 402 foundIstiod := false 403 var cancel1 func() 404 var cancel2 func() 405 logger.Infof("[XDS Wrapped Client] Start sniffing with istio hostname = %s, localIp = %s", 406 w.istiodAddr.HostnameOrIP, w.localIP) 407 408 // todo @(laurence) here, if istiod is unhealthy, here should be timeout and tell developer. 409 _ = w.xdsClient.WatchCluster("*", func(update resource.ClusterUpdate, err error) { 410 if update.ClusterName == "" { 411 return 412 } 413 if update.ClusterName[:1] == constant.MeshDeleteClusterPrefix { 414 // delete event 415 w.cdsMapLock.Lock() 416 defer w.cdsMapLock.Unlock() 417 delete(w.cdsMap, update.ClusterName[1:]) 418 logger.Infof("[XDS Wrapped Client] Delete cluster %s", update.ClusterName) 419 w.cdsUpdateEventChan <- struct{}{} // send update event 420 return 421 } 422 w.cdsMapLock.Lock() 423 w.cdsMap[update.ClusterName] = update 424 w.cdsMapLock.Unlock() 425 426 w.cdsUpdateEventChan <- struct{}{} // send update event 427 if foundLocal && foundIstiod { 428 return 429 } 430 logger.Infof("[XDS Wrapped Client] Sniffing with cluster name = %s", update.ClusterName) 431 // only into here during start sniffing istiod/service prcedure 432 cluster := xdsCommon.NewCluster(update.ClusterName) 433 if cluster.Addr.HostnameOrIP == w.istiodAddr.HostnameOrIP { 434 // 1. find istiod podIP 435 // todo: When would eds level watch be canceled? 436 logger.Info("[XDS Wrapped Client] Sniffing get istiod cluster") 437 cancel1 = w.xdsClient.WatchEndpoints(update.ClusterName, func(endpoint resource.EndpointsUpdate, err error) { 438 if foundIstiod { 439 return 440 } 441 logger.Infof("[XDS Wrapped Client] Sniffing get istiod endpoint = %+v, localities = %+v", endpoint, endpoint.Localities) 442 for _, v := range endpoint.Localities { 443 for _, e := range v.Endpoints { 444 w.istiodPodIP = xdsCommon.NewHostNameOrIPAddr(e.Address).HostnameOrIP 445 logger.Infof("[XDS Wrapped Client] Sniffing found istiod podIP = %s", w.istiodPodIP) 446 foundIstiod = true 447 close(foundIstiodStopCh) 448 } 449 } 450 }) 451 return 452 } 453 // 2. found local hostAddr 454 // todo: When would eds level watch be canceled? 455 cancel2 = w.xdsClient.WatchEndpoints(update.ClusterName, func(endpoint resource.EndpointsUpdate, err error) { 456 if foundLocal { 457 return 458 } 459 for _, v := range endpoint.Localities { 460 for _, e := range v.Endpoints { 461 logger.Infof("[XDS Wrapped Client] Sniffing Found eds endpoint = %+v", e) 462 if xdsCommon.NewHostNameOrIPAddr(e.Address).HostnameOrIP == w.localIP { 463 cluster := xdsCommon.NewCluster(update.ClusterName) 464 w.hostAddr = cluster.Addr 465 foundLocal = true 466 close(foundLocalStopCh) 467 } 468 } 469 } 470 }) 471 }) 472 473 if localDebugMode { 474 go func() { 475 <-foundIstiodStopCh 476 <-foundLocalStopCh 477 cancel1() 478 cancel2() 479 }() 480 return nil 481 } 482 483 go func() { 484 <-foundIstiodStopCh 485 <-foundLocalStopCh 486 close(discoveryFinishedStopCh) 487 }() 488 489 select { 490 case <-discoveryFinishedStopCh: 491 // discovery success 492 // waiting for cancel function to have value 493 time.Sleep(time.Second) 494 cancel1() 495 cancel2() 496 logger.Infof("[XDS Wrapper Client] Sniffing Finished with host addr = %s, istiod pod ip = %s", w.hostAddr, w.istiodPodIP) 497 return nil 498 case <-timeoutCh: 499 logger.Warnf("[XDS Wrapper Client] Sniffing timeout with duration = %v", w.xdsSniffingTimeout) 500 if cancel1 != nil { 501 cancel1() 502 } 503 if cancel2 != nil { 504 cancel2() 505 } 506 select { 507 case <-foundIstiodStopCh: 508 return DiscoverLocalError 509 default: 510 return DiscoverIstiodPodIpError 511 } 512 } 513 } 514 515 // runWatchingCdsUpdateEvent is blocking function, starts to read event from cdsUpdateEventChan and call cdsUpdateEventHandlers 516 func (w *WrappedClientImpl) runWatchingCdsUpdateEvent() { 517 for range w.cdsUpdateEventChan { 518 w.cdsUpdateEventHandlersLock.RLock() 519 for _, h := range w.cdsUpdateEventHandlers { 520 h() 521 } 522 w.cdsUpdateEventHandlersLock.RUnlock() 523 } 524 } 525 526 // getAllVersionClusterName get all clusterID that is the subset of given hostAddr from cache: cdsMap 527 // like: if given hostAddr is 'outbound|20000||dubbo-go-app.default.svc.cluster.local', and result would be 528 // ['outbound|20000|v1|dubbo-go-app.default.svc.cluster.local', 529 // 'outbound|20000||dubbo-go-app.default.svc.cluster.local', 530 // 'outbound|20000|v2|dubbo-go-app.default.svc.cluster.local'] 531 func (w *WrappedClientImpl) getAllVersionClusterName(hostAddr string) []string { 532 addr := xdsCommon.NewHostNameOrIPAddr(hostAddr) 533 allVersionClusterNames := make([]string, 0) 534 w.cdsMapLock.RLock() 535 defer w.cdsMapLock.RUnlock() 536 for clusterName, _ := range w.cdsMap { 537 cluster := xdsCommon.NewCluster(clusterName) 538 if cluster.Addr.Port == addr.Port && cluster.Addr.HostnameOrIP == addr.HostnameOrIP { 539 allVersionClusterNames = append(allVersionClusterNames, clusterName) 540 } 541 } 542 return allVersionClusterNames 543 } 544 545 func (w *WrappedClientImpl) MatchRoute(routerConfig resource.RouteConfigUpdate, invocation protocol.Invocation) (*resource.Route, error) { 546 ctx := invocation.GetAttachmentAsContext() 547 rpcInfo := resolver.RPCInfo{ 548 Context: ctx, 549 Method: "/" + invocation.MethodName(), 550 } 551 // try to route to sub virtual host 552 for _, vh := range routerConfig.VirtualHosts { 553 for _, r := range vh.Routes { 554 //route. 555 matcher, err := resource.RouteToMatcher(r) 556 if err != nil { 557 return nil, err 558 } 559 if matcher.Match(rpcInfo) { 560 return r, nil 561 } 562 } 563 } 564 return nil, errors.New("not found route") 565 } 566 567 type XDSWrapperClient interface { 568 Subscribe(svcUniqueName, interfaceName, hostAddr string, lst registry.NotifyListener) error 569 UnSubscribe(svcUniqueName string) 570 GetRouterConfig(hostAddr string) resource.RouteConfigUpdate 571 GetHostAddrByServiceUniqueKey(serviceUniqueKey string) (string, error) 572 GetDubboGoMetadata() (map[string]string, error) 573 ChangeInterfaceMap(serviceUniqueKey string, add bool) error 574 GetClusterUpdateIgnoreVersion(hostAddr string) resource.ClusterUpdate 575 GetHostAddress() xdsCommon.HostAddr 576 GetIstioPodIP() string 577 MatchRoute(routerConfig resource.RouteConfigUpdate, invocation protocol.Invocation) (*resource.Route, error) 578 }