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 }