Skip to content
quarry Artifact [3ed2771aaa]
Login

Artifact [3ed2771aaa]

Artifact 3ed2771aaa6632b96e78cc5089627ecb8a5aba5202d99a43e474e5aa21d70404:


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