package maintenance import ( "context" "log" "os" "path/filepath" "sync" "time" "crypto-miner-server/internal/db" ) // retentionTickInterval is the delay between scheduled retention passes (overridable in tests). var retentionTickInterval = 6 * time.Hour // runRetentionFn is the work function invoked by StartRetentionJobs (overridable in tests). var runRetentionFn = runRetention var ( retentionMu sync.Mutex retentionCancel context.CancelFunc ) // StartRetentionJobs purges old stats and build artifacts on an interval. func StartRetentionJobs(database *db.Database, dataDir string, statsHours, buildDays int) { if statsHours <= 0 && buildDays <= 0 { return } ctx, cancel := context.WithCancel(context.Background()) retentionMu.Lock() retentionCancel = cancel retentionMu.Unlock() go func() { runRetentionFn(database, dataDir, statsHours, buildDays) ticker := time.NewTicker(retentionTickInterval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: runRetentionFn(database, dataDir, statsHours, buildDays) } } }() } // StopRetentionJobs stops the background retention loop started by StartRetentionJobs. func StopRetentionJobs() { retentionMu.Lock() cancel := retentionCancel retentionCancel = nil retentionMu.Unlock() if cancel != nil { cancel() } } func runRetention(database *db.Database, dataDir string, statsHours, buildDays int) { if statsHours > 0 { cutoff := time.Now().Add(-time.Duration(statsHours) * time.Hour) n, err := database.PurgeHashrateSamplesBefore(cutoff) if err != nil { log.Printf("[Retention] hashrate purge failed: %v", err) } else if n > 0 { log.Printf("[Retention] purged %d hashrate samples older than %dh", n, statsHours) } } if buildDays > 0 { cutoff := time.Now().Add(-time.Duration(buildDays) * 24 * time.Hour) builds, err := database.ListBuildsOlderThan(cutoff) if err != nil { log.Printf("[Retention] build list failed: %v", err) return } for _, b := range builds { // Delete the DB record first. If this fails the build directory is // still intact so the next retention pass can retry cleanly. if err := database.DeleteBuild(b.ID); err != nil { log.Printf("[Retention] delete build %s from DB: %v — skipping file removal", b.ID, err) continue } // Remove files only after the DB row is gone. var dir string if b.FilePath != "" { dir = filepath.Dir(b.FilePath) } else { dir = filepath.Join(dataDir, "builds", b.ID) } if err := os.RemoveAll(dir); err != nil { log.Printf("[Retention] remove build dir %s: %v", dir, err) } else { log.Printf("[Retention] removed build %s (%s)", b.ID, b.WorkerName) } } } }