@@ -506,3 +506,170 @@ func (s) TestADS_ACK_NACK_ResourceIsNotRequestedAnymore(t *testing.T) {
506506 t .Fatal (err )
507507 }
508508}
509+
510+ // TestADS_NACKError_DuplicateSuppression verifies that when a duplicate invalid
511+ // LDS resource is sent by the server, the client suppresses the duplicate error
512+ // notification to the watcher.
513+ func (s ) TestADS_NACKError_DuplicateSuppression (t * testing.T ) {
514+ // The first NACK is expected upon receiving the invalid resource. The
515+ // second NACK is a duplicate, sent because the management server resends
516+ // the invalid resource in a loop as long as the resource update has not
517+ // changed to a valid one. The third NACK is waited for to verify that the
518+ // duplicated error is still suppressed as expected.
519+ const wantNACKCount = 3
520+
521+ nacksReceivedCh := make (chan struct {})
522+ nackCount := 0
523+ mgmtServer := e2e .StartManagementServer (t , e2e.ManagementServerOptions {
524+ OnStreamRequest : func (_ int64 , req * v3discoverypb.DiscoveryRequest ) error {
525+ if req .GetTypeUrl () != xdsresource .V3ListenerURL {
526+ return nil
527+ }
528+ if req .GetErrorDetail () != nil {
529+ nackCount ++
530+ if nackCount == wantNACKCount {
531+ close (nacksReceivedCh )
532+ }
533+ }
534+ return nil
535+ },
536+ })
537+
538+ const listenerName = "listener"
539+ nodeID := uuid .New ().String ()
540+
541+ // Create an xDS client pointing to the above server.
542+ configs := map [string ]grpctransport.Config {"insecure" : {Credentials : insecure .NewBundle ()}}
543+ client := createXDSClient (t , mgmtServer .Address , nodeID , grpctransport .NewBuilder (configs ))
544+
545+ watcher := newListenerWatcher ()
546+ ldsCancel := client .WatchResource (xdsresource .V3ListenerURL , listenerName , watcher )
547+ defer ldsCancel ()
548+
549+ resources := e2e.UpdateOptions {
550+ NodeID : nodeID ,
551+ Listeners : []* v3listenerpb.Listener {badListenerResource (t , listenerName )},
552+ SkipValidation : true ,
553+ }
554+ ctx , cancel := context .WithTimeout (context .Background (), defaultTestTimeout )
555+ defer cancel ()
556+ if err := mgmtServer .Update (ctx , resources ); err != nil {
557+ t .Fatalf ("Failed to update management server with resources: %v, err: %v" , resources , err )
558+ }
559+
560+ // Verify that the watcher receives the initial error.
561+ v , err := watcher .resourceErrCh .Receive (ctx )
562+ if err != nil {
563+ t .Fatalf ("Timeout when waiting for an endpoints resource error from the management server: %v" , err )
564+ }
565+ const wantNackErr = "no RouteSpecifier"
566+ if gotErr := v .(listenerUpdateErrTuple ).resourceErr ; ! strings .Contains (gotErr .Error (), wantNackErr ) {
567+ t .Fatalf ("Update received with error: %v, want %q" , gotErr , wantNackErr )
568+ }
569+
570+ // Wait for the management server to receive at least wantNACKCount NACKs,
571+ // indicating that the NACK resend loop has run multiple times.
572+ select {
573+ case <- nacksReceivedCh :
574+ case <- ctx .Done ():
575+ t .Fatalf ("Timeout waiting for %d NACKs to be received by the server: %v" , wantNACKCount , ctx .Err ())
576+ }
577+
578+ // Verify that no duplicate error update was written to the error channel,
579+ // proving that subsequent duplicate error updates were suppressed.
580+ sCtx , sCancel := context .WithTimeout (ctx , defaultTestShortTimeout )
581+ defer sCancel ()
582+ if v , err := watcher .resourceErrCh .Receive (sCtx ); err == nil {
583+ t .Fatalf ("Unexpected duplicate error received by watcher: %v" , v )
584+ }
585+ }
586+
587+ // TestADS_NACKError_DuplicateSuppression_ConcatenatedErrorChange verifies that
588+ // when multiple invalid resources cause a concatenated error stored in
589+ // metadata, and a subsequent update carries a duplicate error for only a subset
590+ // of resources (changing the concatenated error string), the client still
591+ // suppresses the duplicate error notification to the watcher.
592+ func (s ) TestADS_NACKError_DuplicateSuppression_ConcatenatedErrorChange (t * testing.T ) {
593+ mgmtServer := e2e .StartManagementServer (t , e2e.ManagementServerOptions {})
594+
595+ const listenerName1 = "listener-1"
596+ const listenerName2 = "listener-2"
597+ nodeID := uuid .New ().String ()
598+
599+ configs := map [string ]grpctransport.Config {"insecure" : {Credentials : insecure .NewBundle ()}}
600+ client := createXDSClient (t , mgmtServer .Address , nodeID , grpctransport .NewBuilder (configs ))
601+
602+ watcher1 := newListenerWatcher ()
603+ ldsCancel1 := client .WatchResource (xdsresource .V3ListenerURL , listenerName1 , watcher1 )
604+ defer ldsCancel1 ()
605+
606+ watcher2 := newListenerWatcher ()
607+ ldsCancel2 := client .WatchResource (xdsresource .V3ListenerURL , listenerName2 , watcher2 )
608+ defer ldsCancel2 ()
609+
610+ // Update 1: Management server sends two invalid listener resources.
611+ resources1 := e2e.UpdateOptions {
612+ NodeID : nodeID ,
613+ Listeners : []* v3listenerpb.Listener {
614+ badListenerResource (t , listenerName1 ),
615+ badListenerResource (t , listenerName2 ),
616+ },
617+ SkipValidation : true ,
618+ }
619+ ctx , cancel := context .WithTimeout (context .Background (), defaultTestTimeout )
620+ defer cancel ()
621+
622+ if err := mgmtServer .Update (ctx , resources1 ); err != nil {
623+ t .Fatalf ("Failed to update management server with resources: %v, err: %v" , resources1 , err )
624+ }
625+
626+ // Verify that watcher1 receives the initial error for listener 1.
627+ v1 , err := watcher1 .resourceErrCh .Receive (ctx )
628+ if err != nil {
629+ t .Fatalf ("Timeout waiting for listener 1 error from management server: %v" , err )
630+ }
631+ const wantNackErr = "no RouteSpecifier"
632+ if gotErr := v1 .(listenerUpdateErrTuple ).resourceErr ; ! strings .Contains (gotErr .Error (), wantNackErr ) {
633+ t .Fatalf ("Update 1 listener 1 received with error: %v, want %q" , gotErr , wantNackErr )
634+ }
635+
636+ // Verify that watcher2 receives the initial error for listener 2.
637+ v2 , err := watcher2 .resourceErrCh .Receive (ctx )
638+ if err != nil {
639+ t .Fatalf ("Timeout waiting for listener 2 error from management server: %v" , err )
640+ }
641+
642+ if gotErr := v2 .(listenerUpdateErrTuple ).resourceErr ; ! strings .Contains (gotErr .Error (), wantNackErr ) {
643+ t .Fatalf ("Update 1 listener 2 received with error: %v, want %q" , gotErr , wantNackErr )
644+ }
645+
646+ // Update 2: Management server sends listener 1 still invalid, but listener
647+ // 2 now valid. This changes the concatenated error stored in metadata
648+ // (from Error1+Error2 to just Error1).
649+ resources2 := e2e.UpdateOptions {
650+ NodeID : nodeID ,
651+ Listeners : []* v3listenerpb.Listener {
652+ badListenerResource (t , listenerName1 ),
653+ e2e .DefaultClientListener (listenerName2 , "route-config-2" ),
654+ },
655+ SkipValidation : true ,
656+ }
657+
658+ if err := mgmtServer .Update (ctx , resources2 ); err != nil {
659+ t .Fatalf ("Failed to update management server with resources: %v, err: %v" , resources2 , err )
660+ }
661+
662+ // Wait for watcher2 to receive the valid resource update for listener 2.
663+ if _ , err := watcher2 .updateCh .Receive (ctx ); err != nil {
664+ t .Fatalf ("Timeout waiting for listener 2 resource update: %v" , err )
665+ }
666+
667+ // Verify that no duplicate error update was written to watcher 1 error
668+ // channel, proving that the duplicate error on listener 1 was suppressed
669+ // despite the concatenated error changing.
670+ sCtx , sCancel := context .WithTimeout (ctx , defaultTestShortTimeout )
671+ defer sCancel ()
672+ if v , err := watcher1 .resourceErrCh .Receive (sCtx ); err == nil {
673+ t .Fatalf ("Unexpected duplicate error received by watcher 1: %v" , v )
674+ }
675+ }
0 commit comments