Skip to content

Commit 65fd12d

Browse files
committed
override time.Now() for predictable report interval calculations
1 parent 7313692 commit 65fd12d

File tree

6 files changed

+112
-116
lines changed

6 files changed

+112
-116
lines changed

xds/internal/clients/lrsclient/load_store.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import (
2525
"time"
2626
)
2727

28+
// clockNow is used to get the current time. It can be overridden in tests.
29+
var clockNow = time.Now
30+
2831
// A LoadStore aggregates loads for multiple clusters and services that are
2932
// intended to be reported via LRS.
3033
//
@@ -84,7 +87,7 @@ func (ls *LoadStore) ReporterForCluster(clusterName, serviceName string) *PerClu
8487
p := &PerClusterReporter{
8588
cluster: clusterName,
8689
service: serviceName,
87-
lastLoadReportAt: time.Now(),
90+
lastLoadReportAt: clockNow(),
8891
}
8992
c[serviceName] = p
9093
return p
@@ -245,8 +248,8 @@ func (p *PerClusterReporter) stats() *loadData {
245248
})
246249

247250
p.mu.Lock()
248-
sd.reportInterval = time.Since(p.lastLoadReportAt)
249-
p.lastLoadReportAt = time.Now()
251+
sd.reportInterval = clockNow().Sub(p.lastLoadReportAt)
252+
p.lastLoadReportAt = clockNow()
250253
p.mu.Unlock()
251254

252255
if sd.totalDrops == 0 && len(sd.drops) == 0 && len(sd.localityStats) == 0 {

xds/internal/clients/lrsclient/load_store_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"sort"
2323
"sync"
2424
"testing"
25+
"time"
2526

2627
"github.com/google/go-cmp/cmp"
2728
"github.com/google/go-cmp/cmp/cmpopts"
@@ -471,3 +472,49 @@ func TestStoreStatsEmptyDataNotReported(t *testing.T) {
471472
t.Error(err)
472473
}
473474
}
475+
476+
// TestStoreReportInterval tests that the report interval is correctly
477+
// calculated between consecutive calls to stats().
478+
func TestStoreReportInterval(t *testing.T) {
479+
originalClockNow := clockNow
480+
t.Cleanup(func() { clockNow = originalClockNow })
481+
482+
// Initial time for reporter creation
483+
currentTime := time.Now()
484+
clockNow = func() time.Time {
485+
return currentTime
486+
}
487+
488+
store := newLoadStore()
489+
reporter := store.ReporterForCluster("test-cluster", "test-service")
490+
// To ensure stats() returns non-nil data, report a dummy drop.
491+
reporter.CallDropped("dummy-category")
492+
493+
// First call to stats() calculates the report interval from reporter
494+
// creation time.
495+
currentTime = currentTime.Add(5 * time.Second)
496+
stats1 := reporter.stats()
497+
498+
if stats1 == nil {
499+
t.Fatalf("stats1 is nil after reporting a drop")
500+
}
501+
wantInterval := 5 * time.Second
502+
if stats1.reportInterval != wantInterval {
503+
t.Errorf("First call stats() = %v, want %v", stats1.reportInterval, wantInterval)
504+
}
505+
506+
// Second call to stats() calculates the report interval from last stats()
507+
// call time.
508+
currentTime = currentTime.Add(10 * time.Second)
509+
// Report another dummy drop to ensure stats2 is not nil.
510+
reporter.CallDropped("dummy-category-2")
511+
stats2 := reporter.stats()
512+
513+
if stats2 == nil {
514+
t.Fatalf("stats2 is nil after reporting a drop")
515+
}
516+
wantInterval = 10 * time.Second
517+
if stats2.reportInterval != wantInterval {
518+
t.Errorf("Second call stats() = %v, want %v", stats2.reportInterval, wantInterval)
519+
}
520+
}

xds/internal/clients/lrsclient/loadreport_test.go

