feat(catchup): scheduleOverdue helper for missed-window detection
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
// catchup.go — server-side catch-up for intermittent (non-always-on)
|
||||
// hosts. When such a host reconnects we wait a short settle window,
|
||||
// then dispatch a backup for any schedule whose window elapsed while
|
||||
// the host was asleep. This is separate from pending_runs: a host that
|
||||
// was asleep never fired its local cron, so no pending row exists.
|
||||
package http
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// scheduleOverdue reports whether a schedule's most recent expected
|
||||
// fire is newer than the host's last successful backup — i.e. a window
|
||||
// passed with no backup. A nil lastBackup means "never backed up" and
|
||||
// is always overdue (provided the cron parses). An unparseable cron is
|
||||
// treated as not-overdue so a bad expression can never trigger a
|
||||
// surprise dispatch. Uses the same cronParser the agent's scheduler
|
||||
// and schedule validation use, so interpretation is identical.
|
||||
func scheduleOverdue(cronExpr string, lastBackup *time.Time, now time.Time) bool {
|
||||
sched, err := cronParser.Parse(cronExpr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if lastBackup == nil {
|
||||
return true
|
||||
}
|
||||
next := sched.Next(*lastBackup)
|
||||
return !next.After(now)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestScheduleOverdue(t *testing.T) {
|
||||
mustParse := func(s string) time.Time {
|
||||
t.Helper()
|
||||
v, err := time.Parse(time.RFC3339, s)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %q: %v", s, err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
daily := "0 2 * * *" // 02:00 every day
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
cron string
|
||||
lastBackup *time.Time
|
||||
now time.Time
|
||||
want bool
|
||||
}{
|
||||
{name: "never backed up is overdue", cron: daily, lastBackup: nil, now: mustParse("2026-06-15T09:00:00Z"), want: true},
|
||||
{name: "missed last nights window", cron: daily, lastBackup: ptrTime(mustParse("2026-06-13T02:05:00Z")), now: mustParse("2026-06-15T09:00:00Z"), want: true},
|
||||
{name: "backed up after the most recent window", cron: daily, lastBackup: ptrTime(mustParse("2026-06-15T02:05:00Z")), now: mustParse("2026-06-15T09:00:00Z"), want: false},
|
||||
{name: "unparseable cron is never overdue", cron: "not a cron", lastBackup: nil, now: mustParse("2026-06-15T09:00:00Z"), want: false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := scheduleOverdue(c.cron, c.lastBackup, c.now)
|
||||
if got != c.want {
|
||||
t.Fatalf("scheduleOverdue(%q, %v, %v) = %v, want %v", c.cron, c.lastBackup, c.now, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ptrTime(t time.Time) *time.Time { return &t }
|
||||
Reference in New Issue
Block a user