istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/xds/discovery_test.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package xds
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"reflect"
    21  	"sync"
    22  	"sync/atomic"
    23  	"testing"
    24  	"time"
    25  
    26  	discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    27  	uatomic "go.uber.org/atomic"
    28  	"google.golang.org/grpc"
    29  
    30  	"istio.io/istio/pilot/pkg/model"
    31  	"istio.io/istio/pkg/config/schema/kind"
    32  	"istio.io/istio/pkg/test/util/retry"
    33  	"istio.io/istio/pkg/util/sets"
    34  )
    35  
    36  func createProxies(n int) []*Connection {
    37  	proxies := make([]*Connection, 0, n)
    38  	for p := 0; p < n; p++ {
    39  		conn := newConnection("", &fakeStream{})
    40  		conn.SetID(fmt.Sprintf("proxy-%v", p))
    41  		proxies = append(proxies, conn)
    42  	}
    43  	return proxies
    44  }
    45  
    46  func wgDoneOrTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
    47  	c := make(chan struct{})
    48  	go func() {
    49  		wg.Wait()
    50  		c <- struct{}{}
    51  	}()
    52  	select {
    53  	case <-c:
    54  		return true
    55  	case <-time.After(timeout):
    56  		return false
    57  	}
    58  }
    59  
    60  func TestSendPushesManyPushes(t *testing.T) {
    61  	stopCh := make(chan struct{})
    62  	defer close(stopCh)
    63  
    64  	semaphore := make(chan struct{}, 2)
    65  	queue := NewPushQueue()
    66  	defer queue.ShutDown()
    67  
    68  	proxies := createProxies(5)
    69  
    70  	pushes := make(map[string]int)
    71  	pushesMu := &sync.Mutex{}
    72  
    73  	for _, proxy := range proxies {
    74  		proxy := proxy
    75  		// Start receive thread
    76  		go func() {
    77  			for {
    78  				select {
    79  				case ev := <-proxy.PushCh():
    80  					p := ev.(*Event)
    81  					p.done()
    82  					pushesMu.Lock()
    83  					pushes[proxy.ID()]++
    84  					pushesMu.Unlock()
    85  				case <-stopCh:
    86  					return
    87  				}
    88  			}
    89  		}()
    90  	}
    91  	go doSendPushes(stopCh, semaphore, queue)
    92  
    93  	for push := 0; push < 100; push++ {
    94  		for _, proxy := range proxies {
    95  			queue.Enqueue(proxy, &model.PushRequest{Push: &model.PushContext{}})
    96  		}
    97  		time.Sleep(time.Millisecond * 10)
    98  	}
    99  	for queue.Pending() > 0 {
   100  		time.Sleep(time.Millisecond)
   101  	}
   102  	pushesMu.Lock()
   103  	defer pushesMu.Unlock()
   104  	for proxy, numPushes := range pushes {
   105  		if numPushes == 0 {
   106  			t.Fatalf("Proxy %v had 0 pushes", proxy)
   107  		}
   108  	}
   109  }
   110  
   111  func TestSendPushesSinglePush(t *testing.T) {
   112  	stopCh := make(chan struct{})
   113  	defer close(stopCh)
   114  
   115  	semaphore := make(chan struct{}, 2)
   116  	queue := NewPushQueue()
   117  	defer queue.ShutDown()
   118  
   119  	proxies := createProxies(5)
   120  
   121  	wg := &sync.WaitGroup{}
   122  	wg.Add(5)
   123  
   124  	pushes := make(map[string]int)
   125  	pushesMu := &sync.Mutex{}
   126  
   127  	for _, proxy := range proxies {
   128  		proxy := proxy
   129  		// Start receive thread
   130  		go func() {
   131  			for {
   132  				select {
   133  				case ev := <-proxy.PushCh():
   134  					p := ev.(*Event)
   135  					p.done()
   136  					pushesMu.Lock()
   137  					pushes[proxy.ID()]++
   138  					pushesMu.Unlock()
   139  					wg.Done()
   140  				case <-stopCh:
   141  					return
   142  				}
   143  			}
   144  		}()
   145  	}
   146  	go doSendPushes(stopCh, semaphore, queue)
   147  
   148  	for _, proxy := range proxies {
   149  		queue.Enqueue(proxy, &model.PushRequest{Push: &model.PushContext{}})
   150  	}
   151  
   152  	if !wgDoneOrTimeout(wg, time.Second) {
   153  		t.Fatalf("Expected 5 pushes but got %v", len(pushes))
   154  	}
   155  	expected := map[string]int{
   156  		"proxy-0": 1,
   157  		"proxy-1": 1,
   158  		"proxy-2": 1,
   159  		"proxy-3": 1,
   160  		"proxy-4": 1,
   161  	}
   162  	if !reflect.DeepEqual(expected, pushes) {
   163  		t.Fatalf("Expected pushes %+v, got %+v", expected, pushes)
   164  	}
   165  }
   166  
   167  type fakeStream struct {
   168  	grpc.ServerStream
   169  }
   170  
   171  func (h *fakeStream) Send(*discovery.DiscoveryResponse) error {
   172  	return nil
   173  }
   174  
   175  func (h *fakeStream) Recv() (*discovery.DiscoveryRequest, error) {
   176  	return nil, nil
   177  }
   178  
   179  func (h *fakeStream) Context() context.Context {
   180  	return context.Background()
   181  }
   182  
   183  func TestDebounce(t *testing.T) {
   184  	// This test tests the timeout and debouncing of config updates
   185  	// If it is flaking, DebounceAfter may need to be increased, or the code refactored to mock time.
   186  	// For now, this seems to work well
   187  	opts := DebounceOptions{
   188  		DebounceAfter:     time.Millisecond * 50,
   189  		debounceMax:       time.Millisecond * 100,
   190  		enableEDSDebounce: false,
   191  	}
   192  
   193  	tests := []struct {
   194  		name string
   195  		test func(updateCh chan *model.PushRequest, expect func(partial, full int32))
   196  	}{
   197  		{
   198  			name: "Should not debounce partial pushes",
   199  			test: func(updateCh chan *model.PushRequest, expect func(partial, full int32)) {
   200  				updateCh <- &model.PushRequest{Full: false}
   201  				expect(1, 0)
   202  				updateCh <- &model.PushRequest{Full: false}
   203  				expect(2, 0)
   204  				updateCh <- &model.PushRequest{Full: false}
   205  				expect(3, 0)
   206  				updateCh <- &model.PushRequest{Full: false}
   207  				expect(4, 0)
   208  				updateCh <- &model.PushRequest{Full: false}
   209  				expect(5, 0)
   210  			},
   211  		},
   212  		{
   213  			name: "Should debounce full pushes",
   214  			test: func(updateCh chan *model.PushRequest, expect func(partial, full int32)) {
   215  				updateCh <- &model.PushRequest{Full: true}
   216  				expect(0, 0)
   217  			},
   218  		},
   219  		{
   220  			name: "Should send full updates in batches",
   221  			test: func(updateCh chan *model.PushRequest, expect func(partial, full int32)) {
   222  				updateCh <- &model.PushRequest{Full: true}
   223  				updateCh <- &model.PushRequest{Full: true}
   224  				expect(0, 1)
   225  			},
   226  		},
   227  		{
   228  			name: "Should send full updates in batches, partial updates immediately",
   229  			test: func(updateCh chan *model.PushRequest, expect func(partial, full int32)) {
   230  				updateCh <- &model.PushRequest{Full: true}
   231  				updateCh <- &model.PushRequest{Full: true}
   232  				updateCh <- &model.PushRequest{Full: false}
   233  				updateCh <- &model.PushRequest{Full: false}
   234  				expect(2, 1)
   235  				updateCh <- &model.PushRequest{Full: false}
   236  				expect(3, 1)
   237  			},
   238  		},
   239  		{
   240  			name: "Should force a push after DebounceMax",
   241  			test: func(updateCh chan *model.PushRequest, expect func(partial, full int32)) {
   242  				// Send many requests within debounce window
   243  				updateCh <- &model.PushRequest{Full: true}
   244  				time.Sleep(opts.DebounceAfter / 2)
   245  				updateCh <- &model.PushRequest{Full: true}
   246  				time.Sleep(opts.DebounceAfter / 2)
   247  				updateCh <- &model.PushRequest{Full: true}
   248  				time.Sleep(opts.DebounceAfter / 2)
   249  				updateCh <- &model.PushRequest{Full: true}
   250  				time.Sleep(opts.DebounceAfter / 2)
   251  				expect(0, 1)
   252  			},
   253  		},
   254  		{
   255  			name: "Should push synchronously after debounce",
   256  			test: func(updateCh chan *model.PushRequest, expect func(partial, full int32)) {
   257  				updateCh <- &model.PushRequest{Full: true}
   258  				time.Sleep(opts.DebounceAfter + 10*time.Millisecond)
   259  				updateCh <- &model.PushRequest{Full: true}
   260  				expect(0, 2)
   261  			},
   262  		},
   263  	}
   264  
   265  	for _, tt := range tests {
   266  		t.Run(tt.name, func(t *testing.T) {
   267  			stopCh := make(chan struct{})
   268  			updateCh := make(chan *model.PushRequest)
   269  			pushingCh := make(chan struct{}, 1)
   270  			errCh := make(chan error, 1)
   271  
   272  			var partialPushes int32
   273  			var fullPushes int32
   274  
   275  			wg := sync.WaitGroup{}
   276  
   277  			fakePush := func(req *model.PushRequest) {
   278  				if req.Full {
   279  					select {
   280  					case pushingCh <- struct{}{}:
   281  					default:
   282  						errCh <- fmt.Errorf("multiple pushes happen simultaneously")
   283  						return
   284  					}
   285  					atomic.AddInt32(&fullPushes, 1)
   286  					time.Sleep(opts.debounceMax * 2)
   287  					<-pushingCh
   288  				} else {
   289  					atomic.AddInt32(&partialPushes, 1)
   290  				}
   291  			}
   292  			updateSent := uatomic.NewInt64(0)
   293  
   294  			wg.Add(1)
   295  			go func() {
   296  				debounce(updateCh, stopCh, opts, fakePush, updateSent)
   297  				wg.Done()
   298  			}()
   299  
   300  			expect := func(expectedPartial, expectedFull int32) {
   301  				t.Helper()
   302  				err := retry.UntilSuccess(func() error {
   303  					select {
   304  					case err := <-errCh:
   305  						t.Error(err)
   306  						return err
   307  					default:
   308  						partial := atomic.LoadInt32(&partialPushes)
   309  						full := atomic.LoadInt32(&fullPushes)
   310  						if partial != expectedPartial || full != expectedFull {
   311  							return fmt.Errorf("got %v full and %v partial, expected %v full and %v partial", full, partial, expectedFull, expectedPartial)
   312  						}
   313  						return nil
   314  					}
   315  				}, retry.Timeout(opts.DebounceAfter*8), retry.Delay(opts.DebounceAfter/2))
   316  				if err != nil {
   317  					t.Error(err)
   318  				}
   319  			}
   320  
   321  			// Send updates
   322  			tt.test(updateCh, expect)
   323  
   324  			close(stopCh)
   325  			wg.Wait()
   326  		})
   327  	}
   328  }
   329  
   330  func BenchmarkPushRequest(b *testing.B) {
   331  	// allTriggers contains all triggers, so we can pick one at random.
   332  	// It is not a big issue if it falls out of sync, as we are just trying to generate test data
   333  	allTriggers := []model.TriggerReason{
   334  		model.EndpointUpdate,
   335  		model.ConfigUpdate,
   336  		model.ServiceUpdate,
   337  		model.ProxyUpdate,
   338  		model.GlobalUpdate,
   339  		model.UnknownTrigger,
   340  		model.DebugTrigger,
   341  		model.SecretTrigger,
   342  		model.NetworksTrigger,
   343  		model.ProxyRequest,
   344  		model.NamespaceUpdate,
   345  	}
   346  	// Number of (simulated) proxies
   347  	proxies := 500
   348  	// Number of (simulated) pushes merged
   349  	pushesMerged := 10
   350  	// Number of configs per push
   351  	configs := 1
   352  
   353  	for n := 0; n < b.N; n++ {
   354  		var req *model.PushRequest
   355  		for i := 0; i < pushesMerged; i++ {
   356  			trigger := allTriggers[i%len(allTriggers)]
   357  			nreq := &model.PushRequest{
   358  				ConfigsUpdated: sets.New[model.ConfigKey](),
   359  				Reason:         model.NewReasonStats(trigger),
   360  			}
   361  			for c := 0; c < configs; c++ {
   362  				nreq.ConfigsUpdated.Insert(model.ConfigKey{Kind: kind.ServiceEntry, Name: fmt.Sprintf("%d", c), Namespace: "default"})
   363  			}
   364  			req = req.Merge(nreq)
   365  		}
   366  		for p := 0; p < proxies; p++ {
   367  			recordPushTriggers(req.Reason)
   368  		}
   369  	}
   370  }