hub v0.163.0: a rebuilt box gets the household's sealed package back (R-1176); one run per daily job on clock-change days (R-1183); a scoped abandonment purge (R-1182); the new-link page says the old box is still linked (R-1203)
gates / gates (push) Successful in 13m23s
gates / gates (push) Successful in 13m23s
This commit is contained in:
1 parent
e07b077ac2
commit
9688e76efd
16 files changed
+1018
-29
No files matched your search
+22
-5
@@ -1054,12 +1054,9 @@ func scheduleDaily(ctx context.Context, name, timeStr string, fn func(), logger
|
||||
|
||||
hour, min := parseHM(timeStr)
|
||||
|
||||
var last time.Time
|
||||
for {
|
||||
now := time.Now().In(budapest)
|
||||
next := time.Date(now.Year(), now.Month(), now.Day(), hour, min, 0, 0, budapest)
|
||||
if !next.After(now) {
|
||||
next = next.Add(24 * time.Hour)
|
||||
}
|
||||
next := nextDailyRun(time.Now(), last, hour, min, budapest)
|
||||
delay := time.Until(next)
|
||||
logger.Printf("[INFO] %s: next run at %s (in %s)", name, next.Format("2006-01-02 15:04 MST"), delay.Round(time.Second))
|
||||
|
||||
@@ -1067,11 +1064,31 @@ func scheduleDaily(ctx context.Context, name, timeStr string, fn func(), logger
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(delay):
|
||||
last = next
|
||||
fn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// nextDailyRun returns the next HH:MM in loc that is after now and after the slot that ran last (the zero time
|
||||
// when none has). R-1183: the day after is computed as a calendar day (time.Date with day+1), never as now+24h —
|
||||
// on 2026-10-25 (25 hours) the old Add(24h) fired a 04:00 job at 03:00 CET and again at 04:00, and on 2027-03-28
|
||||
// (23 hours) it fired at 05:00 CEST. The controller's scheduler made the same fix (scheduler.go, day+1).
|
||||
// The `last` guard keeps a timer that wakes a moment early from running the same slot twice.
|
||||
// Pinned by TestR1183_* in r1183_dst_schedule_test.go.
|
||||
func nextDailyRun(now, last time.Time, hour, min int, loc *time.Location) time.Time {
|
||||
if last.After(now) {
|
||||
now = last
|
||||
}
|
||||
now = now.In(loc)
|
||||
for d := 0; ; d++ {
|
||||
next := time.Date(now.Year(), now.Month(), now.Day()+d, hour, min, 0, 0, loc)
|
||||
if next.After(now) {
|
||||
return next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseHM parses "HH:MM" into hour and minute. Returns 0, 0 on invalid input.
|
||||
func parseHM(s string) (int, int) {
|
||||
var h, m int
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// R-1183: a daily hub job runs ONCE on a 25-hour day (2026-10-25, the clocks go back) and once on a 23-hour day
|
||||
// (2027-03-28, the clocks go forward), each time at the wall-clock hour it was set for. The old loop added 24 h to
|
||||
// an absolute time, so on 2026-10-25 a 04:00 job fired at 03:00 CET and again at 04:00 CET, and on 2027-03-28 it
|
||||
// fired at 05:00 CEST — the replay is in audits/dataloss-hunt-2026-10-20/C3-T9-hub-dst/.
|
||||
//
|
||||
// The test walks the scheduler's own sequence (nextDailyRun after each run) across both days with a fake clock.
|
||||
func walkRuns(t *testing.T, start time.Time, until time.Time, hour, min int, loc *time.Location) []time.Time {
|
||||
t.Helper()
|
||||
var runs []time.Time
|
||||
now := start
|
||||
var last time.Time
|
||||
for i := 0; i < 20; i++ {
|
||||
next := nextDailyRun(now, last, hour, min, loc)
|
||||
if next.After(until) {
|
||||
break
|
||||
}
|
||||
runs = append(runs, next)
|
||||
last = next
|
||||
now = next.Add(2 * time.Second) // the job takes a moment; the loop asks again
|
||||
}
|
||||
return runs
|
||||
}
|
||||
|
||||
func TestR1183_DailyJobRunsOnceOnTheDayTheClocksGoBack(t *testing.T) {
|
||||
loc, err := time.LoadLocation("Europe/Budapest")
|
||||
if err != nil {
|
||||
t.Skip("no tz database")
|
||||
}
|
||||
for _, hm := range [][2]int{{4, 0}, {5, 0}, {7, 10}, {2, 0}, {2, 30}} {
|
||||
start := time.Date(2026, 10, 24, 12, 0, 0, 0, loc)
|
||||
until := time.Date(2026, 10, 26, 23, 0, 0, 0, loc)
|
||||
runs := walkRuns(t, start, until, hm[0], hm[1], loc)
|
||||
perDay := map[string]int{}
|
||||
for _, r := range runs {
|
||||
l := r.In(loc)
|
||||
perDay[l.Format("2006-01-02")]++
|
||||
if l.Hour() != hm[0] || l.Minute() != hm[1] {
|
||||
t.Errorf("%02d:%02d job ran at %s — not at its wall-clock time", hm[0], hm[1], l.Format("2006-01-02 15:04 MST"))
|
||||
}
|
||||
}
|
||||
for _, d := range []string{"2026-10-25", "2026-10-26"} {
|
||||
if perDay[d] != 1 {
|
||||
t.Errorf("%02d:%02d job ran %d time(s) on %s, want 1 (runs: %v)", hm[0], hm[1], perDay[d], d, runs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestR1183_DailyJobRunsOnceOnTheDayTheClocksGoForward(t *testing.T) {
|
||||
loc, err := time.LoadLocation("Europe/Budapest")
|
||||
if err != nil {
|
||||
t.Skip("no tz database")
|
||||
}
|
||||
for _, hm := range [][2]int{{4, 0}, {5, 0}, {7, 10}} {
|
||||
start := time.Date(2027, 3, 27, 12, 0, 0, 0, loc)
|
||||
until := time.Date(2027, 3, 29, 23, 0, 0, 0, loc)
|
||||
runs := walkRuns(t, start, until, hm[0], hm[1], loc)
|
||||
perDay := map[string]int{}
|
||||
for _, r := range runs {
|
||||
l := r.In(loc)
|
||||
perDay[l.Format("2006-01-02")]++
|
||||
if l.Hour() != hm[0] || l.Minute() != hm[1] {
|
||||
t.Errorf("%02d:%02d job ran at %s — not at its wall-clock time", hm[0], hm[1], l.Format("2006-01-02 15:04 MST"))
|
||||
}
|
||||
}
|
||||
for _, d := range []string{"2027-03-28", "2027-03-29"} {
|
||||
if perDay[d] != 1 {
|
||||
t.Errorf("%02d:%02d job ran %d time(s) on %s, want 1 (runs: %v)", hm[0], hm[1], perDay[d], d, runs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A timer that fires a moment early (a wall-clock step) must not run the same slot twice.
|
||||
func TestR1183_TheSameSlotNeverRunsTwice(t *testing.T) {
|
||||
loc, err := time.LoadLocation("Europe/Budapest")
|
||||
if err != nil {
|
||||
t.Skip("no tz database")
|
||||
}
|
||||
slot := time.Date(2026, 11, 3, 4, 0, 0, 0, loc)
|
||||
early := slot.Add(-300 * time.Millisecond) // the loop asks again while the wall clock still reads 03:59:59.7
|
||||
if next := nextDailyRun(early, slot, 4, 0, loc); !next.After(slot) {
|
||||
t.Fatalf("after running the %s slot, the next run is %s — the same slot again", slot, next)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user