package engine import ( "context" "log/slog" "time" "tangled.org/core/spindle/db" "tangled.org/core/spindle/storage" ) const ( cachePruneBatch = 100 cachePendingMaxAge = time.Hour ) func StartCachePruner(ctx context.Context, l *slog.Logger, index *db.DB, store storage.Storage, retention, interval time.Duration) { if store == nil || interval <= 0 { return } go func() { prune := func() { total := 0 for { n, err := PruneCaches(ctx, index, store, time.Now(), retention, cachePendingMaxAge, cachePruneBatch) total += n if err != nil { l.Warn("cache prune failed", "count", total, "err", err) return } if n < cachePruneBatch { if total > 0 { l.Info("pruned cache entries", "count", total) } return } } } prune() ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: prune() } } }() } func PruneCaches(ctx context.Context, index *db.DB, store storage.Storage, now time.Time, retention, pendingMaxAge time.Duration, limit int) (int, error) { readyBefore := time.Unix(0, 0) if retention > 0 { readyBefore = now.Add(-retention) } entries, err := index.ExpiredCacheEntries(ctx, readyBefore, now.Add(-pendingMaxAge), limit) if err != nil { return 0, err } pruned := 0 for _, entry := range entries { if entry.State != "deleting" { claimed, err := index.ClaimCacheEntry(ctx, entry.ID, entry.State, entry.LastUsedAt) if err != nil { return pruned, err } if !claimed { continue } } if err := store.Delete(ctx, entry.StorageKey); err != nil { if entry.State != "deleting" { _ = index.RestoreCacheEntryState(context.WithoutCancel(ctx), entry.ID, entry.State) } return pruned, err } if err := index.DeleteCacheEntry(ctx, entry.ID); err != nil { return pruned, err } pruned++ } return pruned, nil }