The second review of #147 found three blockers before the home server. 1. ${setting:} was not filled in a unit's name, so the distribution timers would have reached the machine as "zfs-scrub-weekly@${setting:scrub-pool}.timer". mesh-controller #171 fills it and refuses any value that is not a unit-name part. The module's own schedule needs no setting in a name now: the node-engine's timer for the scheduled process zfs-mesh-scrub fires `zfs-tools scrub-run` every quarter of an hour, and the run reads the pool, cadence, day and time from the configuration file. 2. The run does what upstream's zfs-scrub@.service does: if a scrub is running it waits for it with zpool wait -t scrub, otherwise it starts one and waits. It never pauses or stops a scrub, and the unit has no ExecStop. A resilver defers the scrub. The served slot is kept in /var/lib/zfs-tools. 3. Unassigning left the pool with no schedule. mesh-host #66 (hq ADR 0269, proposed) gives the distribution timer back running and enabled. The README says not to unassign before it is merged. Also: the run comes before the distribution timers in the declaration; the day and time are checked against fixed sets and reach no unit file (an invalid value runs nothing); a due scrub not started an hour after its time is a warning; the error counts outlive a restart in a small state file.
169 lines
6.3 KiB
Go
169 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// The scheduled run (`zfs-tools scrub-run`, second review of mesh-catalog #147): the node-engine's timer
|
|
// fires it every quarter of an hour, and it decides from the configuration whether a scrub is due. It
|
|
// starts one, or waits for one already running, as upstream's zfs-scrub@.service does, and never pauses or
|
|
// stops a scrub.
|
|
|
|
func monthlyConf(pool string) string {
|
|
return "pool=" + pool + "\ncadence=monthly\nday=Mon\ntime=00:00\n"
|
|
}
|
|
|
|
func TestTheSlotIsTheLastScheduledTimeNotAfterNow(t *testing.T) {
|
|
loc := time.Local
|
|
at := func(y int, m time.Month, d, h, min int) time.Time { return time.Date(y, m, d, h, min, 0, 0, loc) }
|
|
cases := []struct {
|
|
conf string
|
|
now time.Time
|
|
want time.Time
|
|
}{
|
|
{"pool=tank\ncadence=weekly\nday=Mon\ntime=00:00\n", at(2026, 10, 9, 12, 0), at(2026, 10, 5, 0, 0)},
|
|
{"pool=tank\ncadence=weekly\nday=Fri\ntime=13:30\n", at(2026, 10, 9, 12, 0), at(2026, 10, 2, 13, 30)},
|
|
{"pool=tank\ncadence=weekly\nday=Fri\ntime=11:30\n", at(2026, 10, 9, 12, 0), at(2026, 10, 9, 11, 30)},
|
|
{monthlyConf("tank"), at(2026, 10, 9, 12, 0), at(2026, 10, 5, 0, 0)},
|
|
{monthlyConf("tank"), at(2026, 10, 4, 23, 0), at(2026, 9, 7, 0, 0)},
|
|
{"pool=tank\ncadence=monthly\nday=Thu\ntime=06:00\n", at(2026, 10, 1, 6, 0), at(2026, 10, 1, 6, 0)},
|
|
}
|
|
for _, c := range cases {
|
|
s, err := ParseScrubConfig(c.conf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := s.Slot(c.now); !got.Equal(c.want) {
|
|
t.Errorf("%q at %s: slot %s, want %s", c.conf, c.now, got, c.want)
|
|
}
|
|
if next := s.NextSlot(c.now); !next.After(c.now) || !s.Slot(next).Equal(next) {
|
|
t.Errorf("%q at %s: next slot %s", c.conf, c.now, next)
|
|
}
|
|
}
|
|
}
|
|
|
|
// root runs the scheduled run: no sudo.
|
|
func rootFake(t *testing.T, conf string) (*fake, string) {
|
|
f := withZFS(t)
|
|
f.uid = 0
|
|
f.files[ScrubConfig] = conf
|
|
dir := t.TempDir()
|
|
return f, filepath.Join(dir, "scrub-served.json")
|
|
}
|
|
|
|
func TestADueScrubIsStartedThenWaitedForAndMarkedServed(t *testing.T) {
|
|
f, served := rootFake(t, monthlyConf("tank"))
|
|
f.on("zpool scrub -- tank", "")
|
|
f.on("zpool wait -t scrub -- tank", "")
|
|
said, err := f.machine().ScrubRun(context.Background(), served)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := strings.Join(ran(f, "zpool scrub"), "|") + "/" + strings.Join(ran(f, "zpool wait"), "|"); got != "zpool scrub -- tank/zpool wait -t scrub -- tank" {
|
|
t.Errorf("ran %q", got)
|
|
}
|
|
if !strings.Contains(said, "started") {
|
|
t.Errorf("said %q", said)
|
|
}
|
|
// Served: the next fire within the same slot runs nothing.
|
|
f.calls = nil
|
|
if _, err := f.machine().ScrubRun(context.Background(), served); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n := len(ran(f, "zpool scrub")) + len(ran(f, "zpool wait")); n != 0 {
|
|
t.Errorf("a served slot ran %v", f.calls)
|
|
}
|
|
}
|
|
|
|
func TestAScrubInProgressIsWaitedForNeverPausedOrStarted(t *testing.T) {
|
|
f, served := rootFake(t, monthlyConf("tank"))
|
|
f.on(line("zpool", StatusArgv()), fixture(t, "zpool-status-scrubbing.txt"))
|
|
f.on(line("zpool", PoolsArgv()), "tank\t1000\t100\t900\t10\t1\tONLINE\t1.00\n")
|
|
f.on("zpool wait -t scrub -- tank", "")
|
|
if _, err := f.machine().ScrubRun(context.Background(), served); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Join(f.calls, "|") == "" || len(ran(f, "zpool scrub")) != 0 || len(ran(f, "zpool wait -t scrub -- tank")) != 1 {
|
|
t.Errorf("ran %v", f.calls)
|
|
}
|
|
}
|
|
|
|
func TestAStartRefusedBecauseAScrubJustBeganIsWaitedForNotPaused(t *testing.T) {
|
|
f, served := rootFake(t, monthlyConf("tank"))
|
|
f.answers["zpool scrub -- tank"] = Ran{Code: 1, Err: "cannot scrub tank: currently scrubbing; use 'zpool scrub -s' to cancel the current scrub"}
|
|
f.on("zpool wait -t scrub -- tank", "")
|
|
if _, err := f.machine().ScrubRun(context.Background(), served); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, c := range f.calls {
|
|
if strings.Contains(c, "scrub -p") || strings.Contains(c, "scrub -s") {
|
|
t.Errorf("the run paused or stopped a scrub: %s", c)
|
|
}
|
|
}
|
|
if len(ran(f, "zpool wait -t scrub -- tank")) != 1 {
|
|
t.Errorf("not waited for: %v", f.calls)
|
|
}
|
|
}
|
|
|
|
func TestAResilverDefersTheScrubWithoutServingTheSlot(t *testing.T) {
|
|
f, served := rootFake(t, monthlyConf("tank"))
|
|
f.on(line("zpool", StatusArgv()), fixture(t, "zpool-status-degraded.txt"))
|
|
f.on(line("zpool", PoolsArgv()), "tank\t1000\t100\t900\t10\t1\tDEGRADED\t1.00\n")
|
|
said, err := f.machine().ScrubRun(context.Background(), served)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(ran(f, "zpool scrub")) != 0 || !strings.Contains(said, "resilver") {
|
|
t.Errorf("ran %v, said %q", f.calls, said)
|
|
}
|
|
if _, err := os.Stat(served); err == nil {
|
|
t.Errorf("the slot was marked served while the scrub waits for the resilver")
|
|
}
|
|
}
|
|
|
|
func TestAScrubAlreadyStartedInTheSlotServesItWithoutAStateFile(t *testing.T) {
|
|
// tank's scrub started 2026-09-13 00:24 (finished 09-14); a weekly Sunday-00:00 slot of 09-13 is served
|
|
// by it, so a run on 09-15 starts nothing.
|
|
f, served := rootFake(t, "pool=tank\ncadence=weekly\nday=Sun\ntime=00:00\n")
|
|
f.now = time.Date(2026, 9, 15, 12, 0, 0, 0, time.Local)
|
|
if _, err := f.machine().ScrubRun(context.Background(), served); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n := len(ran(f, "zpool scrub")) + len(ran(f, "zpool wait")); n != 0 {
|
|
t.Errorf("ran %v", f.calls)
|
|
}
|
|
}
|
|
|
|
func TestAnInvalidScheduleRunsNothingAndFails(t *testing.T) {
|
|
for _, conf := range []string{
|
|
"pool=tank\ncadence=monthly\nday=Mon\ntime=*:*\n",
|
|
"pool=tank\ncadence=monthly\nday=Mon..Fri\ntime=00:00\n",
|
|
"pool=tank\ncadence=monthly\nday=*\ntime=00:00\n",
|
|
"pool=tank\ncadence=hourly\nday=Mon\ntime=00:00\n",
|
|
"pool=tank\ncadence=monthly\nday=Mon\ntime=0:00\n",
|
|
} {
|
|
f, served := rootFake(t, conf)
|
|
if _, err := f.machine().ScrubRun(context.Background(), served); err == nil {
|
|
t.Errorf("%q: not refused", conf)
|
|
}
|
|
if len(ran(f, "zpool scrub"))+len(ran(f, "zpool wait")) != 0 {
|
|
t.Errorf("%q ran %v", conf, f.calls)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAScheduledScrubThatDidNotStartIsAWarning(t *testing.T) {
|
|
f := withZFS(t)
|
|
f.files[ScrubConfig] = "pool=tank\ncadence=weekly\nday=Mon\ntime=00:00\n"
|
|
f.on(line("systemctl", TimerShowArgv(ScrubTimer)), "ActiveState=active\nUnitFileState=enabled\nNextElapseUSecRealtime=@1791842400\nLastTriggerUSec=@1791237600\n")
|
|
h := f.machine().Health(context.Background())
|
|
if !strings.Contains(strings.Join(h.Warnings, " "), "The scheduled scrub of tank due 2026-10-05 00:00 has not started") {
|
|
t.Errorf("warnings: %v", h.Warnings)
|
|
}
|
|
}
|