Files
minstrel/internal/sync/compactor.go
T
bvandeusenandClaude Opus 5.5 d191a28217 docs: server comments say what the code does, not how it came to be (#6277)
Comment blocks in cmd/minstrel and 30 smaller internal packages, rewritten
to rule 32's standard with each citation read in context: task, milestone
and rule ids go, and so do the stories they were attached to ("used to",
"Before #N", measurements from one install, quotes, pointers to Fabled
Librarian's records and to docs/superpowers files that are not in the
repo). Where a story wrapped a real constraint, the constraint stays as a
present-tense fact.

Comments that had gone stale are corrected:
- taste.Config: the tuned values are overlaid by internal/recsettings;
  it is no longer "a later follow-up".
- StorageConfig: the data dir holds art, caches and the stream secret,
  not only playlist covers.
- the Subsonic album list and getUser comments describe what they do
  today rather than a milestone that would change it.

Checked: the non-comment token stream of every changed file matches
HEAD, and gofmt -l is empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:15:05 -04:00

83 lines
2.6 KiB
Go

// Periodic compactor for the library_changes log. Every mutation in the
// library writes one row, so the table grows unbounded without a retention
// policy. This worker trims rows older than CompactorRetention on a daily
// tick.
//
// Why 30 days: clients keep a cursor and pull deltas from the last seen id.
// A client that checks in at least once per retention window never sees its
// cursor go stale; one offline longer than that gets the /api/library/sync
// 410 and does a full resync.
package sync
import (
"context"
"log/slog"
"time"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)
// CompactorRetention is the age past which library_changes rows are
// dropped. Mutable for tests; production runs should not change it
// without considering the corresponding client cursor-stale fallback.
var CompactorRetention = 30 * 24 * time.Hour
// CompactorInterval is how often the worker checks. Daily is enough —
// the table grows on the order of a few thousand rows/day for an
// active library, well below a size where intra-day trimming matters.
var CompactorInterval = 24 * time.Hour
// Compactor is a background worker that periodically deletes
// library_changes rows older than CompactorRetention.
type Compactor struct {
pool *pgxpool.Pool
logger *slog.Logger
interval time.Duration
}
// NewCompactor builds a compactor with production defaults.
func NewCompactor(pool *pgxpool.Pool, logger *slog.Logger) *Compactor {
return &Compactor{
pool: pool,
logger: logger,
interval: CompactorInterval,
}
}
// Run blocks until ctx is cancelled, ticking every c.interval.
// Fires one tick immediately on startup so a long-running process
// that hasn't been compacted in a while catches up.
func (c *Compactor) Run(ctx context.Context) {
c.tickOnce(ctx)
t := time.NewTicker(c.interval)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
c.tickOnce(ctx)
}
}
}
// tickOnce deletes rows older than CompactorRetention and logs the
// row count. Errors are logged at WARN; we never abort the loop on
// transient DB failures.
func (c *Compactor) tickOnce(ctx context.Context) {
cutoff := pgtype.Timestamptz{Time: time.Now().Add(-CompactorRetention), Valid: true}
rows, err := dbq.New(c.pool).DeleteOldLibraryChanges(ctx, cutoff)
if err != nil {
c.logger.Warn("library_changes compactor: delete failed", "err", err)
return
}
if rows > 0 {
c.logger.Info("library_changes compactor: trimmed rows",
"count", rows, "older_than", CompactorRetention)
}
}