package obs
import (
"errors"
"sync/atomic"
"testing"
"time"
)
// A traffic page is computed once a minute per range and filters: fresh
// answers come from the memo, stale ones come back at once while one
// refresh runs behind them, a past window is never memoised, and a
// failed refresh keeps serving the last answer.
func TestTrafficIsMemoisedAndRefreshedBehind(t *testing.T) {
var m trafficMemo
var calls atomic.Int32
compute := func(q TrafficQuery) (*Traffic, error) {
calls.Add(1)
return &Traffic{Range: q.Range}, nil
}
q := TrafficQuery{Range: TrafficRange{Name: "7d"}, Filters: map[string]string{"repo": "quarry"}}
now := time.Now()
if _, err := m.get(q, now, compute); err != nil || calls.Load() != 1 {
t.Fatalf("FAIL: first read %v, %d computes", err, calls.Load())
}
m.get(q, now.Add(20*time.Second), compute)
if calls.Load() != 1 {
t.Errorf("FAIL: a fresh read computed again (%d)", calls.Load())
}
if got, _ := m.get(q, now.Add(2*time.Minute), compute); got == nil {
t.Error("FAIL: a stale read did not answer at once")
}
for i := 0; i < 50 && calls.Load() < 2; i++ {
time.Sleep(10 * time.Millisecond)
}
if calls.Load() != 2 {
t.Errorf("FAIL: a stale read started %d refreshes, want one", calls.Load()-1)
}
other := TrafficQuery{Range: TrafficRange{Name: "7d"}}
m.get(other, now, compute)
if calls.Load() != 3 {
t.Error("FAIL: a different filter set shared an answer")
}
past := TrafficQuery{Range: TrafficRange{Name: "7d"}, Now: now.Add(-time.Hour)}
m.get(past, now, compute)
m.get(past, now, compute)
if calls.Load() != 5 {
t.Error("FAIL: a question about the past was memoised")
}
failing := func(TrafficQuery) (*Traffic, error) { return nil, errors.New("down") }
var f trafficMemo
f.get(q, now, compute)
if got, err := f.get(q, now.Add(2*time.Minute), failing); got == nil || err != nil {
t.Error("FAIL: a failing refresh did not keep serving the last answer")
}
t.Log("ok")
}
type warmSink struct {
Sink
calls atomic.Int32
}
func (s *warmSink) Traffic(q TrafficQuery) (*Traffic, error) {
s.calls.Add(1)
return &Traffic{Range: q.Range}, nil
}
// WarmTraffic fills the memo for every named range at once, so the
// first view of each is answered from it.
func TestWarmTrafficFillsEveryRange(t *testing.T) {
var r Recorder
ws := &warmSink{}
r.sink = ws
stop := make(chan struct{})
go r.WarmTraffic(stop)
for i := 0; i < 100 && int(ws.calls.Load()) < len(TrafficRanges); i++ {
time.Sleep(10 * time.Millisecond)
}
close(stop)
before := ws.calls.Load()
for _, rg := range TrafficRanges {
if _, err := r.Traffic(TrafficQuery{Range: rg}); err != nil {
t.Fatal(err)
}
}
if ws.calls.Load() != before || int(before) < len(TrafficRanges) {
t.Errorf("FAIL: warming computed %d ranges and the reads computed %d more", before, ws.calls.Load()-before)
}
t.Log("ok")
}