Skip to content

Commit 645da33

Browse files
test: fix TestDescCacheTimestampUpdate flake (#20975)
## Problem `TestDescCacheTimestampUpdate` was flaky on Windows CI because `time.Now()` has ~15.6ms resolution, causing consecutive calls to return identical timestamps. ## Solution Inject `quartz.Clock` into `MetricsAggregator` using an options pattern, making the test deterministic by using a mock clock with explicit time advancement. ### Changes - Add `clock quartz.Clock` field to `MetricsAggregator` struct - Add `WithClock()` option for dependency injection - Replace all `time.Now()` calls with `ma.clock.Now()` - Update test to use mock clock with `mClock.Advance(time.Second)` --- This PR was fully generated by [`mux`](https://github.com/coder/mux) using Claude Opus 4.5, and reviewed by me. Closes coder/internal#1146
1 parent ab4366f commit 645da33

File tree

2 files changed

+30
-9
lines changed

2 files changed

+30
-9
lines changed

coderd/prometheusmetrics/aggregator.go

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
agentproto "github.com/coder/coder/v2/agent/proto"
1717
"github.com/coder/coder/v2/coderd/agentmetrics"
1818
"github.com/coder/coder/v2/coderd/pproflabel"
19+
20+
"github.com/coder/quartz"
1921
)
2022

2123
const (
@@ -47,6 +49,7 @@ type MetricsAggregator struct {
4749

4850
log slog.Logger
4951
metricsCleanupInterval time.Duration
52+
clock quartz.Clock
5053

5154
collectCh chan (chan []prometheus.Metric)
5255
updateCh chan updateRequest
@@ -151,7 +154,7 @@ func (am *annotatedMetric) shallowCopy() annotatedMetric {
151154
}
152155
}
153156

154-
func NewMetricsAggregator(logger slog.Logger, registerer prometheus.Registerer, duration time.Duration, aggregateByLabels []string) (*MetricsAggregator, error) {
157+
func NewMetricsAggregator(logger slog.Logger, registerer prometheus.Registerer, duration time.Duration, aggregateByLabels []string, options ...func(*MetricsAggregator)) (*MetricsAggregator, error) {
155158
metricsCleanupInterval := defaultMetricsCleanupInterval
156159
if duration > 0 {
157160
metricsCleanupInterval = duration
@@ -192,9 +195,10 @@ func NewMetricsAggregator(logger slog.Logger, registerer prometheus.Registerer,
192195
return nil, err
193196
}
194197

195-
return &MetricsAggregator{
198+
ma := &MetricsAggregator{
196199
log: logger.Named(loggerName),
197200
metricsCleanupInterval: metricsCleanupInterval,
201+
clock: quartz.NewReal(),
198202

199203
store: map[metricKey]annotatedMetric{},
200204

@@ -206,7 +210,19 @@ func NewMetricsAggregator(logger slog.Logger, registerer prometheus.Registerer,
206210
cleanupHistogram: cleanupHistogram,
207211

208212
aggregateByLabels: aggregateByLabels,
209-
}, nil
213+
}
214+
215+
for _, option := range options {
216+
option(ma)
217+
}
218+
219+
return ma, nil
220+
}
221+
222+
func WithClock(clock quartz.Clock) func(*MetricsAggregator) {
223+
return func(ma *MetricsAggregator) {
224+
ma.clock = clock
225+
}
210226
}
211227

212228
// labelAggregator is used to control cardinality of collected Prometheus metrics by pre-aggregating series based on given labels.
@@ -349,7 +365,7 @@ func (ma *MetricsAggregator) Run(ctx context.Context) func() {
349365
ma.log.Debug(ctx, "clean expired metrics")
350366

351367
timer := prometheus.NewTimer(ma.cleanupHistogram)
352-
now := time.Now()
368+
now := ma.clock.Now()
353369

354370
for key, val := range ma.store {
355371
if now.After(val.expiryDate) {
@@ -407,7 +423,7 @@ func (ma *MetricsAggregator) getOrCreateDesc(name string, help string, baseLabel
407423
}
408424
key := cacheKeyForDesc(name, baseLabelNames, extraLabels)
409425
if d, ok := ma.descCache[key]; ok {
410-
d.lastUsed = time.Now()
426+
d.lastUsed = ma.clock.Now()
411427
ma.descCache[key] = d
412428
return d.desc
413429
}
@@ -419,7 +435,7 @@ func (ma *MetricsAggregator) getOrCreateDesc(name string, help string, baseLabel
419435
labels[nBase+i] = l.Name
420436
}
421437
d := prometheus.NewDesc(name, help, labels, nil)
422-
ma.descCache[key] = descCacheEntry{d, time.Now()}
438+
ma.descCache[key] = descCacheEntry{d, ma.clock.Now()}
423439
return d
424440
}
425441

@@ -497,7 +513,7 @@ func (ma *MetricsAggregator) Update(ctx context.Context, labels AgentMetricLabel
497513
templateName: labels.TemplateName,
498514
metrics: metrics,
499515

500-
timestamp: time.Now(),
516+
timestamp: ma.clock.Now(),
501517
}:
502518
case <-ctx.Done():
503519
ma.log.Debug(ctx, "update request is canceled")
@@ -508,7 +524,7 @@ func (ma *MetricsAggregator) Update(ctx context.Context, labels AgentMetricLabel
508524

509525
// Move to a function for testability
510526
func (ma *MetricsAggregator) cleanupDescCache() {
511-
now := time.Now()
527+
now := ma.clock.Now()
512528
for key, entry := range ma.descCache {
513529
if now.Sub(entry.lastUsed) > ma.metricsCleanupInterval {
514530
delete(ma.descCache, key)

coderd/prometheusmetrics/aggregator_internal_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
agentproto "github.com/coder/coder/v2/agent/proto"
1212
"github.com/coder/coder/v2/coderd/agentmetrics"
1313
"github.com/coder/coder/v2/testutil"
14+
"github.com/coder/quartz"
1415
)
1516

1617
func TestDescCache_DescExpire(t *testing.T) {
@@ -62,8 +63,9 @@ func TestDescCache_DescExpire(t *testing.T) {
6263
func TestDescCacheTimestampUpdate(t *testing.T) {
6364
t.Parallel()
6465

66+
mClock := quartz.NewMock(t)
6567
registry := prometheus.NewRegistry()
66-
ma, err := NewMetricsAggregator(slogtest.Make(t, nil), registry, time.Hour, nil)
68+
ma, err := NewMetricsAggregator(slogtest.Make(t, nil), registry, time.Hour, nil, WithClock(mClock))
6769
require.NoError(t, err)
6870

6971
baseLabelNames := []string{"label1", "label2"}
@@ -78,6 +80,9 @@ func TestDescCacheTimestampUpdate(t *testing.T) {
7880
initialEntry := ma.descCache[key]
7981
initialTime := initialEntry.lastUsed
8082

83+
// Advance the mock clock to ensure a different timestamp
84+
mClock.Advance(time.Second)
85+
8186
desc2 := ma.getOrCreateDesc("test_metric", "help text", baseLabelNames, extraLabels)
8287
require.NotNil(t, desc2)
8388

0 commit comments

Comments
 (0)