diff --git a/TODO.md b/TODO.md index 9636d71..0dad09f 100644 --- a/TODO.md +++ b/TODO.md @@ -32,7 +32,8 @@ P2: security: referer blacklist - 2026-10-04 shutdown stops cache eviction in progress (closes #102): `StartEviction` runs the eviction goroutine with its own context, which `StopEviction` cancels, so a pass in progress stops at its next database - call, file, row or eviction candidate instead of running to completion; + call, file, row or eviction candidate instead of running to completion, and + no pass starts after it, so a stop logs at most one warning; `StopEviction` takes a context and, when that context ends before the goroutine exits, stops waiting and returns its error; the handlers' stop hook passes fx's stop context, so an eviction still running when fx's stop diff --git a/internal/imgcache/eviction.go b/internal/imgcache/eviction.go index 7ec9273..29de851 100644 --- a/internal/imgcache/eviction.go +++ b/internal/imgcache/eviction.go @@ -117,8 +117,10 @@ func (c *Cache) EvictToLimit(ctx context.Context) error { // evictBatch fetches one batch of LRU candidates across variants and // source blobs and evicts them oldest-first until excessBytes are -// freed or the batch is exhausted. It returns the bytes freed. Once ctx -// is cancelled, it stops at the next candidate and returns ctx's error. +// freed or the batch is exhausted. It returns the bytes freed. A +// candidate that fails once ctx is cancelled (every one started after +// that fails at its first database call) ends the batch with ctx's +// error, without a warning. func (c *Cache) evictBatch(ctx context.Context, excessBytes int64) (int64, error) { candidates, err := c.evictionCandidates(ctx) if err != nil { @@ -132,12 +134,12 @@ func (c *Cache) evictBatch(ctx context.Context, excessBytes int64) (int64, error break } - if ctx.Err() != nil { - return freed, ctx.Err() - } - err := c.evictCandidate(ctx, candidate) if err != nil { + if ctx.Err() != nil { + return freed, ctx.Err() + } + c.log.Warn("failed to evict cache entry", "cache_key", candidate.cacheKey, "content_hash", candidate.contentHash, @@ -463,7 +465,7 @@ func (c *Cache) StopEviction(ctx context.Context) error { } // evictionLoop is the body of the background eviction goroutine. It -// returns when ctx is cancelled. +// returns when ctx is cancelled, and starts no pass after that. func (c *Cache) evictionLoop(ctx context.Context, interval time.Duration) { defer close(c.evictionDone) @@ -496,8 +498,13 @@ func (c *Cache) evictionLoop(ctx context.Context, interval time.Duration) { } // runEvictionPass runs one eviction pass, logging failures instead of -// propagating them (the loop must keep running). +// propagating them (the loop must keep running). It does nothing once +// ctx is cancelled. func (c *Cache) runEvictionPass(ctx context.Context) { + if ctx.Err() != nil { + return + } + err := c.EvictToLimit(ctx) if err != nil { c.log.Warn("cache eviction pass failed", "error", err) @@ -505,8 +512,13 @@ func (c *Cache) runEvictionPass(ctx context.Context) { } // runReconciliationPass runs one reconciliation pass, logging failures -// instead of propagating them (the loop must keep running). +// instead of propagating them (the loop must keep running). It does +// nothing once ctx is cancelled. func (c *Cache) runReconciliationPass(ctx context.Context) { + if ctx.Err() != nil { + return + } + err := c.reconcileAccounting(ctx) if err != nil { c.log.Warn("cache accounting reconciliation failed", "error", err)