Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 15:49:16 -05:00
pkg-proxy/internal/server/ecosystem_cache.go
Abhinav Gautam a97b690858
Separate artifact and metadata cache metrics on /metrics and the analytics page (#463)
* Separate artifact and metadata cache metrics on /metrics and the analytics page

* Serve metadata cache stats from a shared snapshot and split Grafana cache panels by kind
2026-10-20 11:25:33 +01:00

122 lines
3.8 KiB
Go

package server
import (
"sync"
"time"
"github.com/git-pkgs/proxy/internal/database"
)
// ecosystemStatsTTL bounds how stale a served snapshot can be. The metrics
// refresh loop runs on the same cadence, so a scrape and a page load a moment
// apart report the same figures rather than two slightly different ones.
const ecosystemStatsTTL = time.Minute
// ecosystemStatsErrorTTL is the retry interval after a failed read. It is short
// so a recovered database is picked up quickly, but not zero, so a database
// that is down is not queried once per request.
const ecosystemStatsErrorTTL = 5 * time.Second
// statsCache memoizes one of the grouped statistics queries behind the
// analytics page and the metrics gauges.
//
// Each query is a grouped aggregation over a whole table, which is fine once a
// minute for the metrics gauges but not once per page load: the UI is
// unauthenticated, so without this a client reloading /ui/analytics would keep
// the database busy for as long as it cared to. The page and the gauges read
// the same snapshot, so they never disagree.
//
// L selects the query. It is a zero-size type rather than a stored function so
// the zero value stays usable: a Server assembled as a struct literal -- as
// tests do -- needs no constructor and cannot end up with a nil cache.
type statsCache[E any, L statsLoader[E]] struct {
ttl time.Duration
mu sync.Mutex
stats []E
err error
lastAt time.Time
// goodAt is when stats was last read successfully, which is not lastAt
// once reads start failing and the retained snapshot is served on.
goodAt time.Time
}
// statsLoader runs the query a statsCache memoizes, returning one row per
// ecosystem.
type statsLoader[E any] interface {
load(db *database.DB) ([]E, error)
}
type ecosystemStatsLoader struct{}
func (ecosystemStatsLoader) load(db *database.DB) ([]database.EcosystemStats, error) {
return db.GetEcosystemStats()
}
type metadataStatsLoader struct{}
func (metadataStatsLoader) load(db *database.DB) ([]database.MetadataCacheStats, error) {
return db.GetMetadataCacheStats()
}
// ecosystemStatsCache memoizes GetEcosystemStats.
type ecosystemStatsCache = statsCache[database.EcosystemStats, ecosystemStatsLoader]
// metadataStatsCache memoizes GetMetadataCacheStats.
type metadataStatsCache = statsCache[database.MetadataCacheStats, metadataStatsLoader]
// SnapshotAt reports when the rows currently held were last read successfully.
// The zero time means no read has ever succeeded.
func (c *statsCache[E, L]) SnapshotAt() time.Time {
c.mu.Lock()
defer c.mu.Unlock()
return c.goodAt
}
func (c *statsCache[E, L]) interval() time.Duration {
if c.err != nil {
return ecosystemStatsErrorTTL
}
if c.ttl <= 0 {
return ecosystemStatsTTL
}
return c.ttl
}
// Get returns the cached rows, refreshing them from db when they have aged out.
//
// A failed read returns the last good snapshot alongside the error, so a
// caller can choose to serve stale figures rather than fail outright.
func (c *statsCache[E, L]) Get(db *database.DB) ([]E, error) {
c.mu.Lock()
defer c.mu.Unlock()
if !c.lastAt.IsZero() && time.Since(c.lastAt) < c.interval() {
return c.stats, c.err
}
return c.refreshLocked(db)
}
// Refresh forces a read, bypassing the TTL. The metrics loop uses it so its own
// tick is never served a snapshot that is about to expire.
func (c *statsCache[E, L]) Refresh(db *database.DB) ([]E, error) {
c.mu.Lock()
defer c.mu.Unlock()
return c.refreshLocked(db)
}
func (c *statsCache[E, L]) refreshLocked(db *database.DB) ([]E, error) {
var loader L
stats, err := loader.load(db)
c.lastAt = time.Now()
c.err = err
if err != nil {
// Keep the last good snapshot: a transient failure should not erase
// figures the caller could still usefully show.
return c.stats, err
}
c.stats = stats
c.goodAt = c.lastAt
return c.stats, nil
}