Files
coder/coderd/notifications/metrics_internal_test.go
T
dylanhuff-at-coder 0401ed3af5 fix(coderd/notifications): serialize pending updates gauge writes (#25495)
Fixes a race where concurrent notification dispatch goroutines could
overwrite `coderd_notifications_pending_updates` with an older
buffer-length snapshot. Pending update snapshots now serialize count
evaluation with the gauge write, and inhibited dispatch results refresh
the metric when buffered.
2026-05-29 11:02:13 -07:00

86 lines
2.3 KiB
Go

package notifications
import (
"sync"
"testing"
"github.com/prometheus/client_golang/prometheus"
promtest "github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/testutil"
)
func TestMetricsSetPendingUpdatesSerializesGaugeWrites(t *testing.T) {
t.Parallel()
realGauge := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "test_pending_updates",
Help: "test pending updates gauge",
})
blockingGauge := &pendingUpdatesBlockingGauge{
Gauge: realGauge,
blockValue: 3,
entered: make(chan struct{}),
release: make(chan struct{}),
}
metrics := &Metrics{
PendingUpdates: blockingGauge,
pendingUpdatesGauge: &pendingUpdatesGauge{gauge: blockingGauge},
}
success := make(chan dispatchResult, 4)
failure := make(chan dispatchResult, 4)
success <- dispatchResult{}
success <- dispatchResult{}
firstDone := make(chan struct{})
go func() {
defer close(firstDone)
failure <- dispatchResult{}
// The first writer observes total=3 and blocks inside Set(3)
// while still holding the pendingUpdatesGauge mutex.
metrics.pendingUpdatesGauge.set(func() int { return len(success) + len(failure) })
}()
testutil.TryReceive(testutil.Context(t, testutil.WaitShort), t, blockingGauge.entered)
// The main goroutine raises the real total to 4 before a second
// writer queues behind the locked gauge.
success <- dispatchResult{}
secondDone := make(chan struct{})
go func() {
defer close(secondDone)
// This count must be evaluated after release, while holding the
// mutex, so the final gauge value cannot regress to 3.
metrics.pendingUpdatesGauge.set(func() int { return len(success) + len(failure) })
}()
close(blockingGauge.release)
testutil.TryReceive(testutil.Context(t, testutil.WaitShort), t, firstDone)
testutil.TryReceive(testutil.Context(t, testutil.WaitShort), t, secondDone)
require.Equal(t, 4, len(success)+len(failure))
require.EqualValues(t, 4, promtest.ToFloat64(metrics.PendingUpdates))
}
type pendingUpdatesBlockingGauge struct {
prometheus.Gauge
blockValue float64
entered chan struct{}
release chan struct{}
once sync.Once
}
func (g *pendingUpdatesBlockingGauge) Set(value float64) {
if value == g.blockValue {
g.once.Do(func() {
close(g.entered)
<-g.release
})
}
g.Gauge.Set(value)
}