136 lines
3.6 KiB
Go
136 lines
3.6 KiB
Go
package activitylog
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
const (
|
|
keyRetentionDays = "activitylog.retention_days"
|
|
defaultRetentionStr = "90"
|
|
defaultRetention = 90
|
|
cleanupBatchSize = 10000
|
|
)
|
|
|
|
// SettingsStore is satisfied by *catalog.ServerSettingsRepo.
|
|
type SettingsStore interface {
|
|
Get(ctx context.Context, key string) (string, error)
|
|
Set(ctx context.Context, key, value string) error
|
|
}
|
|
|
|
type PartitionManager interface {
|
|
EnsureFuturePartitions(ctx context.Context) error
|
|
DropExpiredPartitions(ctx context.Context, cutoff time.Time) ([]string, error)
|
|
DeleteExpiredRowsFromDefault(ctx context.Context, cutoff time.Time) (int64, error)
|
|
}
|
|
|
|
// SeedDefaults writes default activity log settings if not already set.
|
|
func SeedDefaults(ctx context.Context, store SettingsStore) error {
|
|
existing, err := store.Get(ctx, keyRetentionDays)
|
|
if err != nil {
|
|
return fmt.Errorf("seed activitylog defaults: %w", err)
|
|
}
|
|
if existing != "" {
|
|
return nil
|
|
}
|
|
return store.Set(ctx, keyRetentionDays, defaultRetentionStr)
|
|
}
|
|
|
|
// RunCleanup starts a background goroutine that runs batched deletes daily.
|
|
// Blocks until ctx is cancelled.
|
|
func RunCleanup(ctx context.Context, pool *pgxpool.Pool, store SettingsStore, pm PartitionManager) {
|
|
// Run once at startup, then every 24 hours
|
|
CleanupOnce(ctx, pool, store, pm)
|
|
|
|
ticker := time.NewTicker(24 * time.Hour)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
CleanupOnce(ctx, pool, store, pm)
|
|
}
|
|
}
|
|
}
|
|
|
|
// CleanupOnce runs a single activity log retention pass.
|
|
func CleanupOnce(ctx context.Context, pool *pgxpool.Pool, store SettingsStore, pm PartitionManager) int64 {
|
|
days := defaultRetention
|
|
if raw, err := store.Get(ctx, keyRetentionDays); err == nil && raw != "" {
|
|
if d := parseInt(raw); d > 0 {
|
|
days = d
|
|
}
|
|
}
|
|
|
|
cutoff := time.Now().UTC().AddDate(0, 0, -days)
|
|
if pm != nil {
|
|
if err := pm.EnsureFuturePartitions(ctx); err != nil {
|
|
slog.Warn("activitylog ensure future partitions error", "error", err)
|
|
}
|
|
|
|
partitionCleanupFailed := false
|
|
totalDeleted := int64(0)
|
|
if dropped, err := pm.DropExpiredPartitions(ctx, cutoff); err != nil {
|
|
slog.Warn("activitylog partition cleanup error", "error", err)
|
|
partitionCleanupFailed = true
|
|
} else if len(dropped) > 0 {
|
|
slog.Info("activitylog dropped expired partitions", "partitions", dropped)
|
|
}
|
|
|
|
if deleted, err := pm.DeleteExpiredRowsFromDefault(ctx, cutoff); err != nil {
|
|
slog.Warn("activitylog default partition cleanup error", "error", err)
|
|
partitionCleanupFailed = true
|
|
} else if deleted > 0 {
|
|
totalDeleted += deleted
|
|
slog.Info("activitylog default partition cleanup completed", "deleted", deleted, "retention_days", days)
|
|
}
|
|
|
|
if !partitionCleanupFailed {
|
|
return totalDeleted
|
|
}
|
|
slog.Warn("activitylog partition cleanup degraded, falling back to row deletes", "retention_days", days)
|
|
}
|
|
|
|
total := deleteExpiredRowsBefore(ctx, pool, cutoff)
|
|
if total > 0 {
|
|
slog.Info("activitylog cleanup completed", "deleted", total, "retention_days", days)
|
|
}
|
|
return total
|
|
}
|
|
|
|
func deleteExpiredRowsBefore(ctx context.Context, pool *pgxpool.Pool, cutoff time.Time) int64 {
|
|
total := int64(0)
|
|
for {
|
|
result, err := pool.Exec(ctx, `
|
|
DELETE FROM activity_log
|
|
WHERE id IN (
|
|
SELECT id FROM activity_log
|
|
WHERE timestamp < $1
|
|
LIMIT $2
|
|
)
|
|
`, cutoff, cleanupBatchSize)
|
|
if err != nil {
|
|
slog.Warn("activitylog cleanup error", "error", err)
|
|
return total
|
|
}
|
|
deleted := result.RowsAffected()
|
|
total += deleted
|
|
if deleted < int64(cleanupBatchSize) {
|
|
break
|
|
}
|
|
}
|
|
return total
|
|
}
|
|
|
|
func parseInt(s string) int {
|
|
var v int
|
|
fmt.Sscanf(s, "%d", &v)
|
|
return v
|
|
}
|