primeiro commit
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"s3watch/internal/syncer"
|
||||
)
|
||||
|
||||
func TestRunOnceRunsHookOnlyWhenChanged(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
result syncer.Result
|
||||
wantCalls int
|
||||
}{
|
||||
{
|
||||
name: "changed",
|
||||
result: syncer.Result{Downloaded: 1},
|
||||
wantCalls: 1,
|
||||
},
|
||||
{
|
||||
name: "unchanged",
|
||||
result: syncer.Result{Unchanged: 1},
|
||||
wantCalls: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hook := &fakeHook{}
|
||||
err := Run(context.Background(), Config{Interval: time.Second, Once: true}, fakeSyncer{result: tt.result}, hook, slog.New(slog.DiscardHandler))
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
if hook.calls != tt.wantCalls {
|
||||
t.Fatalf("hook calls = %d, want %d", hook.calls, tt.wantCalls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunOnceReturnsHookError(t *testing.T) {
|
||||
wantErr := errors.New("hook failed")
|
||||
err := Run(
|
||||
context.Background(),
|
||||
Config{Interval: time.Second, Once: true},
|
||||
fakeSyncer{result: syncer.Result{Updated: 1}},
|
||||
&fakeHook{err: wantErr},
|
||||
slog.New(slog.DiscardHandler),
|
||||
)
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("Run() error = %v, want %v", err, wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeSyncer struct {
|
||||
result syncer.Result
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeSyncer) Sync(context.Context) (syncer.Result, error) {
|
||||
return f.result, f.err
|
||||
}
|
||||
|
||||
type fakeHook struct {
|
||||
calls int
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeHook) Run(context.Context, syncer.Result) error {
|
||||
f.calls++
|
||||
return f.err
|
||||
}
|
||||
Reference in New Issue
Block a user