mirror of
https://github.com/coder/coder.git
synced 2026-09-22 21:22:17 +08:00
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.
86 lines
2.3 KiB
Go
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)
|
|
}
|