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  }