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