package notifications import ( "context" "log/slog" "time" "github.com/jackc/pgx/v5/pgtype" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" ) const ( // ReadRetention is how long a read notification is kept. An inbox is a // record of what happened lately, not an archive. ReadRetention = 90 * 24 * time.Hour // AnyRetention bounds an inbox nobody opens: past it, even unread rows go. AnyRetention = 365 * 24 * time.Hour // retentionInterval matches the library_changes compactor's daily tick. retentionInterval = 24 * time.Hour ) // Retention trims old notifications on a daily tick, in the shape of the // library_changes compactor (internal/sync/compactor.go). type Retention struct { db dbq.DBTX logger *slog.Logger } // NewRetention returns a Retention trimming through db. func NewRetention(db dbq.DBTX, logger *slog.Logger) *Retention { return &Retention{db: db, logger: logger} } // Run blocks until ctx is cancelled. It trims once at startup, so a process // that has been down a while catches up, then daily. func (r *Retention) Run(ctx context.Context) { r.TrimOnce(ctx, time.Now()) t := time.NewTicker(retentionInterval) defer t.Stop() for { select { case <-ctx.Done(): return case now := <-t.C: r.TrimOnce(ctx, now) } } } // TrimOnce deletes what has outlived its retention as of now. Errors are // logged, never fatal: the next tick tries again. func (r *Retention) TrimOnce(ctx context.Context, now time.Time) int64 { n, err := dbq.New(r.db).TrimNotifications(ctx, dbq.TrimNotificationsParams{ ReadCutoff: pgtype.Timestamptz{Time: now.Add(-ReadRetention), Valid: true}, AnyCutoff: pgtype.Timestamptz{Time: now.Add(-AnyRetention), Valid: true}, }) if err != nil { r.logger.Warn("notifications retention: trim failed", "err", err) return 0 } if n > 0 { r.logger.Info("notifications retention: trimmed rows", "count", n) } return n }