Lines changed: 4 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ func (s) TestReportLoad_ConnectionCreation(t *testing.T) {
159159
t.Fatal("Timeout when waiting for LRS stream to be created")
160160
}
161161

162-
// Call the load reporting API to report load to the second management
162+
// Call the load reporting API to report load to the first management
163163
// server, and ensure that a connection to the server is created.
164164
serverIdentifier2 := clients.ServerIdentifier{ServerURI: mgmtServer2.Address, Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}}
165165
loadStore2, err := client.ReportLoad(serverIdentifier2)
@@ -199,11 +199,10 @@ func (s) TestReportLoad_ConnectionCreation(t *testing.T) {
199199
}
200200

201201
// Send a response from the server with a small deadline.
202-
serverReportInterval := 50 * time.Millisecond
203202
lrsServer.LRSResponseChan <- &fakeserver.Response{
204203
Resp: &v3lrspb.LoadStatsResponse{
205204
SendAllClusters: true,
206-
LoadReportingInterval: &durationpb.Duration{Nanos: int32(serverReportInterval.Nanoseconds())}, // 50ms
205+
LoadReportingInterval: &durationpb.Duration{Nanos: 50000000}, // 50ms
207206
},
208207
}
209208

@@ -217,19 +216,6 @@ func (s) TestReportLoad_ConnectionCreation(t *testing.T) {
217216
t.Fatalf("Received load for %d clusters, want 1", l)
218217
}
219218

220-
// Verify that LoadReportInterval for the first load report is positive but
221-
// not excessively large.
222-
//
223-
// Max expected: serverReportInterval + tolerance (e.g., 500ms).
224-
firstLoadReportInterval := gotLoad[0].GetLoadReportInterval().AsDuration()
225-
if firstLoadReportInterval <= 0 {
226-
t.Fatalf("First LoadReportInterval = %v, want > 0", firstLoadReportInterval)
227-
}
228-
tolerance := 500 * time.Millisecond
229-
if firstLoadReportInterval > serverReportInterval+tolerance {
230-
t.Errorf("First LoadReportInterval is unexpectedly large: %v", firstLoadReportInterval)
231-
}
232-
233219
// This field is set by the client to indicate the actual time elapsed since
234220
// the last report was sent. We cannot deterministically compare this, and
235221
// we cannot use the cmpopts.IgnoreFields() option on proto structs, since
@@ -338,11 +324,10 @@ func (s) TestReportLoad_StreamCreation(t *testing.T) {
338324
}
339325

340326
// Send a response from the server with a small deadline.
341-
serverReportInterval := 50 * time.Millisecond
342327
lrsServer.LRSResponseChan <- &fakeserver.Response{
343328
Resp: &v3lrspb.LoadStatsResponse{
344329
SendAllClusters: true,
345-
LoadReportingInterval: &durationpb.Duration{Nanos: int32(serverReportInterval.Nanoseconds())}, // 50ms
330+
LoadReportingInterval: &durationpb.Duration{Nanos: 50000000}, // 50ms
346331
},
347332
}
348333

@@ -356,19 +341,6 @@ func (s) TestReportLoad_StreamCreation(t *testing.T) {
356341
t.Fatalf("Received load for %d clusters, want 1", l)
357342
}
358343

