/* * * Copyright 2024 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package lrsclient_test import ( "context" "net" "sync" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/internal/envconfig" "google.golang.org/grpc/internal/grpctest" grpctestutils "google.golang.org/grpc/internal/testutils" "google.golang.org/grpc/internal/xds/clients" "google.golang.org/grpc/internal/xds/clients/grpctransport" "google.golang.org/grpc/internal/xds/clients/internal/testutils" "google.golang.org/grpc/internal/xds/clients/internal/testutils/e2e" "google.golang.org/grpc/internal/xds/clients/internal/testutils/fakeserver" "google.golang.org/grpc/internal/xds/clients/lrsclient" lrsclientinternal "google.golang.org/grpc/internal/xds/clients/lrsclient/internal" "google.golang.org/grpc/status" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/durationpb" v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" v3lrspb "github.com/envoyproxy/go-control-plane/envoy/service/load_stats/v3" ) type s struct { grpctest.Tester } func Test(t *testing.T) { grpctest.RunSubTests(t, s{}) } const ( testKey1 = "test-key1" testKey2 = "test-key2" defaultTestWatchExpiryTimeout = 100 * time.Millisecond defaultTestTimeout = 5 * time.Second defaultTestShortTimeout = 10 * time.Millisecond // For events expected to *not* happen. ) var ( testLocality1 = clients.Locality{Region: "test-region1"} testLocality2 = clients.Locality{Region: "test-region2"} toleranceCmpOpt = cmpopts.EquateApprox(0, 1e-5) ignoreOrderCmpOpt = protocmp.FilterField(&v3endpointpb.ClusterStats{}, "upstream_locality_stats", cmpopts.SortSlices(func(a, b protocmp.Message) bool { return a.String() < b.String() }), ) ) type wrappedListener struct { net.Listener newConnChan *testutils.Channel // Connection attempts are pushed here. } func (wl *wrappedListener) Accept() (net.Conn, error) { c, err := wl.Listener.Accept() if err != nil { return nil, err } wl.newConnChan.Send(struct{}{}) return c, err } // Tests a load reporting scenario where the LRS client is reporting loads to // multiple servers. Verifies the following: // - calling the load reporting API with different server configuration // results in connections being created to those corresponding servers // - the same load.Store is not returned when the load reporting API called // with different server configurations // - canceling the load reporting from the client results in the LRS stream // being canceled on the server func (s) TestReportLoad_ConnectionCreation(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() // Create two management servers that also serve LRS. l, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatalf("net.Listen() failed: %v", err) } newConnChan1 := testutils.NewChannelWithSize(1) lis1 := &wrappedListener{ Listener: l, newConnChan: newConnChan1, } mgmtServer1 := e2e.StartManagementServer(t, e2e.ManagementServerOptions{ Listener: lis1, SupportLoadReportingService: true, }) l, err = net.Listen("tcp", "localhost:0") if err != nil { t.Fatalf("net.Listen() failed: %v", err) } newConnChan2 := testutils.NewChannelWithSize(1) lis2 := &wrappedListener{ Listener: l, newConnChan: newConnChan2, } mgmtServer2 := e2e.StartManagementServer(t, e2e.ManagementServerOptions{ Listener: lis2, SupportLoadReportingService: true, }) // Create an LRS client with a configuration that contains both of // the above two servers. The authority name is immaterial here since load // reporting is per-server and not per-authority. nodeID := uuid.New().String() configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}} config := lrsclient.Config{ Node: clients.Node{ID: nodeID, UserAgentName: "user-agent", UserAgentVersion: "0.0.0.0"}, TransportBuilder: grpctransport.NewBuilder(configs), } client, err := lrsclient.New(config) if err != nil { t.Fatalf("lrsclient.New() failed: %v", err) } serverIdentifier1 := clients.ServerIdentifier{ServerURI: mgmtServer1.Address, Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}} loadStore1, err := client.ReportLoad(serverIdentifier1) if err != nil { t.Fatalf("client.ReportLoad() failed: %v", err) } ssCtx, ssCancel := context.WithTimeout(context.Background(), defaultTestShortTimeout) defer ssCancel() defer loadStore1.Stop(ssCtx) // Call the load reporting API to report load to the first management // server, and ensure that a connection to the server is created. if _, err := newConnChan1.Receive(ctx); err != nil { t.Fatal("Timeout when waiting for a connection to the first management server, after starting load reporting") } if _, err := mgmtServer1.LRSServer.LRSStreamOpenChan.Receive(ctx); err != nil { t.Fatal("Timeout when waiting for LRS stream to be created") } // Call the load reporting API to report load to the first management // server, and ensure that a connection to the server is created. serverIdentifier2 := clients.ServerIdentifier{ServerURI: mgmtServer2.Address, Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}} loadStore2, err := client.ReportLoad(serverIdentifier2) if err != nil { t.Fatalf("client.ReportLoad() failed: %v", err) } if _, err := newConnChan2.Receive(ctx); err != nil { t.Fatal("Timeout when waiting for a connection to the second management server, after starting load reporting") } if _, err := mgmtServer2.LRSServer.LRSStreamOpenChan.Receive(ctx); err != nil { t.Fatal("Timeout when waiting for LRS stream to be created") } if loadStore1 == loadStore2 { t.Fatalf("Got same store for different servers, want different") } // Push some loads on the received store. loadStore2.ReporterForCluster("cluster", "eds").CallDropped("test") // Ensure the initial load reporting request is received at the server. lrsServer := mgmtServer2.LRSServer req, err := lrsServer.LRSRequestChan.Receive(ctx) if err != nil { t.Fatalf("Timeout when waiting for initial LRS request: %v", err) } gotInitialReq := req.(*fakeserver.Request).Req.(*v3lrspb.LoadStatsRequest) nodeProto := &v3corepb.Node{ Id: nodeID, UserAgentName: "user-agent", UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: "0.0.0.0"}, ClientFeatures: []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw", "envoy.lrs.supports_send_all_clusters"}, } wantInitialReq := &v3lrspb.LoadStatsRequest{Node: nodeProto} if diff := cmp.Diff(gotInitialReq, wantInitialReq, protocmp.Transform()); diff != "" { t.Fatalf("Unexpected diff in initial LRS request (-got, +want):\n%s", diff) } // Send a response from the server with a small deadline. lrsServer.LRSResponseChan <- &fakeserver.Response{ Resp: &v3lrspb.LoadStatsResponse{ SendAllClusters: true, LoadReportingInterval: &durationpb.Duration{Nanos: 50000000}, // 50ms }, } // Ensure that loads are seen on the server. req, err = lrsServer.LRSRequestChan.Receive(ctx) if err != nil { t.Fatalf("Timeout when waiting for LRS request with loads: %v", err) } gotLoad := req.(*fakeserver.Request).Req.(*v3lrspb.LoadStatsRequest).ClusterStats if l := len(gotLoad); l != 1 { t.Fatalf("Received load for %d clusters, want 1", l) } // This field is set by the client to indicate the actual time elapsed since // the last report was sent. We cannot deterministically compare this, and // we cannot use the cmpopts.IgnoreFields() option on proto structs, since // we already use the protocmp.Transform() which marshals the struct into // another message. Hence setting this field to nil is the best option here. gotLoad[0].LoadReportInterval = nil wantLoad := &v3endpointpb.ClusterStats{ ClusterName: "cluster", ClusterServiceName: "eds", TotalDroppedRequests: 1, DroppedRequests: []*v3endpointpb.ClusterStats_DroppedRequests{{Category: "test", DroppedCount: 1}}, } if diff := cmp.Diff(wantLoad, gotLoad[0], protocmp.Transform(), toleranceCmpOpt, ignoreOrderCmpOpt); diff != "" { t.Fatalf("Unexpected diff in LRS request (-got, +want):\n%s", diff) } // Stop this load reporting stream, server should see error canceled. ssCtx, ssCancel = context.WithTimeout(context.Background(), defaultTestShortTimeout) defer ssCancel() loadStore2.Stop(ssCtx) // Server should receive a stream canceled error. There may be additional // load reports from the client in the channel. for { if ctx.Err() != nil { t.Fatal("Timeout when waiting for the LRS stream to be canceled on the server") } u, err := lrsServer.LRSRequestChan.Receive(ctx) if err != nil { continue } // Ignore load reports sent before the stream was cancelled. if u.(*fakeserver.Request).Err == nil { continue } if status.Code(u.(*fakeserver.Request).Err) != codes.Canceled { t.Fatalf("Unexpected LRS request: %v, want error canceled", u) } break } } // Tests a load reporting scenario where the load reporting API is called // multiple times for the same server. The test verifies the following: // - calling the load reporting API the second time for the same server // configuration does not create a new LRS stream // - the LRS stream is closed *only* after all the API calls invoke their // cancel functions // - creating new streams after the previous one was closed works func (s) TestReportLoad_StreamCreation(t *testing.T) { grpctestutils.SetEnvConfig(t, &envconfig.XDSORCAToLRSPropEnabled, true) ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() // Create a management server that serves LRS. mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{SupportLoadReportingService: true}) // Create an LRS client with configuration pointing to the above server. nodeID := uuid.New().String() configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}} config := lrsclient.Config{ Node: clients.Node{ID: nodeID, UserAgentName: "user-agent", UserAgentVersion: "0.0.0.0"}, TransportBuilder: grpctransport.NewBuilder(configs), } client, err := lrsclient.New(config) if err != nil { t.Fatalf("lrsclient.New() failed: %v", err) } // Call the load reporting API, and ensure that an LRS stream is created. serverIdentifier := clients.ServerIdentifier{ServerURI: mgmtServer.Address, Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}} loadStore1, err := client.ReportLoad(serverIdentifier) if err != nil { t.Fatalf("client.ReportLoad() failed: %v", err) } lrsServer := mgmtServer.LRSServer if _, err := lrsServer.LRSStreamOpenChan.Receive(ctx); err != nil { t.Fatalf("Timeout when waiting for LRS stream to be created: %v", err) } // Push some loads on the received store. loadStore1.ReporterForCluster("cluster1", "eds1").CallDropped("test") loadStore1.ReporterForCluster("cluster1", "eds1").CallStarted(testLocality1) loadStore1.ReporterForCluster("cluster1", "eds1").CallServerLoad(testLocality1, testKey1, 3.14) loadStore1.ReporterForCluster("cluster1", "eds1").CallServerLoad(testLocality1, testKey1, 2.718) loadStore1.ReporterForCluster("cluster1", "eds1").CallServerLoad(testLocality1, "cpu_utilization", 1.5) loadStore1.ReporterForCluster("cluster1", "eds1").CallServerLoad(testLocality1, "mem_utilization", 2.0) loadStore1.ReporterForCluster("cluster1", "eds1").CallServerLoad(testLocality1, "application_utilization", 3.0) loadStore1.ReporterForCluster("cluster1", "eds1").CallFinished(testLocality1, nil) loadStore1.ReporterForCluster("cluster1", "eds1").CallStarted(testLocality2) loadStore1.ReporterForCluster("cluster1", "eds1").CallServerLoad(testLocality2, testKey2, 1.618) loadStore1.ReporterForCluster("cluster1", "eds1").CallFinished(testLocality2, nil) // Ensure the initial load reporting request is received at the server. req, err := lrsServer.LRSRequestChan.Receive(ctx) if err != nil { t.Fatalf("Timeout when waiting for initial LRS request: %v", err) } gotInitialReq := req.(*fakeserver.Request).Req.(*v3lrspb.LoadStatsRequest) nodeProto := &v3corepb.Node{ Id: nodeID, UserAgentName: "user-agent", UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: "0.0.0.0"}, ClientFeatures: []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw", "envoy.lrs.supports_send_all_clusters"}, } wantInitialReq := &v3lrspb.LoadStatsRequest{Node: nodeProto} if diff := cmp.Diff(gotInitialReq, wantInitialReq, protocmp.Transform()); diff != "" { t.Fatalf("Unexpected diff in initial LRS request (-got, +want):\n%s", diff) } // Send a response from the server with a small deadline. lrsServer.LRSResponseChan <- &fakeserver.Response{ Resp: &v3lrspb.LoadStatsResponse{ SendAllClusters: true, LoadReportingInterval: &durationpb.Duration{Nanos: 50000000}, // 50ms }, } // Ensure that loads are seen on the server. req, err = lrsServer.LRSRequestChan.Receive(ctx) if err != nil { t.Fatal("Timeout when waiting for LRS request with loads") } gotLoad := req.(*fakeserver.Request).Req.(*v3lrspb.LoadStatsRequest).ClusterStats if l := len(gotLoad); l != 1 { t.Fatalf("Received load for %d clusters, want 1", l) } // This field is set by the client to indicate the actual time elapsed since // the last report was sent. We cannot deterministically compare this, and // we cannot use the cmpopts.IgnoreFields() option on proto structs, since // we already use the protocmp.Transform() which marshals the struct into // another message. Hence setting this field to nil is the best option here. gotLoad[0].LoadReportInterval = nil wantLoad := &v3endpointpb.ClusterStats{ ClusterName: "cluster1", ClusterServiceName: "eds1", TotalDroppedRequests: 1, DroppedRequests: []*v3endpointpb.ClusterStats_DroppedRequests{{Category: "test", DroppedCount: 1}}, UpstreamLocalityStats: []*v3endpointpb.UpstreamLocalityStats{ { Locality: &v3corepb.Locality{Region: "test-region1"}, LoadMetricStats: []*v3endpointpb.EndpointLoadMetricStats{ // TotalMetricValue is the aggregation of 3.14 + 2.718 = 5.858 {MetricName: testKey1, NumRequestsFinishedWithMetric: 2, TotalMetricValue: 5.858}}, CpuUtilization: &v3endpointpb.UnnamedEndpointLoadMetricStats{ NumRequestsFinishedWithMetric: 1, TotalMetricValue: 1.5, }, MemUtilization: &v3endpointpb.UnnamedEndpointLoadMetricStats{ NumRequestsFinishedWithMetric: 1, TotalMetricValue: 2.0, }, ApplicationUtilization: &v3endpointpb.UnnamedEndpointLoadMetricStats{ NumRequestsFinishedWithMetric: 1, TotalMetricValue: 3.0, }, TotalSuccessfulRequests: 1, TotalIssuedRequests: 1, }, { Locality: &v3corepb.Locality{Region: "test-region2"}, LoadMetricStats: []*v3endpointpb.EndpointLoadMetricStats{ {MetricName: testKey2, NumRequestsFinishedWithMetric: 1, TotalMetricValue: 1.618}}, TotalSuccessfulRequests: 1, TotalIssuedRequests: 1, }, }, } if diff := cmp.Diff(wantLoad, gotLoad[0], protocmp.Transform(), toleranceCmpOpt, ignoreOrderCmpOpt); diff != "" { t.Fatalf("Unexpected diff in LRS request (-got, +want):\n%s", diff) } // Make another call to the load reporting API, and ensure that a new LRS // stream is not created. loadStore2, err := client.ReportLoad(serverIdentifier) if err != nil { t.Fatalf("client.ReportLoad() failed: %v", err) } sCtx, sCancel := context.WithTimeout(context.Background(), defaultTestShortTimeout) defer sCancel() if _, err := lrsServer.LRSStreamOpenChan.Receive(sCtx); err != context.DeadlineExceeded { t.Fatal("New LRS stream created when expected to use an existing one") } // Push more loads. loadStore2.ReporterForCluster("cluster2", "eds2").CallDropped("test") // Ensure that loads are seen on the server. We need a loop here because // there could have been some requests from the client in the time between // us reading the first request and now. Those would have been queued in the // request channel that we read out of. for { if ctx.Err() != nil { t.Fatalf("Timeout when waiting for new loads to be seen on the server") } req, err = lrsServer.LRSRequestChan.Receive(ctx) if err != nil { continue } gotLoad = req.(*fakeserver.Request).Req.(*v3lrspb.LoadStatsRequest).ClusterStats if l := len(gotLoad); l != 1 { continue } gotLoad[0].LoadReportInterval = nil wantLoad := &v3endpointpb.ClusterStats{ ClusterName: "cluster2", ClusterServiceName: "eds2", TotalDroppedRequests: 1, DroppedRequests: []*v3endpointpb.ClusterStats_DroppedRequests{{Category: "test", DroppedCount: 1}}, } if diff := cmp.Diff(wantLoad, gotLoad[0], protocmp.Transform()); diff != "" { t.Logf("Unexpected diff in LRS request (-got, +want):\n%s", diff) continue } break } // Cancel the first load reporting call, and ensure that the stream does not // close (because we have another call open). ssCtx, ssCancel := context.WithTimeout(context.Background(), defaultTestShortTimeout) defer ssCancel() loadStore1.Stop(ssCtx) sCtx, sCancel = context.WithTimeout(context.Background(), defaultTestShortTimeout) defer sCancel() if _, err := lrsServer.LRSStreamCloseChan.Receive(sCtx); err != context.DeadlineExceeded { t.Fatal("LRS stream closed when expected to stay open") } // Stop the second load reporting call, and ensure the stream is closed. ssCtx, ssCancel = context.WithTimeout(context.Background(), defaultTestShortTimeout) defer ssCancel() loadStore2.Stop(ssCtx) if _, err := lrsServer.LRSStreamCloseChan.Receive(ctx); err != nil { t.Fatal("Timeout waiting for LRS stream to close") } // Calling the load reporting API again should result in the creation of a // new LRS stream. This ensures that creating and closing multiple streams // works smoothly. loadStore3, err := client.ReportLoad(serverIdentifier) if err != nil { t.Fatalf("client.ReportLoad() failed: %v", err) } if _, err := lrsServer.LRSStreamOpenChan.Receive(ctx); err != nil { t.Fatalf("Timeout when waiting for LRS stream to be created: %v", err) } ssCtx, ssCancel = context.WithTimeout(context.Background(), defaultTestShortTimeout) defer ssCancel() loadStore3.Stop(ssCtx) } // TestReportLoad_StopWithContext tests the behavior of LoadStore.Stop() when // called with a context. It verifies that: // - Stop() blocks until the context expires or final load send attempt is // made. // - Final load report is seen on the server after stop is called. // - The stream is closed after Stop() returns. func (s) TestReportLoad_StopWithContext(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() // Create a management server that serves LRS. mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{SupportLoadReportingService: true}) // Create an LRS client with configuration pointing to the above server. nodeID := uuid.New().String() configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}} config := lrsclient.Config{ Node: clients.Node{ID: nodeID, UserAgentName: "user-agent", UserAgentVersion: "0.0.0.0"}, TransportBuilder: grpctransport.NewBuilder(configs), } client, err := lrsclient.New(config) if err != nil { t.Fatalf("lrsclient.New() failed: %v", err) } // Call the load reporting API, and ensure that an LRS stream is created. serverIdentifier := clients.ServerIdentifier{ServerURI: mgmtServer.Address, Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}} loadStore, err := client.ReportLoad(serverIdentifier) if err != nil { t.Fatalf("client.ReportLoad() failed: %v", err) } lrsServer := mgmtServer.LRSServer if _, err := lrsServer.LRSStreamOpenChan.Receive(ctx); err != nil { t.Fatalf("Timeout when waiting for LRS stream to be created: %v", err) } // Push some loads on the received store. loadStore.ReporterForCluster("cluster1", "eds1").CallDropped("test") // Ensure the initial load reporting request is received at the server. req, err := lrsServer.LRSRequestChan.Receive(ctx) if err != nil { t.Fatalf("Timeout when waiting for initial LRS request: %v", err) } gotInitialReq := req.(*fakeserver.Request).Req.(*v3lrspb.LoadStatsRequest) nodeProto := &v3corepb.Node{ Id: nodeID, UserAgentName: "user-agent", UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: "0.0.0.0"}, ClientFeatures: []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw", "envoy.lrs.supports_send_all_clusters"}, } wantInitialReq := &v3lrspb.LoadStatsRequest{Node: nodeProto} if diff := cmp.Diff(gotInitialReq, wantInitialReq, protocmp.Transform()); diff != "" { t.Fatalf("Unexpected diff in initial LRS request (-got, +want):\n%s", diff) } // Send a response from the server with a small deadline. lrsServer.LRSResponseChan <- &fakeserver.Response{ Resp: &v3lrspb.LoadStatsResponse{ SendAllClusters: true, LoadReportingInterval: &durationpb.Duration{Nanos: 50000000}, // 50ms }, } // Ensure that loads are seen on the server. req, err = lrsServer.LRSRequestChan.Receive(ctx) if err != nil { t.Fatal("Timeout when waiting for LRS request with loads") } gotLoad := req.(*fakeserver.Request).Req.(*v3lrspb.LoadStatsRequest).ClusterStats if l := len(gotLoad); l != 1 { t.Fatalf("Received load for %d clusters, want 1", l) } // This field is set by the client to indicate the actual time elapsed since // the last report was sent. We cannot deterministically compare this, and // we cannot use the cmpopts.IgnoreFields() option on proto structs, since // we already use the protocmp.Transform() which marshals the struct into // another message. Hence setting this field to nil is the best option here. gotLoad[0].LoadReportInterval = nil wantLoad := &v3endpointpb.ClusterStats{ ClusterName: "cluster1", ClusterServiceName: "eds1", TotalDroppedRequests: 1, DroppedRequests: []*v3endpointpb.ClusterStats_DroppedRequests{{Category: "test", DroppedCount: 1}}, } if diff := cmp.Diff(wantLoad, gotLoad[0], protocmp.Transform(), toleranceCmpOpt, ignoreOrderCmpOpt); diff != "" { t.Fatalf("Unexpected diff in LRS request (-got, +want):\n%s", diff) } // Create a context for Stop() that remains until the end of test to ensure // that only possibility of Stop()s to finish is if final load send attempt // is made. If final load attempt is not made, test will timeout. stopCtx, stopCancel := context.WithCancel(ctx) defer stopCancel() // Push more loads. loadStore.ReporterForCluster("cluster2", "eds2").CallDropped("test") stopCalled := make(chan struct{}) // Call Stop in a separate goroutine. It will block until // final load send attempt is made. go func() { loadStore.Stop(stopCtx) close(stopCalled) }() <-stopCalled // Ensure that loads are seen on the server. We need a loop here because // there could have been some requests from the client in the time between // us reading the first request and now. Those would have been queued in the // request channel that we read out of. for { if ctx.Err() != nil { t.Fatalf("Timeout when waiting for new loads to be seen on the server") } req, err = lrsServer.LRSRequestChan.Receive(ctx) if err != nil || req.(*fakeserver.Request).Err != nil { continue } if req.(*fakeserver.Request).Req.(*v3lrspb.LoadStatsRequest) == nil { // This can happen due to a race: // 1. Load for "cluster2" is reported just before Stop(). // 2. The periodic ticker might send this load before Stop()'s // final send mechanism processes it, clearing the data. // 3. Stop()'s final send might then send an empty report. // This is acceptable for this test because we only need to verify // if the final load report send attempt was made. t.Logf("Empty final load report sent on server") break } gotLoad = req.(*fakeserver.Request).Req.(*v3lrspb.LoadStatsRequest).ClusterStats if l := len(gotLoad); l != 1 { continue } gotLoad[0].LoadReportInterval = nil wantLoad := &v3endpointpb.ClusterStats{ ClusterName: "cluster2", ClusterServiceName: "eds2", TotalDroppedRequests: 1, DroppedRequests: []*v3endpointpb.ClusterStats_DroppedRequests{{Category: "test", DroppedCount: 1}}, } if diff := cmp.Diff(wantLoad, gotLoad[0], protocmp.Transform()); diff != "" { t.Logf("Unexpected diff in LRS request (-got, +want):\n%s", diff) continue } break } // Verify the stream is eventually closed on the server side. if _, err := lrsServer.LRSStreamCloseChan.Receive(ctx); err != nil { t.Fatal("Timeout waiting for LRS stream to close") } } // TestReportLoad_LoadReportInterval tests verify that the load report interval // received by the LRS server is the duration between start of last load // reporting by the client and the time when the load is reported to the LRS // server. func (s) TestReportLoad_LoadReportInterval(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() originalTimeNow := lrsclientinternal.TimeNow t.Cleanup(func() { lrsclientinternal.TimeNow = originalTimeNow }) // Create a management server that serves LRS. mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{SupportLoadReportingService: true}) // Create an LRS client with configuration pointing to the above server. nodeID := uuid.New().String() configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}} config := lrsclient.Config{ Node: clients.Node{ID: nodeID, UserAgentName: "user-agent", UserAgentVersion: "0.0.0.0"}, TransportBuilder: grpctransport.NewBuilder(configs), } client, err := lrsclient.New(config) if err != nil { t.Fatalf("lrsclient.New() failed: %v", err) } // Call the load reporting API, and ensure that an LRS stream is created. serverIdentifier := clients.ServerIdentifier{ServerURI: mgmtServer.Address, Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}} loadStore1, err := client.ReportLoad(serverIdentifier) if err != nil { t.Fatalf("client.ReportLoad() failed: %v", err) } lrsServer := mgmtServer.LRSServer if _, err := lrsServer.LRSStreamOpenChan.Receive(ctx); err != nil { t.Fatalf("Timeout when waiting for LRS stream to be created: %v", err) } stateMu := sync.Mutex{} // Initial time for reporter creation currentTime := time.Now() lrsclientinternal.TimeNow = func() time.Time { stateMu.Lock() defer stateMu.Unlock() return currentTime } // Report dummy drop to ensure stats is not nil. loadStore1.ReporterForCluster("cluster1", "eds1").CallDropped("test") // Update currentTime to simulate the passage of time between the reporter // creation and first stats() call. stateMu.Lock() currentTime = currentTime.Add(5 * time.Second) stateMu.Unlock() // Ensure the initial load reporting request is received at the server. req, err := lrsServer.LRSRequestChan.Receive(ctx) if err != nil { t.Fatalf("Timeout when waiting for initial LRS request: %v", err) } gotInitialReq := req.(*fakeserver.Request).Req.(*v3lrspb.LoadStatsRequest) nodeProto := &v3corepb.Node{ Id: nodeID, UserAgentName: "user-agent", UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: "0.0.0.0"}, ClientFeatures: []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw", "envoy.lrs.supports_send_all_clusters"}, } wantInitialReq := &v3lrspb.LoadStatsRequest{Node: nodeProto} if diff := cmp.Diff(gotInitialReq, wantInitialReq, protocmp.Transform()); diff != "" { t.Fatalf("Unexpected diff in initial LRS request (-got, +want):\n%s", diff) } // Send a response from the server with a small deadline. lrsServer.LRSResponseChan <- &fakeserver.Response{ Resp: &v3lrspb.LoadStatsResponse{ SendAllClusters: true, LoadReportingInterval: &durationpb.Duration{Nanos: 50000000}, // 50ms }, } // Ensure that loads are seen on the server. req, err = lrsServer.LRSRequestChan.Receive(ctx) if err != nil { t.Fatal("Timeout when waiting for LRS request with loads") } gotLoad := req.(*fakeserver.Request).Req.(*v3lrspb.LoadStatsRequest).ClusterStats if l := len(gotLoad); l != 1 { t.Fatalf("Received load for %d clusters, want 1", l) } // Verify load received at LRS server has load report interval calculated // from the time of reporter creation. if got, want := gotLoad[0].GetLoadReportInterval().AsDuration(), 5*time.Second; got != want { t.Errorf("Got load report interval %v, want %v", got, want) } ssCtx, ssCancel := context.WithTimeout(context.Background(), time.Millisecond) defer ssCancel() loadStore1.Stop(ssCtx) }