Count hits, misses, fetches and transforms as Prometheus counters (closes #311)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The five counts the cache_stats table held are now Prometheus counters in memory, in internal/imgcache/counters.go, on the default registry that /metrics serves. The Increment methods add to them, so counting makes no database call. cache_stats, Cache.Stats, Service.Stats, CacheStats and the pending counts are gone, with their tests. The background goroutine now writes only access times, and its names say so: StartPendingAccessTimeWrites, StopPendingAccessTimeWrites and pendingaccesstimes.go. The counters count for the whole process, so the tests that check how much they rose do not call t.Parallel. Two stop tests whose write under way was a hit count now use an access time. Model: opus-5-5
This commit is contained in:
+25
-93
@@ -8,8 +8,6 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/pixa/internal/imagerequest"
|
||||
@@ -81,33 +79,22 @@ type Cache struct {
|
||||
evictionDone chan struct{}
|
||||
evictionCancel context.CancelFunc
|
||||
|
||||
// The pending counts: hits, misses, upstream fetches and transforms
|
||||
// that requests counted, kept here until they are written by the
|
||||
// goroutine that StartPendingCountWrites starts, so that no request
|
||||
// waits for the database to count it. pendingVariantAccessTimes holds
|
||||
// the variants served from the cache, and pendingSourceAccessTimes the
|
||||
// cached sources a miss used, whose access time is still to be written
|
||||
// by that goroutine. As for eviction, the channels are created in
|
||||
// pendingVariantAccessTimes holds the variants served from the cache,
|
||||
// and pendingSourceAccessTimes the cached sources a miss used, whose
|
||||
// access time is still to be written by the goroutine that
|
||||
// StartPendingAccessTimeWrites starts, so that no request waits for the
|
||||
// database to record it. As for eviction, the channels are created in
|
||||
// NewCache, so that requests can hand over access times before that
|
||||
// goroutine starts; pendingCountsDone is closed when it returns.
|
||||
// pendingCountsCancel, set by StartPendingCountWrites, cancels its
|
||||
// context, which tells it to finish.
|
||||
pendingHits atomic.Int64
|
||||
pendingMisses atomic.Int64
|
||||
pendingUpstreamFetches atomic.Int64
|
||||
pendingUpstreamFetchBytes atomic.Int64
|
||||
pendingTransforms atomic.Int64
|
||||
// goroutine starts; pendingAccessTimesDone is closed when it returns.
|
||||
// pendingAccessTimesCancel, set by StartPendingAccessTimeWrites,
|
||||
// cancels its context, which tells it to finish.
|
||||
pendingVariantAccessTimes chan imagerequest.VariantKey
|
||||
pendingSourceAccessTimes chan imagerequest.SourceKey
|
||||
pendingCountsDone chan struct{}
|
||||
pendingCountsCancel context.CancelFunc
|
||||
|
||||
// Held through each write of the pending counts, and by
|
||||
// readHitAndMissCounts to read them
|
||||
pendingCountsWriteMutex sync.Mutex
|
||||
pendingAccessTimesDone chan struct{}
|
||||
pendingAccessTimesCancel context.CancelFunc
|
||||
|
||||
// reconciliationPageSize is the most rows one read of a content table
|
||||
// returns in the reconciliation pass and in Stats. newCache sets it to
|
||||
// returns in the reconciliation pass. newCache sets it to
|
||||
// defaultReconciliationPageSize; tests set it smaller.
|
||||
reconciliationPageSize int
|
||||
|
||||
@@ -143,7 +130,7 @@ func newCache(
|
||||
|
||||
pendingVariantAccessTimes: make(chan imagerequest.VariantKey, maxPendingAccessTimes),
|
||||
pendingSourceAccessTimes: make(chan imagerequest.SourceKey, maxPendingAccessTimes),
|
||||
pendingCountsDone: make(chan struct{}),
|
||||
pendingAccessTimesDone: make(chan struct{}),
|
||||
|
||||
reconciliationPageSize: defaultReconciliationPageSize,
|
||||
}
|
||||
@@ -373,51 +360,15 @@ func (c *Cache) CleanExpired(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Stats returns cache statistics.
|
||||
func (c *Cache) Stats(ctx context.Context) (*CacheStats, error) {
|
||||
var stats CacheStats
|
||||
|
||||
err := c.readHitAndMissCounts(ctx, &stats)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, fmt.Errorf("failed to get cache stats: %w", err)
|
||||
}
|
||||
|
||||
// Count and size the cached source images and processed variants from
|
||||
// their tables. A disabled cache holds none, whatever rows an earlier
|
||||
// run left.
|
||||
if !c.disabled {
|
||||
err = c.db.QueryRowContext(ctx, `
|
||||
SELECT (SELECT COUNT(*) FROM source_content)
|
||||
+ (SELECT COUNT(*) FROM variant_content)
|
||||
`).Scan(&stats.TotalItems)
|
||||
if err != nil {
|
||||
c.log.Warn("failed to count cache items for stats", "error", err)
|
||||
}
|
||||
|
||||
stats.TotalSizeBytes, err = c.sumContentSizeBytes(ctx)
|
||||
if err != nil {
|
||||
c.log.Warn("failed to sum cache size for stats", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Compute hit rate as a ratio
|
||||
if stats.HitCount+stats.MissCount > 0 {
|
||||
stats.HitRate = float64(stats.HitCount) / float64(stats.HitCount+stats.MissCount)
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// IncrementStats counts a cache hit or miss, and an upstream fetch that read
|
||||
// fetchBytes bytes, as IncrementUpstreamFetch does. Like the other Increment
|
||||
// methods, it only adds to the pending counts, which the goroutine
|
||||
// StartPendingCountWrites starts writes to the database, so it never waits
|
||||
// for the database or fails.
|
||||
// methods, it only adds to the Prometheus counters in memory (see
|
||||
// counters.go), so it never waits for the database or fails.
|
||||
func (c *Cache) IncrementStats(hit bool, fetchBytes int64) {
|
||||
if hit {
|
||||
c.pendingHits.Add(1)
|
||||
cacheHits.Inc()
|
||||
} else {
|
||||
c.pendingMisses.Add(1)
|
||||
cacheMisses.Inc()
|
||||
}
|
||||
|
||||
c.IncrementUpstreamFetch(fetchBytes)
|
||||
@@ -430,37 +381,18 @@ func (c *Cache) IncrementUpstreamFetch(fetchBytes int64) {
|
||||
return
|
||||
}
|
||||
|
||||
c.pendingUpstreamFetches.Add(1)
|
||||
c.pendingUpstreamFetchBytes.Add(fetchBytes)
|
||||
upstreamFetches.Inc()
|
||||
upstreamFetchBytes.Add(float64(fetchBytes))
|
||||
}
|
||||
|
||||
// IncrementTransformCount counts one image transcoded by the image processor.
|
||||
func (c *Cache) IncrementTransformCount() {
|
||||
c.pendingTransforms.Add(1)
|
||||
}
|
||||
|
||||
// readHitAndMissCounts sets the hit and miss counts of stats to those of the
|
||||
// cache_stats row plus the pending hits and misses (see
|
||||
// StartPendingCountWrites). It holds pendingCountsWriteMutex, so that no
|
||||
// write of the pending counts falls between the two reads.
|
||||
func (c *Cache) readHitAndMissCounts(ctx context.Context, stats *CacheStats) error {
|
||||
c.pendingCountsWriteMutex.Lock()
|
||||
defer c.pendingCountsWriteMutex.Unlock()
|
||||
|
||||
err := c.db.QueryRowContext(ctx, `
|
||||
SELECT hit_count, miss_count
|
||||
FROM cache_stats WHERE id = 1
|
||||
`).Scan(&stats.HitCount, &stats.MissCount)
|
||||
|
||||
stats.HitCount += c.pendingHits.Load()
|
||||
stats.MissCount += c.pendingMisses.Load()
|
||||
|
||||
return err
|
||||
transforms.Inc()
|
||||
}
|
||||
|
||||
// touchVariant hands the access time of a variant served from the cache to
|
||||
// the goroutine StartPendingCountWrites starts, which writes it in its next
|
||||
// batch. It never waits: while maxPendingAccessTimes access times of
|
||||
// the goroutine StartPendingAccessTimeWrites starts, which writes it in its
|
||||
// next batch. It never waits: while maxPendingAccessTimes access times of
|
||||
// variants wait to be written, this one is dropped, and the variant only
|
||||
// looks colder to eviction.
|
||||
func (c *Cache) touchVariant(cacheKey imagerequest.VariantKey) {
|
||||
@@ -471,10 +403,10 @@ func (c *Cache) touchVariant(cacheKey imagerequest.VariantKey) {
|
||||
}
|
||||
|
||||
// touchSourceContent hands the access time of a cached source a miss used to
|
||||
// the goroutine StartPendingCountWrites starts, which writes it in its next
|
||||
// batch. It never waits: while maxPendingAccessTimes access times of sources
|
||||
// wait to be written, this one is dropped, and the source only looks colder
|
||||
// to eviction.
|
||||
// the goroutine StartPendingAccessTimeWrites starts, which writes it in its
|
||||
// next batch. It never waits: while maxPendingAccessTimes access times of
|
||||
// sources wait to be written, this one is dropped, and the source only looks
|
||||
// colder to eviction.
|
||||
func (c *Cache) touchSourceContent(key imagerequest.SourceKey) {
|
||||
select {
|
||||
case c.pendingSourceAccessTimes <- key:
|
||||
|
||||
Reference in New Issue
Block a user