359-
// Verify that LoadReportInterval for the first load report is positive but
360-
// not excessively large.
361-
//
362-
// Max expected: serverReportInterval + tolerance (e.g., 500ms).
363-
firstLoadReportInterval := gotLoad[0].GetLoadReportInterval().AsDuration()
364-
if firstLoadReportInterval <= 0 {
365-
t.Fatalf("First LoadReportInterval for cluster1 = %v, want > 0", firstLoadReportInterval)
366-
}
367-
tolerance := 500 * time.Millisecond
368-
if firstLoadReportInterval > serverReportInterval+tolerance {
369-
t.Errorf("First LoadReportInterval for cluster1 is unexpectedly large: %v", firstLoadReportInterval)
370-
}
371-
372344
// This field is set by the client to indicate the actual time elapsed since
373345
// the last report was sent. We cannot deterministically compare this, and
374346
// we cannot use the cmpopts.IgnoreFields() option on proto structs, since
@@ -434,17 +406,6 @@ func (s) TestReportLoad_StreamCreation(t *testing.T) {
434406
if l := len(gotLoad); l != 1 {
435407
continue
436408
}
437-
// Verify that LoadReportInterval for the subsequent load reports is
438-
// positive but not excessively large.
439-
//
440-
// Max expected: serverReportInterval + tolerance (e.g., 500ms).
441-
loadReportInterval := gotLoad[0].GetLoadReportInterval().AsDuration()
442-
if loadReportInterval <= 0 {
443-
t.Fatalf("LoadReportInterval = %v, want > 0", firstLoadReportInterval)
444-
}
445-
if loadReportInterval > serverReportInterval+tolerance {
446-
t.Errorf("LoadReportInterval is unexpectedly large: %v", loadReportInterval)
447-
}
448409
gotLoad[0].LoadReportInterval = nil
449410
wantLoad := &v3endpointpb.ClusterStats{
450411
ClusterName: "cluster2",
@@ -551,11 +512,10 @@ func (s) TestReportLoad_StopWithContext(t *testing.T) {
551512
}
552513

553514
// Send a response from the server with a small deadline.
554-
serverReportInterval := 50 * time.Millisecond
555515
lrsServer.LRSResponseChan <- &fakeserver.Response{
556516
Resp: &v3lrspb.LoadStatsResponse{
557517
SendAllClusters: true,
558-
LoadReportingInterval: &durationpb.Duration{Nanos: int32(serverReportInterval.Nanoseconds())}, // 50ms
518+
LoadReportingInterval: &durationpb.Duration{Nanos: 50000000}, // 50ms
559519
},
560520
}
561521

@@ -569,19 +529,6 @@ func (s) TestReportLoad_StopWithContext(t *testing.T) {
569529
t.Fatalf("Received load for %d clusters, want 1", l)
570530
}
571531

572-
// Verify that LoadReportInterval for the first load report is positive but
573-
// not excessively large.
574-
//
575-
// Max expected: serverReportInterval + tolerance (e.g., 500ms).
576-
firstLoadReportInterval := gotLoad[0].GetLoadReportInterval().AsDuration()
577-
if firstLoadReportInterval <= 0 {
578-
t.Fatalf("First LoadReportInterval = %v, want > 0", firstLoadReportInterval)
579-
}
580-
tolerance := 500 * time.Millisecond
581-
if firstLoadReportInterval > serverReportInterval+tolerance {
582-
t.Errorf("First LoadReportInterval is unexpectedly large: %v", firstLoadReportInterval)
583-
}
584-
585532
// This field is set by the client to indicate the actual time elapsed since
586533
// the last report was sent. We cannot deterministically compare this, and
587534
// we cannot use the cmpopts.IgnoreFields() option on proto structs, since
@@ -644,17 +591,6 @@ func (s) TestReportLoad_StopWithContext(t *testing.T) {
644591
if l := len(gotLoad); l != 1 {
645592
continue
646593
}
647-
// Verify that LoadReportInterval for the subsequent load reports is
648-
// positive but not excessively large.
649-
//
650-
// Max expected: serverReportInterval + tolerance (e.g., 500ms).
651-
loadReportInterval := gotLoad[0].GetLoadReportInterval().AsDuration()
652-
if loadReportInterval <= 0 {
653-
t.Fatalf("LoadReportInterval = %v, want > 0", firstLoadReportInterval)
654-
}
655-
if loadReportInterval > serverReportInterval+tolerance {
656-
t.Errorf("LoadReportInterval is unexpectedly large: %v", loadReportInterval)
657-
}
658594
gotLoad[0].LoadReportInterval = nil
659595
wantLoad := &v3endpointpb.ClusterStats{
660596
ClusterName: "cluster2",

xds/internal/xdsclient/load/store.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import (
2525

2626
const negativeOneUInt64 = ^uint64(0)
2727

28+
// clockNow is used to get the current time. It can be overridden in tests.
29+
var clockNow = time.Now
30+
2831
// Store keeps the loads for multiple clusters and services to be reported via
2932
// LRS. It contains loads to reported to one LRS server. Create multiple stores
3033
// for multiple servers.
@@ -117,7 +120,7 @@ func (s *Store) PerCluster(clusterName, serviceName string) PerClusterReporter {
117120
p := &perClusterStore{
118121
cluster: clusterName,
119122
service: serviceName,
120-
lastLoadReportAt: time.Now(),
123+
lastLoadReportAt: clockNow(),
121124
}
122125
c[serviceName] = p
123126
return p
@@ -330,8 +333,8 @@ func (ls *perClusterStore) stats() *Data {
330333
})
331334

332335
ls.mu.Lock()
333-
sd.ReportInterval = time.Since(ls.lastLoadReportAt)
334-
ls.lastLoadReportAt = time.Now()
336+
sd.ReportInterval = clockNow().Sub(ls.lastLoadReportAt)
337+
ls.lastLoadReportAt = clockNow()
335338
ls.mu.Unlock()
336339

337340
if sd.TotalDrops == 0 && len(sd.Drops) == 0 && len(sd.LocalityStats) == 0 {

xds/internal/xdsclient/load/store_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"sort"
2323
"sync"
2424
"testing"
25+
"time"
2526

2627
"github.com/google/go-cmp/cmp"
2728
"github.com/google/go-cmp/cmp/cmpopts"
@@ -466,3 +467,49 @@ func TestStoreStatsEmptyDataNotReported(t *testing.T) {
466467
t.Errorf("store.stats() returned unexpected diff (-want +got):\n%s", diff)
467468
}
468469
}
470+
471+
// TestStoreReportInterval tests that the report interval is correctly
472+
// calculated between consecutive calls to Stats().
473+
func TestStoreReportInterval(t *testing.T) {
474+
originalClockNow := clockNow
475+
t.Cleanup(func() { clockNow = originalClockNow })
476+
477+
// Initial time for reporter creation
478+
currentTime := time.Now()
479+
clockNow = func() time.Time {
480+
return currentTime
481+
}
482+
483+
store := NewStore()
484+
reporter := store.PerCluster("test-cluster", "test-service")
485+
// To ensure Stats() returns non-nil data, report a dummy drop.
486+
reporter.CallDropped("dummy-category")
487+
488+
// First call to Stats() calculates the report interval from reporter
489+
// creation time.
490+
currentTime = currentTime.Add(5 * time.Second)
491+
stats1 := store.Stats(nil)
492+
493+
if stats1 == nil {
494+
t.Fatalf("stats1 is nil after reporting a drop")
495+
}
496+
wantInterval := 5 * time.Second
497+
if stats1[0].ReportInterval != wantInterval {
498+
t.Errorf("First call stats() = %v, want %v", stats1[0].ReportInterval, wantInterval)
499+
}
500+
501+
// Second call to Stats() calculates the report interval from last Stats()
502+
// call time.
503+
currentTime = currentTime.Add(10 * time.Second)
504+
// Report another dummy drop to ensure stats2 is not nil.
505+
reporter.CallDropped("dummy-category-2")
506+
stats2 := store.Stats(nil)
507+
508+
if stats2 == nil {
509+
t.Fatalf("stats2 is nil after reporting a drop")
510+
}
511+
wantInterval = 10 * time.Second
512+
if stats2[0].ReportInterval != wantInterval {
513+
t.Errorf("Second call stats() = %v, want %v", stats2[0].ReportInterval, wantInterval)
514+
}
515+
}

0 commit comments

Comments
 (0)