google.golang.org/grpc@v1.72.2/xds/internal/xdsclient/client_refcounted_test.go (about)

     1  /*
     2   *
     3   * Copyright 2022 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  	"context"
    23  	"sync"
    24  	"testing"
    25  
    26  	"github.com/google/uuid"
    27  	"google.golang.org/grpc/internal/testutils"
    28  	"google.golang.org/grpc/internal/testutils/stats"
    29  	"google.golang.org/grpc/internal/testutils/xds/e2e"
    30  	"google.golang.org/grpc/internal/xds/bootstrap"
    31  )
    32  
    33  // Tests that multiple calls to New() with the same name returns the same
    34  // client. Also verifies that only when all references to the newly created
    35  // client are released, the underlying client is closed.
    36  func (s) TestClientNew_Single(t *testing.T) {
    37  	// Create a bootstrap configuration, place it in a file in the temp
    38  	// directory, and set the bootstrap env vars to point to it.
    39  	nodeID := uuid.New().String()
    40  	contents := e2e.DefaultBootstrapContents(t, nodeID, "non-existent-server-address")
    41  	config, err := bootstrap.NewConfigFromContents(contents)
    42  	if err != nil {
    43  		t.Fatalf("Failed to parse bootstrap contents: %s, %v", contents, err)
    44  	}
    45  	pool := NewPool(config)
    46  
    47  	// Override the client creation hook to get notified.
    48  	origClientImplCreateHook := xdsClientImplCreateHook
    49  	clientImplCreateCh := testutils.NewChannel()
    50  	xdsClientImplCreateHook = func(name string) {
    51  		clientImplCreateCh.Replace(name)
    52  	}
    53  	defer func() { xdsClientImplCreateHook = origClientImplCreateHook }()
    54  
    55  	// Override the client close hook to get notified.
    56  	origClientImplCloseHook := xdsClientImplCloseHook
    57  	clientImplCloseCh := testutils.NewChannel()
    58  	xdsClientImplCloseHook = func(name string) {
    59  		clientImplCloseCh.Replace(name)
    60  	}
    61  	defer func() { xdsClientImplCloseHook = origClientImplCloseHook }()
    62  
    63  	// The first call to New() should create a new client.
    64  	_, closeFunc, err := pool.NewClient(t.Name(), &stats.NoopMetricsRecorder{})
    65  	if err != nil {
    66  		t.Fatalf("Failed to create xDS client: %v", err)
    67  	}
    68  
    69  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
    70  	defer cancel()
    71  	if _, err := clientImplCreateCh.Receive(ctx); err != nil {
    72  		t.Fatalf("Timeout when waiting for xDS client to be created: %v", err)
    73  	}
    74  
    75  	// Calling New() again should not create new client implementations.
    76  	const count = 9
    77  	closeFuncs := make([]func(), count)
    78  	for i := 0; i < count; i++ {
    79  		func() {
    80  			_, closeFuncs[i], err = pool.NewClient(t.Name(), &stats.NoopMetricsRecorder{})
    81  			if err != nil {
    82  				t.Fatalf("%d-th call to New() failed with error: %v", i, err)
    83  			}
    84  
    85  			sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
    86  			defer sCancel()
    87  			if _, err := clientImplCreateCh.Receive(sCtx); err == nil {
    88  				t.Fatalf("%d-th call to New() created a new client", i)
    89  			}
    90  		}()
    91  	}
    92  
    93  	// Call Close() multiple times on each of the clients created in the above
    94  	// for loop. Close() calls are idempotent, and the underlying client
    95  	// implementation will not be closed until we release the first reference we
    96  	// acquired above, via the first call to New().
    97  	for i := 0; i < count; i++ {
    98  		func() {
    99  			closeFuncs[i]()
   100  			closeFuncs[i]()
   101  
   102  			sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
   103  			defer sCancel()
   104  			if _, err := clientImplCloseCh.Receive(sCtx); err == nil {
   105  				t.Fatal("Client implementation closed before all references are released")
   106  			}
   107  		}()
   108  	}
   109  
   110  	// Call the last Close(). The underlying implementation should be closed.
   111  	closeFunc()
   112  	if _, err := clientImplCloseCh.Receive(ctx); err != nil {
   113  		t.Fatalf("Timeout waiting for client implementation to be closed: %v", err)
   114  	}
   115  
   116  	// Calling New() again, after the previous Client was actually closed,
   117  	// should create a new one.
   118  	_, closeFunc, err = pool.NewClient(t.Name(), &stats.NoopMetricsRecorder{})
   119  	if err != nil {
   120  		t.Fatalf("Failed to create xDS client: %v", err)
   121  	}
   122  	defer closeFunc()
   123  	if _, err := clientImplCreateCh.Receive(ctx); err != nil {
   124  		t.Fatalf("Timeout when waiting for xDS client to be created: %v", err)
   125  	}
   126  }
   127  
   128  // Tests the scenario where there are multiple calls to New() with different
   129  // names. Verifies that reference counts are tracked correctly for each client
   130  // and that only when all references are released for a client, it is closed.
   131  func (s) TestClientNew_Multiple(t *testing.T) {
   132  	// Create a bootstrap configuration, place it in a file in the temp
   133  	// directory, and set the bootstrap env vars to point to it.
   134  	nodeID := uuid.New().String()
   135  	contents := e2e.DefaultBootstrapContents(t, nodeID, "non-existent-server-address")
   136  	config, err := bootstrap.NewConfigFromContents(contents)
   137  	if err != nil {
   138  		t.Fatalf("Failed to parse bootstrap contents: %s, %v", contents, err)
   139  	}
   140  	pool := NewPool(config)
   141  
   142  	// Override the client creation hook to get notified.
   143  	origClientImplCreateHook := xdsClientImplCreateHook
   144  	clientImplCreateCh := testutils.NewChannel()
   145  	xdsClientImplCreateHook = func(name string) {
   146  		clientImplCreateCh.Replace(name)
   147  	}
   148  	defer func() { xdsClientImplCreateHook = origClientImplCreateHook }()
   149  
   150  	// Override the client close hook to get notified.
   151  	origClientImplCloseHook := xdsClientImplCloseHook
   152  	clientImplCloseCh := testutils.NewChannel()
   153  	xdsClientImplCloseHook = func(name string) {
   154  		clientImplCloseCh.Replace(name)
   155  	}
   156  	defer func() { xdsClientImplCloseHook = origClientImplCloseHook }()
   157  
   158  	// Create two xDS clients.
   159  	client1Name := t.Name() + "-1"
   160  	_, closeFunc1, err := pool.NewClient(client1Name, &stats.NoopMetricsRecorder{})
   161  	if err != nil {
   162  		t.Fatalf("Failed to create xDS client: %v", err)
   163  	}
   164  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   165  	defer cancel()
   166  	name, err := clientImplCreateCh.Receive(ctx)
   167  	if err != nil {
   168  		t.Fatalf("Timeout when waiting for xDS client to be created: %v", err)
   169  	}
   170  	if name.(string) != client1Name {
   171  		t.Fatalf("xDS client created for name %q, want %q", name.(string), client1Name)
   172  	}
   173  
   174  	client2Name := t.Name() + "-2"
   175  	_, closeFunc2, err := pool.NewClient(client2Name, &stats.NoopMetricsRecorder{})
   176  	if err != nil {
   177  		t.Fatalf("Failed to create xDS client: %v", err)
   178  	}
   179  	name, err = clientImplCreateCh.Receive(ctx)
   180  	if err != nil {
   181  		t.Fatalf("Timeout when waiting for xDS client to be created: %v", err)
   182  	}
   183  	if name.(string) != client2Name {
   184  		t.Fatalf("xDS client created for name %q, want %q", name.(string), client1Name)
   185  	}
   186  
   187  	// Create N more references to each of these clients.
   188  	const count = 9
   189  	closeFuncs1 := make([]func(), count)
   190  	closeFuncs2 := make([]func(), count)
   191  	var wg sync.WaitGroup
   192  	wg.Add(2)
   193  	go func() {
   194  		defer wg.Done()
   195  		for i := 0; i < count; i++ {
   196  			var err error
   197  			_, closeFuncs1[i], err = pool.NewClient(client1Name, &stats.NoopMetricsRecorder{})
   198  			if err != nil {
   199  				t.Errorf("%d-th call to New() failed with error: %v", i, err)
   200  			}
   201  		}
   202  	}()
   203  	go func() {
   204  		defer wg.Done()
   205  		for i := 0; i < count; i++ {
   206  			var err error
   207  			_, closeFuncs2[i], err = pool.NewClient(client2Name, &stats.NoopMetricsRecorder{})
   208  			if err != nil {
   209  				t.Errorf("%d-th call to New() failed with error: %v", i, err)
   210  			}
   211  		}
   212  	}()
   213  	wg.Wait()
   214  	if t.Failed() {
   215  		t.FailNow()
   216  	}
   217  
   218  	// Ensure that none of the create hooks are called.
   219  	sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
   220  	defer sCancel()
   221  	if _, err := clientImplCreateCh.Receive(sCtx); err == nil {
   222  		t.Fatalf("New xDS client created when expected to reuse an existing one")
   223  	}
   224  
   225  	// The close function returned by New() is idempotent and calling it
   226  	// multiple times should not decrement the reference count multiple times.
   227  	for i := 0; i < count; i++ {
   228  		closeFuncs1[i]()
   229  		closeFuncs1[i]()
   230  	}
   231  	sCtx, sCancel = context.WithTimeout(ctx, defaultTestShortTimeout)
   232  	defer sCancel()
   233  	if _, err := clientImplCloseCh.Receive(sCtx); err == nil {
   234  		t.Fatal("Client implementation closed before all references are released")
   235  	}
   236  
   237  	// Release the last reference and verify that the client is closed
   238  	// completely.
   239  	closeFunc1()
   240  	name, err = clientImplCloseCh.Receive(ctx)
   241  	if err != nil {
   242  		t.Fatal("Timeout when waiting for xDS client to be closed completely")
   243  	}
   244  	if name.(string) != client1Name {
   245  		t.Fatalf("xDS client closed for name %q, want %q", name.(string), client1Name)
   246  	}
   247  
   248  	// Ensure that the close hook is not called for the second client.
   249  	sCtx, sCancel = context.WithTimeout(ctx, defaultTestShortTimeout)
   250  	defer sCancel()
   251  	if _, err := clientImplCloseCh.Receive(sCtx); err == nil {
   252  		t.Fatal("Client implementation closed before all references are released")
   253  	}
   254  
   255  	// The close function returned by New() is idempotent and calling it
   256  	// multiple times should not decrement the reference count multiple times.
   257  	for i := 0; i < count; i++ {
   258  		closeFuncs2[i]()
   259  		closeFuncs2[i]()
   260  	}
   261  	sCtx, sCancel = context.WithTimeout(ctx, defaultTestShortTimeout)
   262  	defer sCancel()
   263  	if _, err := clientImplCloseCh.Receive(sCtx); err == nil {
   264  		t.Fatal("Client implementation closed before all references are released")
   265  	}
   266  
   267  	// Release the last reference and verify that the client is closed
   268  	// completely.
   269  	closeFunc2()
   270  	name, err = clientImplCloseCh.Receive(ctx)
   271  	if err != nil {
   272  		t.Fatal("Timeout when waiting for xDS client to be closed completely")
   273  	}
   274  	if name.(string) != client2Name {
   275  		t.Fatalf("xDS client closed for name %q, want %q", name.(string), client2Name)
   276  	}
   277  }