google.golang.org/grpc@v1.74.2/xds/internal/xdsclient/pool.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 19 package xdsclient 20 21 import ( 22 "fmt" 23 "sync" 24 "time" 25 26 v3statuspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3" 27 estats "google.golang.org/grpc/experimental/stats" 28 "google.golang.org/grpc/internal/envconfig" 29 istats "google.golang.org/grpc/internal/stats" 30 "google.golang.org/grpc/internal/xds/bootstrap" 31 "google.golang.org/protobuf/proto" 32 ) 33 34 var ( 35 // DefaultPool is the default pool for xDS clients. It is created at init 36 // time and reads bootstrap configuration from env vars to create the xDS 37 // client. 38 DefaultPool = &Pool{ 39 clients: make(map[string]*clientImpl), 40 getConfiguration: sync.OnceValues(bootstrap.GetConfiguration), 41 } 42 ) 43 44 // Pool represents a pool of xDS clients that share the same bootstrap 45 // configuration. 46 type Pool struct { 47 // Note that mu should ideally only have to guard clients. But here, we need 48 // it to guard config as well since SetFallbackBootstrapConfig writes to 49 // config. 50 mu sync.Mutex 51 clients map[string]*clientImpl 52 fallbackConfig *bootstrap.Config 53 // getConfiguration is a sync.OnceValues that attempts to read the bootstrap 54 // configuration from environment variables once. 55 getConfiguration func() (*bootstrap.Config, error) 56 } 57 58 // OptionsForTesting contains options to configure xDS client creation for 59 // testing purposes only. 60 type OptionsForTesting struct { 61 // Name is a unique name for this xDS client. 62 Name string 63 64 // WatchExpiryTimeout is the timeout for xDS resource watch expiry. If 65 // unspecified, uses the default value used in non-test code. 66 WatchExpiryTimeout time.Duration 67 68 // StreamBackoffAfterFailure is the backoff function used to determine the 69 // backoff duration after stream failures. 70 // If unspecified, uses the default value used in non-test code. 71 StreamBackoffAfterFailure func(int) time.Duration 72 73 // MetricsRecorder is the metrics recorder the xDS Client will use. If 74 // unspecified, uses a no-op MetricsRecorder. 75 MetricsRecorder estats.MetricsRecorder 76 } 77 78 // NewPool creates a new xDS client pool with the given bootstrap config. 79 // 80 // If a nil bootstrap config is passed and SetFallbackBootstrapConfig is not 81 // called before a call to NewClient, the latter will fail. i.e. if there is an 82 // attempt to create an xDS client from the pool without specifying bootstrap 83 // configuration (either at pool creation time or by setting the fallback 84 // bootstrap configuration), xDS client creation will fail. 85 func NewPool(config *bootstrap.Config) *Pool { 86 return &Pool{ 87 clients: make(map[string]*clientImpl), 88 getConfiguration: func() (*bootstrap.Config, error) { 89 return config, nil 90 }, 91 } 92 } 93 94 // NewClient returns an xDS client with the given name from the pool. If the 95 // client doesn't already exist, it creates a new xDS client and adds it to the 96 // pool. 97 // 98 // The second return value represents a close function which the caller is 99 // expected to invoke once they are done using the client. It is safe for the 100 // caller to invoke this close function multiple times. 101 func (p *Pool) NewClient(name string, metricsRecorder estats.MetricsRecorder) (XDSClient, func(), error) { 102 return p.newRefCounted(name, metricsRecorder) 103 } 104 105 // NewClientForTesting returns an xDS client configured with the provided 106 // options from the pool. If the client doesn't already exist, it creates a new 107 // xDS client and adds it to the pool. 108 // 109 // The second return value represents a close function which the caller is 110 // expected to invoke once they are done using the client. It is safe for the 111 // caller to invoke this close function multiple times. 112 // 113 // # Testing Only 114 // 115 // This function should ONLY be used for testing purposes. 116 func (p *Pool) NewClientForTesting(opts OptionsForTesting) (XDSClient, func(), error) { 117 if opts.Name == "" { 118 return nil, nil, fmt.Errorf("xds: opts.Name field must be non-empty") 119 } 120 if opts.WatchExpiryTimeout == 0 { 121 opts.WatchExpiryTimeout = defaultWatchExpiryTimeout 122 } 123 if opts.StreamBackoffAfterFailure == nil { 124 opts.StreamBackoffAfterFailure = defaultExponentialBackoff 125 } 126 if opts.MetricsRecorder == nil { 127 opts.MetricsRecorder = istats.NewMetricsRecorderList(nil) 128 } 129 c, cancel, err := p.newRefCounted(opts.Name, opts.MetricsRecorder) 130 if err != nil { 131 return nil, nil, err 132 } 133 c.SetWatchExpiryTimeoutForTesting(opts.WatchExpiryTimeout) 134 return c, cancel, nil 135 } 136 137 // GetClientForTesting returns an xDS client created earlier using the given 138 // name from the pool. If the client with the given name doesn't already exist, 139 // it returns an error. 140 // 141 // The second return value represents a close function which the caller is 142 // expected to invoke once they are done using the client. It is safe for the 143 // caller to invoke this close function multiple times. 144 // 145 // # Testing Only 146 // 147 // This function should ONLY be used for testing purposes. 148 func (p *Pool) GetClientForTesting(name string) (XDSClient, func(), error) { 149 p.mu.Lock() 150 defer p.mu.Unlock() 151 152 c, ok := p.clients[name] 153 if !ok { 154 return nil, nil, fmt.Errorf("xds:: xDS client with name %q not found", name) 155 } 156 c.incrRef() 157 return c, sync.OnceFunc(func() { p.clientRefCountedClose(name) }), nil 158 } 159 160 // SetFallbackBootstrapConfig is used to specify a bootstrap configuration 161 // that will be used as a fallback when the bootstrap environment variables 162 // are not defined. 163 func (p *Pool) SetFallbackBootstrapConfig(config *bootstrap.Config) { 164 p.mu.Lock() 165 defer p.mu.Unlock() 166 p.fallbackConfig = config 167 } 168 169 // DumpResources returns the status and contents of all xDS resources. 170 func (p *Pool) DumpResources() *v3statuspb.ClientStatusResponse { 171 p.mu.Lock() 172 defer p.mu.Unlock() 173 174 resp := &v3statuspb.ClientStatusResponse{} 175 for key, client := range p.clients { 176 b, err := client.DumpResources() 177 if err != nil { 178 return nil 179 } 180 r := &v3statuspb.ClientStatusResponse{} 181 if err := proto.Unmarshal(b, r); err != nil { 182 return nil 183 } 184 cfg := r.Config[0] 185 cfg.ClientScope = key 186 resp.Config = append(resp.Config, cfg) 187 } 188 return resp 189 } 190 191 // BootstrapConfigForTesting returns the bootstrap configuration used by the 192 // pool. The caller should not mutate the returned config. 193 // 194 // To be used only for testing purposes. 195 func (p *Pool) BootstrapConfigForTesting() *bootstrap.Config { 196 p.mu.Lock() 197 defer p.mu.Unlock() 198 cfg, _ := p.getConfiguration() 199 if cfg != nil { 200 return cfg 201 } 202 return p.fallbackConfig 203 } 204 205 // UnsetBootstrapConfigForTesting unsets the bootstrap configuration used by 206 // the pool. 207 // 208 // To be used only for testing purposes. 209 func (p *Pool) UnsetBootstrapConfigForTesting() { 210 p.mu.Lock() 211 defer p.mu.Unlock() 212 p.fallbackConfig = nil 213 p.getConfiguration = sync.OnceValues(bootstrap.GetConfiguration) 214 } 215 216 func (p *Pool) clientRefCountedClose(name string) { 217 p.mu.Lock() 218 client, ok := p.clients[name] 219 if !ok { 220 logger.Errorf("Attempt to close a non-existent xDS client with name %s", name) 221 p.mu.Unlock() 222 return 223 } 224 if client.decrRef() != 0 { 225 p.mu.Unlock() 226 return 227 } 228 delete(p.clients, name) 229 230 for _, s := range client.bootstrapConfig.XDSServers() { 231 for _, f := range s.Cleanups() { 232 f() 233 } 234 } 235 for _, a := range client.bootstrapConfig.Authorities() { 236 for _, s := range a.XDSServers { 237 for _, f := range s.Cleanups() { 238 f() 239 } 240 } 241 } 242 p.mu.Unlock() 243 244 // This attempts to close the transport to the management server and could 245 // theoretically call back into the xdsclient package again and deadlock. 246 // Hence, this needs to be called without holding the lock. 247 client.Close() 248 249 xdsClientImplCloseHook(name) 250 } 251 252 // newRefCounted creates a new reference counted xDS client implementation for 253 // name, if one does not exist already. If an xDS client for the given name 254 // exists, it gets a reference to it and returns it. 255 func (p *Pool) newRefCounted(name string, metricsRecorder estats.MetricsRecorder) (*clientImpl, func(), error) { 256 p.mu.Lock() 257 defer p.mu.Unlock() 258 259 config, err := p.getConfiguration() 260 if err != nil { 261 return nil, nil, fmt.Errorf("xds: failed to read xDS bootstrap config from env vars: %v", err) 262 } 263 264 if config == nil { 265 // If the environment variables are not set, then fallback bootstrap 266 // configuration should be set before attempting to create an xDS client, 267 // else xDS client creation will fail. 268 config = p.fallbackConfig 269 } 270 if config == nil { 271 return nil, nil, fmt.Errorf("failed to read xDS bootstrap config from env vars: bootstrap environment variables (%q or %q) not defined and fallback config not set", envconfig.XDSBootstrapFileNameEnv, envconfig.XDSBootstrapFileContentEnv) 272 } 273 274 if c := p.clients[name]; c != nil { 275 c.incrRef() 276 return c, sync.OnceFunc(func() { p.clientRefCountedClose(name) }), nil 277 } 278 279 c, err := newClientImpl(config, metricsRecorder, name) 280 if err != nil { 281 return nil, nil, err 282 } 283 if logger.V(2) { 284 c.logger.Infof("Created client with name %q and bootstrap configuration:\n %s", name, config) 285 } 286 p.clients[name] = c 287 xdsClientImplCreateHook(name) 288 289 logger.Infof("xDS node ID: %s", config.Node().GetId()) 290 return c, sync.OnceFunc(func() { p.clientRefCountedClose(name) }), nil 291 }