From b9d32985f69f93a56153611940e3bb51224df1fd Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Sun, 4 Oct 2026 05:50:43 +0000 Subject: [PATCH] Start no eviction pass after a stop, and end a pass quietly Both pass runners do nothing once the loop's context is cancelled, so a stop no longer starts an eviction pass after a cancelled reconciliation or on a pending write-pressure wakeup or tick. In evictBatch, a candidate that fails after cancellation ends the batch with the context's error instead of logging its own warning; this replaces the check before each candidate, since every candidate started after cancellation fails at its first database call. A stop now logs at most one warning. Model: opus-5-5 --- TODO.md | 3 ++- internal/imgcache/eviction.go | 30 +++++++++++++++++++++--------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/TODO.md b/TODO.md index efe2ffa..a15e955 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)