Skip to content
quarry Artifact [21f56c2e40]
Login

Artifact [21f56c2e40]

Artifact 21f56c2e40b2068d62cc1b9297e2289773113d1cdc18c7cff2fceb76ed411d0f:


package obs

import (
	"sort"
	"strings"
	"sync"
	"time"
)

// trafficFresh is how long a computed traffic page is served as it is,
// the live view's own tick (web's trafficEvery), so the stream still
// moves every time it asks;
// trafficStale is how long past that it is still served while a
// refresh runs behind it. The page took 0.8-1.5 s to compute from the
// log on the forge, against a 250 ms budget, and its numbers move by the
// minute, not by the request.
const (
	trafficFresh = 30 * time.Second
	trafficStale = 10 * time.Minute
)

type trafficEntry struct {
	t       *Traffic
	at      time.Time
	pending bool
}

// trafficMemo keeps the last answer per range and filter set.
type trafficMemo struct {
	mu sync.Mutex
	m  map[string]*trafficEntry
}

func trafficKey(q TrafficQuery) string {
	keys := make([]string, 0, len(q.Filters))
	for k, v := range q.Filters {
		if v != "" {
			keys = append(keys, k+"="+v)
		}
	}
	sort.Strings(keys)
	return q.Range.Name + "|" + strings.Join(keys, "&")
}

// get answers q from the memo when it can: fresh as it is, stale while
// one refresh runs behind it. compute is the uncached read. A query that
// names its own Now is a question about the past, and is never memoised.
func (m *trafficMemo) get(q TrafficQuery, now time.Time, compute func(TrafficQuery) (*Traffic, error)) (*Traffic, error) {
	if !q.Now.IsZero() {
		return compute(q)
	}
	key := trafficKey(q)
	m.mu.Lock()
	if m.m == nil {
		m.m = map[string]*trafficEntry{}
	}
	e := m.m[key]
	switch {
	case e != nil && now.Sub(e.at) < trafficFresh:
		t := e.t
		m.mu.Unlock()
		return t, nil
	case e != nil && now.Sub(e.at) < trafficFresh+trafficStale:
		t := e.t
		if !e.pending {
			e.pending = true
			go m.refresh(key, q, compute)
		}
		m.mu.Unlock()
		return t, nil
	}
	m.mu.Unlock()
	t, err := compute(q)
	if err != nil {
		return nil, err
	}
	m.mu.Lock()
	m.m[key] = &trafficEntry{t: t, at: time.Now()}
	m.mu.Unlock()
	return t, nil
}

func (m *trafficMemo) refresh(key string, q TrafficQuery, compute func(TrafficQuery) (*Traffic, error)) {
	t, err := compute(q)
	m.mu.Lock()
	defer m.mu.Unlock()
	e := m.m[key]
	if err != nil {
		if e != nil {
			e.pending = false
		}
		return
	}
	m.m[key] = &trafficEntry{t: t, at: time.Now()}
}