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")
}