package worker import ( "context" "log/slog" "sync" "time" "github.com/orchard9/rdev/internal/port" ) // OperationCleanup runs periodic cleanup of old operations. // Operations older than the retention period (default 30 days) are deleted. type OperationCleanup struct { repo port.OperationRepository logger *slog.Logger retentionPeriod time.Duration cleanupInterval time.Duration ctx context.Context cancel context.CancelFunc wg sync.WaitGroup } // OperationCleanupConfig holds configuration for operation cleanup. type OperationCleanupConfig struct { // RetentionPeriod is how long to keep operations. // Default: 30 days. RetentionPeriod time.Duration // CleanupInterval is how often to run cleanup. // Default: 1 hour. CleanupInterval time.Duration Logger *slog.Logger } // DefaultOperationCleanupConfig returns sensible defaults. func DefaultOperationCleanupConfig() *OperationCleanupConfig { return &OperationCleanupConfig{ RetentionPeriod: 30 * 24 * time.Hour, // 30 days CleanupInterval: 1 * time.Hour, Logger: slog.Default(), } } // NewOperationCleanup creates a new operation cleanup worker. func NewOperationCleanup(repo port.OperationRepository, cfg *OperationCleanupConfig) *OperationCleanup { if cfg == nil { cfg = DefaultOperationCleanupConfig() } ctx, cancel := context.WithCancel(context.Background()) return &OperationCleanup{ repo: repo, logger: cfg.Logger.With("component", "operation-cleanup"), retentionPeriod: cfg.RetentionPeriod, cleanupInterval: cfg.CleanupInterval, ctx: ctx, cancel: cancel, } } // Start begins the cleanup loop. func (c *OperationCleanup) Start() { c.logger.Info("operation cleanup started", "retention_period", c.retentionPeriod, "cleanup_interval", c.cleanupInterval, ) c.wg.Add(1) go c.cleanupLoop() } // Stop gracefully shuts down the cleanup worker. func (c *OperationCleanup) Stop() { c.logger.Info("operation cleanup stopping") c.cancel() c.wg.Wait() c.logger.Info("operation cleanup stopped") } // cleanupLoop runs periodic cleanup. func (c *OperationCleanup) cleanupLoop() { defer c.wg.Done() // Run immediately on start c.runCleanup() ticker := time.NewTicker(c.cleanupInterval) defer ticker.Stop() for { select { case <-c.ctx.Done(): return case <-ticker.C: c.runCleanup() } } } // runCleanup deletes operations older than the retention period. func (c *OperationCleanup) runCleanup() { ctx, cancel := context.WithTimeout(c.ctx, TimeoutMaintenance) defer cancel() cutoff := time.Now().Add(-c.retentionPeriod) deleted, err := c.repo.DeleteOlderThan(ctx, cutoff) if err != nil { c.logger.Error("failed to cleanup old operations", "error", err, "cutoff", cutoff, ) return } if deleted > 0 { c.logger.Info("cleaned up old operations", "deleted", deleted, "cutoff", cutoff, ) } }