package dnsfilter import ( "context" "log/slog" "sync" "time" ) // Runner manages the background blocklist update goroutine. type Runner struct { mu sync.RWMutex manager *Manager cancel context.CancelFunc trigger chan struct{} running bool onUpdate func() // called after lists are downloaded and compiled } // NewRunner creates a new DNS filter update runner. func NewRunner(manager *Manager, onUpdate func()) *Runner { return &Runner{ manager: manager, trigger: make(chan struct{}, 1), onUpdate: onUpdate, } } // Start launches the background update goroutine. // Safe to call multiple times — cancels the previous run first. func (r *Runner) Start(parentCtx context.Context, intervalHours int) { r.mu.Lock() if r.cancel != nil { r.cancel() } ctx, cancel := context.WithCancel(parentCtx) r.cancel = cancel r.running = true r.mu.Unlock() if intervalHours <= 0 { intervalHours = 24 } go r.run(ctx, intervalHours) } // Stop cancels the running goroutine. func (r *Runner) Stop() { r.mu.Lock() defer r.mu.Unlock() if r.cancel != nil { r.cancel() r.cancel = nil } r.running = false } // IsRunning returns whether the runner is active. func (r *Runner) IsRunning() bool { r.mu.RLock() defer r.mu.RUnlock() return r.running } // Trigger requests an immediate update (non-blocking). func (r *Runner) Trigger() { select { case r.trigger <- struct{}{}: default: } } func (r *Runner) run(ctx context.Context, intervalHours int) { interval := time.Duration(intervalHours) * time.Hour slog.Info("dns-filter: runner started", "interval", interval) // Immediate update on start r.updateAndCompile(ctx) ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): r.mu.Lock() r.running = false r.mu.Unlock() slog.Info("dns-filter: runner stopped") return case <-ticker.C: r.updateAndCompile(ctx) case <-r.trigger: r.updateAndCompile(ctx) } } } func (r *Runner) updateAndCompile(ctx context.Context) { if err := r.manager.UpdateLists(ctx); err != nil { slog.Error("dns-filter: update failed", "error", err) } if _, err := r.manager.Compile(); err != nil { slog.Error("dns-filter: compile failed", "error", err) return } if r.onUpdate != nil { r.onUpdate() } }