feat: add Syncer that refreshes cache and tolerates fetch failure
This commit is contained in:
parent
4afe123a76
commit
74f3c03013
|
|
@ -0,0 +1,59 @@
|
||||||
|
package dyzurbot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FetchFunc retrieves and parses the current rota from upstream (the Sheet).
|
||||||
|
type FetchFunc func(ctx context.Context) (Rota, error)
|
||||||
|
|
||||||
|
// Syncer refreshes a Store from an upstream fetch.
|
||||||
|
type Syncer struct {
|
||||||
|
store *Store
|
||||||
|
fetch FetchFunc
|
||||||
|
now func() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSyncer wires a Store to a fetch function.
|
||||||
|
func NewSyncer(store *Store, fetch FetchFunc) *Syncer {
|
||||||
|
return &Syncer{store: store, fetch: fetch, now: time.Now}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync fetches once. On success it replaces the cache; on failure it logs a
|
||||||
|
// warning, leaves the last good cache in place, and returns the error so a
|
||||||
|
// caller (e.g. a manual /sync command) can report it.
|
||||||
|
func (sy *Syncer) Sync(ctx context.Context) error {
|
||||||
|
rota, err := sy.fetch(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("sync: fetch failed, keeping cached data: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sy.store.Replace(rota, sy.now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run polls Sync on the given interval until ctx is cancelled.
|
||||||
|
func (sy *Syncer) Run(ctx context.Context, interval time.Duration) {
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
_ = sy.Sync(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SheetFetcher builds a FetchFunc that reads the rota from Google Sheets.
|
||||||
|
func SheetFetcher(spreadsheetID, readRange string) FetchFunc {
|
||||||
|
return func(ctx context.Context) (Rota, error) {
|
||||||
|
rows, err := ReadSheet(ctx, spreadsheetID, readRange)
|
||||||
|
if err != nil {
|
||||||
|
return Rota{}, err
|
||||||
|
}
|
||||||
|
return ParseRota(rows), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package dyzurbot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSyncSuccessReplacesCache(t *testing.T) {
|
||||||
|
store := NewStore(filepath.Join(t.TempDir(), "rota.json"))
|
||||||
|
fixed := time.Date(2026, 6, 17, 20, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
sy := NewSyncer(store, func(context.Context) (Rota, error) { return sampleRota(), nil })
|
||||||
|
sy.now = func() time.Time { return fixed }
|
||||||
|
|
||||||
|
if err := sy.Sync(context.Background()); err != nil {
|
||||||
|
t.Fatalf("Sync: %v", err)
|
||||||
|
}
|
||||||
|
rota, ts := store.Snapshot()
|
||||||
|
if len(rota.People) != 1 || !ts.Equal(fixed) {
|
||||||
|
t.Errorf("cache not updated: people=%d ts=%v", len(rota.People), ts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncFailurePreservesCache(t *testing.T) {
|
||||||
|
store := NewStore(filepath.Join(t.TempDir(), "rota.json"))
|
||||||
|
good := time.Date(2026, 6, 17, 20, 0, 0, 0, time.UTC)
|
||||||
|
if err := store.Replace(sampleRota(), good); err != nil {
|
||||||
|
t.Fatalf("seed Replace: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantErr := errors.New("sheet unreachable")
|
||||||
|
sy := NewSyncer(store, func(context.Context) (Rota, error) { return Rota{}, wantErr })
|
||||||
|
|
||||||
|
if err := sy.Sync(context.Background()); !errors.Is(err, wantErr) {
|
||||||
|
t.Fatalf("Sync error: got %v, want %v", err, wantErr)
|
||||||
|
}
|
||||||
|
rota, ts := store.Snapshot()
|
||||||
|
if len(rota.People) != 1 || !ts.Equal(good) {
|
||||||
|
t.Errorf("cache should be untouched: people=%d ts=%v", len(rota.People), ts)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue