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  }