Files
s3watch/internal/daemon/daemon.go
T
2026-09-16 10:27:58 -03:00

95 lines
1.9 KiB
Go

// Package daemon coordinates periodic sync cycles and change hooks.
package daemon
import (
"context"
"fmt"
"log/slog"
"time"
"s3watch/internal/syncer"
)
// Config controls daemon scheduling behavior.
type Config struct {
Interval time.Duration
Once bool
}
// Syncer synchronizes remote state into local state.
type Syncer interface {
Sync(context.Context) (syncer.Result, error)
}
// HookRunner runs after a sync cycle applies changes.
type HookRunner interface {
Run(context.Context, syncer.Result) error
}
// Run executes sync cycles until the context is canceled.
func Run(ctx context.Context, cfg Config, syncService Syncer, runner HookRunner, logger *slog.Logger) error {
if cfg.Interval <= 0 {
return fmt.Errorf("interval must be greater than zero")
}
if syncService == nil {
return fmt.Errorf("syncer is required")
}
if logger == nil {
logger = slog.Default()
}
for {
if err := runCycle(ctx, syncService, runner, logger, cfg.Once); err != nil {
if cfg.Once {
return err
}
logger.Error("sync cycle failed", "error", err)
}
if cfg.Once {
return nil
}
timer := time.NewTimer(cfg.Interval)
select {
case <-ctx.Done():
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
return nil
case <-timer.C:
}
}
}
func runCycle(ctx context.Context, syncService Syncer, runner HookRunner, logger *slog.Logger, strict bool) error {
result, err := syncService.Sync(ctx)
if err != nil {
return fmt.Errorf("syncing: %w", err)
}
logger.Info("sync cycle completed",
"downloaded", result.Downloaded,
"uploaded", result.Uploaded,
"updated", result.Updated,
"deleted", result.Deleted,
"unchanged", result.Unchanged,
)
if !result.Changed() || runner == nil {
return nil
}
if err := runner.Run(ctx, result); err != nil {
if strict {
return fmt.Errorf("running hook: %w", err)
}
logger.Error("hook failed", "error", err)
}
return nil
}