Start no eviction pass after a stop, and end a pass quietly
check / check (push) Waiting to run

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
This commit is contained in:
2026-10-04 05:50:43 +00:00
parent 742b6fa4c1
commit 12b4415c57
2 changed files with 23 additions and 10 deletions
+2 -1
View File
@@ -32,7 +32,8 @@ P2: security: referer blacklist
- 2026-10-04 shutdown stops cache eviction in progress (closes #102): - 2026-10-04 shutdown stops cache eviction in progress (closes #102):
`StartEviction` runs the eviction goroutine with its own context, which `StartEviction` runs the eviction goroutine with its own context, which
`StopEviction` cancels, so a pass in progress stops at its next database `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 `StopEviction` takes a context and, when that context ends before the
goroutine exits, stops waiting and returns its error; the handlers' stop hook 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 passes fx's stop context, so an eviction still running when fx's stop
+21 -9
View File
@@ -117,8 +117,10 @@ func (c *Cache) EvictToLimit(ctx context.Context) error {
// evictBatch fetches one batch of LRU candidates across variants and // evictBatch fetches one batch of LRU candidates across variants and
// source blobs and evicts them oldest-first until excessBytes are // source blobs and evicts them oldest-first until excessBytes are
// freed or the batch is exhausted. It returns the bytes freed. Once ctx // freed or the batch is exhausted. It returns the bytes freed. A
// is cancelled, it stops at the next candidate and returns ctx's error. // 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) { func (c *Cache) evictBatch(ctx context.Context, excessBytes int64) (int64, error) {
candidates, err := c.evictionCandidates(ctx) candidates, err := c.evictionCandidates(ctx)
if err != nil { if err != nil {
@@ -132,12 +134,12 @@ func (c *Cache) evictBatch(ctx context.Context, excessBytes int64) (int64, error
break break
} }
if ctx.Err() != nil {
return freed, ctx.Err()
}
err := c.evictCandidate(ctx, candidate) err := c.evictCandidate(ctx, candidate)
if err != nil { if err != nil {
if ctx.Err() != nil {
return freed, ctx.Err()
}
c.log.Warn("failed to evict cache entry", c.log.Warn("failed to evict cache entry",
"cache_key", candidate.cacheKey, "cache_key", candidate.cacheKey,
"content_hash", candidate.contentHash, "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 // 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) { func (c *Cache) evictionLoop(ctx context.Context, interval time.Duration) {
defer close(c.evictionDone) 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 // 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) { func (c *Cache) runEvictionPass(ctx context.Context) {
if ctx.Err() != nil {
return
}
err := c.EvictToLimit(ctx) err := c.EvictToLimit(ctx)
if err != nil { if err != nil {
c.log.Warn("cache eviction pass failed", "error", err) 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 // 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) { func (c *Cache) runReconciliationPass(ctx context.Context) {
if ctx.Err() != nil {
return
}
err := c.reconcileAccounting(ctx) err := c.reconcileAccounting(ctx)
if err != nil { if err != nil {
c.log.Warn("cache accounting reconciliation failed", "error", err) c.log.Warn("cache accounting reconciliation failed", "error", err)