Delete expired negative cache rows in batches

CleanExpired deletes at most 1000 expired rows per statement and starts
no further statement once its context ends, so a request on the one
database connection waits behind one batch at most.

Model: opus-5-5
This commit is contained in:
2026-10-15 07:45:11 +00:00
parent 01434ecf3b
commit 98bce3bb1a
2 changed files with 60 additions and 21 deletions
+49 -12
View File
@@ -32,6 +32,12 @@ const httpStatusOK = 200
// variant not among them is served as before, reading its .meta file.
const metaCacheSize = 10000
// cleanExpiredBatchSize is the most expired negative cache rows one
// statement of CleanExpired deletes. Each batch is a statement of its
// own, so a request waits behind one batch at most, however many rows
// have expired.
const cleanExpiredBatchSize = 1000
// CacheConfig holds cache configuration.
type CacheConfig struct {
StateDir string
@@ -483,20 +489,27 @@ func (c *Cache) GetSourceContent(
return c.srcContent.LoadWithSize(contentHash)
}
// CleanExpired removes the negative cache rows past their expiry. The
// eviction goroutine calls it at startup and on every periodic tick, so
// a row stays at most NegativeTTL plus one eviction interval, whether or
// not its URL is requested again.
// CleanExpired removes the negative cache rows past their expiry, one
// batch per statement, until none is left. Once ctx ends it starts no
// further batch and returns ctx's error. The eviction goroutine calls it
// at startup and on every periodic tick, so a row stays about
// NegativeTTL plus one eviction interval, whether or not its URL is
// requested again.
func (c *Cache) CleanExpired(ctx context.Context) error {
// Clean expired negative cache entries
_, err := c.db.ExecContext(ctx, `
DELETE FROM negative_cache WHERE expires_at < CURRENT_TIMESTAMP
`)
if err != nil {
return fmt.Errorf("failed to clean negative cache: %w", err)
}
for {
if ctx.Err() != nil {
return ctx.Err()
}
return nil
deleted, err := c.cleanExpiredBatch(ctx)
if err != nil {
return err
}
if deleted < cleanExpiredBatchSize {
return nil
}
}
}
// Stats returns cache statistics.
@@ -760,3 +773,27 @@ func (c *Cache) checkNegativeCache(
return true, nil
}
// cleanExpiredBatch deletes up to cleanExpiredBatchSize negative cache
// rows past their expiry in one statement, and returns how many it
// deleted. DELETE ... LIMIT needs a SQLite build option, so the subquery
// picks the rows.
func (c *Cache) cleanExpiredBatch(ctx context.Context) (int64, error) {
result, err := c.db.ExecContext(ctx, `
DELETE FROM negative_cache WHERE id IN (
SELECT id FROM negative_cache
WHERE expires_at < CURRENT_TIMESTAMP
LIMIT ?
)
`, cleanExpiredBatchSize)
if err != nil {
return 0, fmt.Errorf("failed to clean negative cache: %w", err)
}
deleted, err := result.RowsAffected()
if err != nil {
return 0, fmt.Errorf("failed to count cleaned negative cache rows: %w", err)
}
return deleted, nil
}