Files
bookhoard/internal/services/scheduler.go
T
john-okeefe 0551f17f0e refactor(scheduler): migrate from per-user to system-wide scan settings
Update scheduler to use system-wide settings instead of per-user:
- Change Database interface to use GetSystemSetting
- Remove GetScanSettings (per-user method)
- Update checkAndScheduleScans to read system settings
- Apply system-wide scan frequency to all libraries

Scheduler now respects global scan settings for all library scanning,
enabling consistent system-wide scan behavior.
2026-02-09 20:10:40 -05:00

237 lines
5.3 KiB
Go

package services
import (
"bookhoard/internal/database"
"context"
"fmt"
"log"
"strconv"
"sync"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)
type Scheduler struct {
worker *Worker
db Database
timers map[string]*time.Timer
mu sync.RWMutex
scanSettings map[string]ScanSetting
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
}
type Database interface {
GetSystemSetting(ctx context.Context, settingKey string) (string, error)
ListLibraries(ctx context.Context) ([]database.ListLibrariesRow, error)
GetLibraryFolders(ctx context.Context, libraryID pgtype.UUID) ([]database.LibraryFolders, error)
}
type Library struct {
ID pgtype.UUID
Name string
}
type LibraryFolder struct {
ID pgtype.UUID
LibraryID pgtype.UUID
FolderPath string
}
type ScanSetting struct {
UserID string
Enabled bool
Frequency int
}
func NewScheduler(worker *Worker, db Database) *Scheduler {
ctx, cancel := context.WithCancel(context.Background())
return &Scheduler{
worker: worker,
db: db,
timers: make(map[string]*time.Timer),
scanSettings: make(map[string]ScanSetting),
ctx: ctx,
cancel: cancel,
}
}
func (s *Scheduler) Start() {
s.wg.Add(1)
go func() {
defer s.wg.Done()
s.runSettingsChecker()
}()
}
func (s *Scheduler) Stop() {
s.cancel()
s.mu.Lock()
for _, timer := range s.timers {
if timer != nil {
timer.Stop()
}
}
s.timers = make(map[string]*time.Timer)
s.mu.Unlock()
s.wg.Wait()
}
func (s *Scheduler) runSettingsChecker() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.checkAndScheduleScans()
case <-s.ctx.Done():
return
}
}
}
func (s *Scheduler) checkAndScheduleScans() {
ctx, cancel := context.WithTimeout(s.ctx, 30*time.Second)
defer cancel()
libraries, err := s.db.ListLibraries(ctx)
if err != nil {
log.Printf("Error fetching libraries for scan scheduling: %v", err)
return
}
autoScanEnabledStr, err := s.db.GetSystemSetting(ctx, "auto_scan_enabled")
if err != nil {
log.Printf("Error getting auto_scan_enabled setting: %v", err)
return
}
autoScanEnabled, err := strconv.ParseBool(autoScanEnabledStr)
if err != nil {
log.Printf("Error parsing auto_scan_enabled: %v", err)
return
}
if !autoScanEnabled {
return
}
scanFrequencyStr, err := s.db.GetSystemSetting(ctx, "scan_frequency_minutes")
if err != nil {
log.Printf("Error getting scan_frequency_minutes setting: %v", err)
return
}
scanFrequency, err := strconv.Atoi(scanFrequencyStr)
if err != nil {
log.Printf("Error parsing scan_frequency_minutes: %v", err)
return
}
if scanFrequency < 15 {
return
}
for _, library := range libraries {
libraryIDStr := fmt.Sprintf("%x", library.ID.Bytes)
s.mu.Lock()
currentSetting, exists := s.scanSettings[libraryIDStr]
scanSetting := ScanSetting{
UserID: libraryIDStr,
Enabled: autoScanEnabled,
Frequency: scanFrequency,
}
s.scanSettings[libraryIDStr] = scanSetting
s.mu.Unlock()
if !exists || currentSetting.Frequency != scanSetting.Frequency {
s.scheduleLibraryScan(ctx, library.ID, libraryIDStr, scanSetting.Frequency)
}
}
}
func (s *Scheduler) scheduleLibraryScan(ctx context.Context, libraryID pgtype.UUID, userID string, frequencyMinutes int) {
userIDStr := fmt.Sprintf("%x", libraryID.Bytes)
s.mu.Lock()
defer s.mu.Unlock()
timerID := userIDStr + "-scan"
if existingTimer, exists := s.timers[timerID]; exists {
existingTimer.Stop()
}
duration := time.Duration(frequencyMinutes) * time.Minute
timer := time.AfterFunc(duration, func() {
s.triggerScheduledScan(ctx, libraryID, userIDStr)
s.scheduleLibraryScan(ctx, libraryID, userID, frequencyMinutes)
})
s.timers[timerID] = timer
libraryIDStr := fmt.Sprintf("%x", libraryID.Bytes)
log.Printf("Scheduled scan for library %s every %d minutes", libraryIDStr, frequencyMinutes)
}
func (s *Scheduler) triggerScheduledScan(ctx context.Context, libraryID pgtype.UUID, userID string) {
libraryIDStr := fmt.Sprintf("%x", libraryID.Bytes)
log.Printf("Triggering scheduled scan for library %s", libraryIDStr)
folders, err := s.db.GetLibraryFolders(ctx, libraryID)
if err != nil {
log.Printf("Error getting folders for library %s: %v", libraryIDStr, err)
return
}
if len(folders) == 0 {
log.Printf("No folders configured for library %s, skipping scan", libraryIDStr)
return
}
folderPaths := make([]string, len(folders))
for i, folder := range folders {
folderPaths[i] = folder.FolderPath
}
job := &Job{
ID: uuid.New().String(),
Type: JobTypeScan,
Params: map[string]interface{}{
"library_id": fmt.Sprintf("%x", libraryID.Bytes),
"folders": folderPaths,
"admin_id": userID,
"db": s.db,
},
Status: JobStatusPending,
Context: ctx,
}
if err := s.worker.EnqueueJob(job); err != nil {
log.Printf("Error enqueuing scan job: %v", err)
} else {
libraryIDStr := fmt.Sprintf("%x", libraryID.Bytes)
log.Printf("Enqueued scan job %s for library %s", job.ID, libraryIDStr)
}
}
func (s *Scheduler) UpdateScanSettings(userID string, enabled bool, frequencyMinutes int) {
s.mu.Lock()
defer s.mu.Unlock()
s.scanSettings[userID] = ScanSetting{
UserID: userID,
Enabled: enabled,
Frequency: frequencyMinutes,
}
}