google.golang.org/grpc@v1.62.1/xds/internal/xdsclient/transport/loadreport_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  package transport_test
    19  
    20  import (
    21  	"context"
    22  	"testing"
    23  	"time"
    24  
    25  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    26  	"github.com/google/go-cmp/cmp"
    27  	"github.com/google/uuid"
    28  	"google.golang.org/grpc/internal/testutils/xds/fakeserver"
    29  	"google.golang.org/grpc/xds/internal/testutils"
    30  	"google.golang.org/grpc/xds/internal/xdsclient/transport"
    31  	"google.golang.org/protobuf/testing/protocmp"
    32  	"google.golang.org/protobuf/types/known/durationpb"
    33  
    34  	v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
    35  	v3lrspb "github.com/envoyproxy/go-control-plane/envoy/service/load_stats/v3"
    36  )
    37  
    38  func (s) TestReportLoad(t *testing.T) {
    39  	// Create a fake xDS management server listening on a local port.
    40  	mgmtServer, cleanup := startFakeManagementServer(t)
    41  	defer cleanup()
    42  	t.Logf("Started xDS management server on %s", mgmtServer.Address)
    43  
    44  	// Create a transport to the fake management server.
    45  	nodeProto := &v3corepb.Node{Id: uuid.New().String()}
    46  	tr, err := transport.New(transport.Options{
    47  		ServerCfg:      *testutils.ServerConfigForAddress(t, mgmtServer.Address),
    48  		NodeProto:      nodeProto,
    49  		OnRecvHandler:  func(transport.ResourceUpdate) error { return nil }, // No ADS validation.
    50  		OnErrorHandler: func(error) {},                                      // No ADS stream error handling.
    51  		OnSendHandler:  func(*transport.ResourceSendInfo) {},                // No ADS stream update handling.
    52  		Backoff:        func(int) time.Duration { return time.Duration(0) }, // No backoff.
    53  	})
    54  	if err != nil {
    55  		t.Fatalf("Failed to create xDS transport: %v", err)
    56  	}
    57  	defer tr.Close()
    58  
    59  	// Ensure that a new connection is made to the management server.
    60  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
    61  	defer cancel()
    62  	if _, err := mgmtServer.NewConnChan.Receive(ctx); err != nil {
    63  		t.Fatalf("Timeout when waiting for a new connection to the management server: %v", err)
    64  	}
    65  
    66  	// Call the load reporting API, and ensure that an LRS stream is created.
    67  	store1, cancelLRS1 := tr.ReportLoad()
    68  	if err != nil {
    69  		t.Fatalf("Failed to start LRS load reporting: %v", err)
    70  	}
    71  	if _, err := mgmtServer.LRSStreamOpenChan.Receive(ctx); err != nil {
    72  		t.Fatalf("Timeout when waiting for LRS stream to be created: %v", err)
    73  	}
    74  
    75  	// Push some loads on the received store.
    76  	store1.PerCluster("cluster1", "eds1").CallDropped("test")
    77  
    78  	// Ensure the initial request is received.
    79  	req, err := mgmtServer.LRSRequestChan.Receive(ctx)
    80  	if err != nil {
    81  		t.Fatalf("Timeout when waiting for initial LRS request: %v", err)
    82  	}
    83  	gotInitialReq := req.(*fakeserver.Request).Req.(*v3lrspb.LoadStatsRequest)
    84  	nodeProto.ClientFeatures = []string{"envoy.lrs.supports_send_all_clusters"}
    85  	wantInitialReq := &v3lrspb.LoadStatsRequest{Node: nodeProto}
    86  	if diff := cmp.Diff(gotInitialReq, wantInitialReq, protocmp.Transform()); diff != "" {
    87  		t.Fatalf("Unexpected diff in initial LRS request (-got, +want):\n%s", diff)
    88  	}
    89  
    90  	// Send a response from the server with a small deadline.
    91  	mgmtServer.LRSResponseChan <- &fakeserver.Response{
    92  		Resp: &v3lrspb.LoadStatsResponse{
    93  			SendAllClusters:       true,
    94  			LoadReportingInterval: &durationpb.Duration{Nanos: 50000000}, // 50ms
    95  		},
    96  	}
    97  
    98  	// Ensure that loads are seen on the server.
    99  	req, err = mgmtServer.LRSRequestChan.Receive(ctx)
   100  	if err != nil {
   101  		t.Fatalf("Timeout when waiting for LRS request with loads: %v", err)
   102  	}
   103  	gotLoad := req.(*fakeserver.Request).Req.(*v3lrspb.LoadStatsRequest).ClusterStats
   104  	if l := len(gotLoad); l != 1 {
   105  		t.Fatalf("Received load for %d clusters, want 1", l)
   106  	}
   107  	// This field is set by the client to indicate the actual time elapsed since
   108  	// the last report was sent. We cannot deterministically compare this, and
   109  	// we cannot use the cmpopts.IgnoreFields() option on proto structs, since
   110  	// we already use the protocmp.Transform() which marshals the struct into
   111  	// another message. Hence setting this field to nil is the best option here.
   112  	gotLoad[0].LoadReportInterval = nil
   113  	wantLoad := &v3endpointpb.ClusterStats{
   114  		ClusterName:          "cluster1",
   115  		ClusterServiceName:   "eds1",
   116  		TotalDroppedRequests: 1,
   117  		DroppedRequests:      []*v3endpointpb.ClusterStats_DroppedRequests{{Category: "test", DroppedCount: 1}},
   118  	}
   119  	if diff := cmp.Diff(wantLoad, gotLoad[0], protocmp.Transform()); diff != "" {
   120  		t.Fatalf("Unexpected diff in LRS request (-got, +want):\n%s", diff)
   121  	}
   122  
   123  	// Make another call to the load reporting API, and ensure that a new LRS
   124  	// stream is not created.
   125  	store2, cancelLRS2 := tr.ReportLoad()
   126  	if err != nil {
   127  		t.Fatalf("Failed to start LRS load reporting: %v", err)
   128  	}
   129  	sCtx, sCancel := context.WithTimeout(context.Background(), defaultTestShortTimeout)
   130  	defer sCancel()
   131  	if _, err := mgmtServer.LRSStreamOpenChan.Receive(sCtx); err != context.DeadlineExceeded {
   132  		t.Fatal("New LRS stream created when expected to use an existing one")
   133  	}
   134  
   135  	// Push more loads.
   136  	store2.PerCluster("cluster2", "eds2").CallDropped("test")
   137  
   138  	// Ensure that loads are seen on the server. We need a loop here because
   139  	// there could have been some requests from the client in the time between
   140  	// us reading the first request and now. Those would have been queued in the
   141  	// request channel that we read out of.
   142  	for {
   143  		if ctx.Err() != nil {
   144  			t.Fatalf("Timeout when waiting for new loads to be seen on the server")
   145  		}
   146  
   147  		req, err = mgmtServer.LRSRequestChan.Receive(ctx)
   148  		if err != nil {
   149  			continue
   150  		}
   151  		gotLoad = req.(*fakeserver.Request).Req.(*v3lrspb.LoadStatsRequest).ClusterStats
   152  		if l := len(gotLoad); l != 1 {
   153  			continue
   154  		}
   155  		gotLoad[0].LoadReportInterval = nil
   156  		wantLoad := &v3endpointpb.ClusterStats{
   157  			ClusterName:          "cluster2",
   158  			ClusterServiceName:   "eds2",
   159  			TotalDroppedRequests: 1,
   160  			DroppedRequests:      []*v3endpointpb.ClusterStats_DroppedRequests{{Category: "test", DroppedCount: 1}},
   161  		}
   162  		if diff := cmp.Diff(wantLoad, gotLoad[0], protocmp.Transform()); diff != "" {
   163  			t.Logf("Unexpected diff in LRS request (-got, +want):\n%s", diff)
   164  			continue
   165  		}
   166  		break
   167  	}
   168  
   169  	// Cancel the first load reporting call, and ensure that the stream does not
   170  	// close (because we have aother call open).
   171  	cancelLRS1()
   172  	sCtx, sCancel = context.WithTimeout(context.Background(), defaultTestShortTimeout)
   173  	defer sCancel()
   174  	if _, err := mgmtServer.LRSStreamCloseChan.Receive(sCtx); err != context.DeadlineExceeded {
   175  		t.Fatal("LRS stream closed when expected to stay open")
   176  	}
   177  
   178  	// Cancel the second load reporting call, and ensure the stream is closed.
   179  	cancelLRS2()
   180  	if _, err := mgmtServer.LRSStreamCloseChan.Receive(ctx); err != nil {
   181  		t.Fatal("Timeout waiting for LRS stream to close")
   182  	}
   183  
   184  	// Calling the load reporting API again should result in the creation of a
   185  	// new LRS stream. This ensures that creating and closing multiple streams
   186  	// works smoothly.
   187  	_, cancelLRS3 := tr.ReportLoad()
   188  	if err != nil {
   189  		t.Fatalf("Failed to start LRS load reporting: %v", err)
   190  	}
   191  	if _, err := mgmtServer.LRSStreamOpenChan.Receive(ctx); err != nil {
   192  		t.Fatalf("Timeout when waiting for LRS stream to be created: %v", err)
   193  	}
   194  	cancelLRS3()
   195  }