Skip to content

Commit c841c90

Browse files
authored
xds: suppress the duplicate errors at the client side (#9185)
Fixes #8994 This change suppresses duplicate errors on the xds client side by skipping calling watchers for the duplicates. This also prevents generating new child names for the same broken EDS resource (EDS resource has no localities) that is being re-send by the control plane. RELEASE NOTES: - xds: handle receipt of duplicated errors at the client side by suppressing them in the same manner the duplicate updates are suppressed
1 parent d7025c5 commit c841c90

2 files changed

Lines changed: 184 additions & 11 deletions

File tree

internal/xds/clients/xdsclient/authority.go

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"context"
2323
"errors"
2424
"fmt"
25+
"strings"
2526
"sync"
2627
"sync/atomic"
2728

@@ -370,24 +371,29 @@ func (a *authority) handleADSResourceUpdate(serverConfig *ServerConfig, rType Re
370371

371372
// On error, keep previous version of the resource. But update status
372373
// and error.
373-
if uErr.Err != nil {
374+
if err := uErr.Err; err != nil {
374375
if a.metricsReporter != nil {
375376
a.metricsReporter.ReportMetric(&metrics.ResourceUpdateInvalid{
376377
ServerURI: serverConfig.ServerIdentifier.ServerURI, ResourceType: rType.TypeName,
377378
})
378379
}
379-
state.md.ErrState = md.ErrState
380-
state.md.Status = md.Status
381-
for watcher := range state.watchers {
382-
watcher := watcher
383-
err := uErr.Err
384-
watcherCnt.Add(1)
385-
if state.cache == nil {
386-
funcsToSchedule = append(funcsToSchedule, func(context.Context) { watcher.ResourceError(err, done) })
387-
} else {
388-
funcsToSchedule = append(funcsToSchedule, func(context.Context) { watcher.AmbientError(err, done) })
380+
381+
// Notify watchers only if this is not a duplicated error from the previous update.
382+
if errState := state.md.ErrState; errState == nil || errState.Err == nil || !strings.Contains(state.md.ErrState.Err.Error(), err.Error()) {
383+
for watcher := range state.watchers {
384+
watcherCnt.Add(1)
385+
if state.cache == nil {
386+
funcsToSchedule = append(funcsToSchedule, func(context.Context) { watcher.ResourceError(err, done) })
387+
} else {
388+
funcsToSchedule = append(funcsToSchedule, func(context.Context) { watcher.AmbientError(err, done) })
389+
}
389390
}
390391
}
392+
393+
// Update error state.
394+
state.md.ErrState = md.ErrState
395+
state.md.Status = md.Status
396+
391397
continue
392398
}
393399

internal/xds/clients/xdsclient/test/ads_stream_ack_nack_test.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)