206 lines
5.8 KiB
Go
206 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// The scheduled run (design 42 zfs-owns-zfs §20, §24).
|
|
//
|
|
// `zfs-tools run` is the module's one scheduled run, root's, fired by the node-engine's timer every quarter of
|
|
// an hour. Each run takes the apply's lock; held, it does nothing and says `run.skipped` {reason: lock-held}.
|
|
// It does nothing while a scrub or resilver runs or before the pool is imported, saying that reason.
|
|
// Otherwise it acts on the hand acts waiting in the state `schedule`, starts the scrub when due and returns
|
|
// at once (the scrub's end is the event follower's to hear, §27), and starts a trim when trim-schedule is due.
|
|
|
|
// The reasons of `run.skipped`.
|
|
const (
|
|
SkipLockHeld = "lock-held"
|
|
SkipPoolNotImported = "pool-not-imported"
|
|
SkipScanRunning = "scrub-or-resilver-running"
|
|
)
|
|
|
|
// KeyPaused is the key of the state `schedule` that holds the schedule's snapshots (§24).
|
|
const KeyPaused = "paused"
|
|
|
|
// TrimArgv starts a trim of the pool.
|
|
func TrimArgv(pool string) []string { return []string{"trim", "--", pool} }
|
|
|
|
// handActs are the acts waiting in the state `schedule`, each under its own key (A-17), but the pause.
|
|
func (m *Machine) handActs() []string {
|
|
if m.States == nil {
|
|
return nil
|
|
}
|
|
keys, err := m.States.Keys(StateSchedule)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var acts []string
|
|
for _, k := range keys {
|
|
if k != KeyPaused {
|
|
acts = append(acts, k)
|
|
}
|
|
}
|
|
sort.Strings(acts)
|
|
return acts
|
|
}
|
|
|
|
func (m *Machine) skip(reason string) string {
|
|
var failed []error
|
|
if err := m.say(EventRunSkipped, map[string]any{"reason": reason}); err != nil {
|
|
m.failure(&failed, "run.skipped", err)
|
|
}
|
|
return "skipped: " + reason
|
|
}
|
|
|
|
// RunScheduled is the scheduled run. It answers what it did in a line; its error is a command that failed.
|
|
func (m *Machine) RunScheduled(ctx context.Context, servedPath string) (string, error) {
|
|
release, got, err := m.Lock(ApplyLock, false)
|
|
if err != nil {
|
|
return "", fmt.Errorf("the lock %s: %v", ApplyLock, err)
|
|
}
|
|
if !got {
|
|
return m.skip(SkipLockHeld), nil
|
|
}
|
|
defer release()
|
|
text, err := m.ReadFile(ConfigFile)
|
|
if err != nil {
|
|
return "", fmt.Errorf("%s could not be read: %v", ConfigFile, err)
|
|
}
|
|
c, refused := ParseConfig(text, m.memMiB())
|
|
if len(refused) > 0 {
|
|
return "refused, nothing run: " + refused[0].Key + " " + refused[0].Reason, nil
|
|
}
|
|
var said []string
|
|
var failed []error
|
|
acts := m.handActs()
|
|
guard := ""
|
|
var p *PoolStatus
|
|
if c.Pool != "" {
|
|
wait, perr := m.poolWait(ctx, c.Pool)
|
|
switch {
|
|
case perr != nil:
|
|
return "", perr
|
|
case strings.Contains(wait, "to be imported"):
|
|
guard = SkipPoolNotImported
|
|
case wait != "":
|
|
guard = SkipScanRunning
|
|
}
|
|
if guard == "" {
|
|
st, err := m.Status(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
p = statusOf(st, c.Pool)
|
|
}
|
|
}
|
|
for _, act := range acts {
|
|
by := m.actBy(act)
|
|
switch {
|
|
case act == ActSnapshots && guard == "":
|
|
ts, err := m.TakeScheduled(ctx, c, true, by)
|
|
said = append(said, ts...)
|
|
if err != nil {
|
|
failed = append(failed, err)
|
|
}
|
|
case act == "apply":
|
|
s, err := m.applyLocked(ctx)
|
|
said = append(said, "apply: "+s)
|
|
if err != nil {
|
|
failed = append(failed, err)
|
|
}
|
|
case act == ActPrune && guard == "":
|
|
ts, err := m.PruneScheduled(ctx, c, true, by)
|
|
said = append(said, ts...)
|
|
if err != nil {
|
|
failed = append(failed, err)
|
|
}
|
|
case guard != "":
|
|
said = append(said, m.skip(guard))
|
|
default:
|
|
m.Warnings = append(m.Warnings, "the hand act "+act+" is not one this run knows")
|
|
}
|
|
_ = m.States.Delete(StateSchedule, act)
|
|
}
|
|
if guard != "" || c.Pool == "" {
|
|
if guard != "" && len(acts) == 0 {
|
|
said = append(said, m.skip(guard))
|
|
}
|
|
return strings.Join(said, "; "), joinErrors(failed)
|
|
}
|
|
served := m.readServed(servedPath)
|
|
now := m.Now()
|
|
for _, work := range []struct {
|
|
name, key, verb string
|
|
s *Schedule
|
|
start func() error
|
|
}{{"scrub", c.Pool, "start", ScheduleOf(c), func() error {
|
|
_, err := m.AsRoot(ctx, "zpool", ScrubArgv("start", c.Pool)...)
|
|
return err
|
|
}}, {"trim", c.Pool + "/trim", "start", TrimScheduleOf(c), func() error {
|
|
_, err := m.AsRoot(ctx, "zpool", TrimArgv(c.Pool)...)
|
|
return err
|
|
}}} {
|
|
if work.s == nil {
|
|
continue
|
|
}
|
|
slot := work.s.Slot(now)
|
|
if slot.IsZero() || served[work.key] >= slot.Unix() {
|
|
continue
|
|
}
|
|
if work.name == "scrub" && startedSince(p, slot) {
|
|
said = append(said, fmt.Sprintf("the scrub of %s scheduled %s ran already", c.Pool, slot.Format("2006-01-02 15:04")))
|
|
} else if err := work.start(); err != nil && !strings.Contains(err.Error(), "currently") {
|
|
failed = append(failed, fmt.Errorf("the scheduled %s of %s could not start: %v", work.name, c.Pool, err))
|
|
continue
|
|
} else {
|
|
said = append(said, fmt.Sprintf("started the %s of %s scheduled %s", work.name, c.Pool, slot.Format("2006-01-02 15:04")))
|
|
}
|
|
served[work.key] = slot.Unix()
|
|
if err := writeServed(servedPath, served); err != nil {
|
|
failed = append(failed, fmt.Errorf("the %s of %s was started, and the served slot could not be kept: %v", work.name, c.Pool, err))
|
|
}
|
|
}
|
|
ts, err := m.TakeScheduled(ctx, c, false, "")
|
|
said = append(said, ts...)
|
|
if err != nil {
|
|
failed = append(failed, err)
|
|
}
|
|
ps, err := m.PruneScheduled(ctx, c, false, "")
|
|
said = append(said, ps...)
|
|
if err != nil {
|
|
failed = append(failed, err)
|
|
}
|
|
if len(said) == 0 {
|
|
said = append(said, "nothing due")
|
|
}
|
|
return strings.Join(said, "; "), joinErrors(failed)
|
|
}
|
|
|
|
func joinErrors(errs []error) error {
|
|
if len(errs) == 0 {
|
|
return nil
|
|
}
|
|
var parts []string
|
|
for _, e := range errs {
|
|
parts = append(parts, e.Error())
|
|
}
|
|
return fmt.Errorf("%s", strings.Join(parts, "; "))
|
|
}
|
|
|
|
// actBy is who a hand act was by, from its key.
|
|
func (m *Machine) actBy(act string) string {
|
|
raw, ok, err := m.States.Get(StateSchedule, act)
|
|
if err != nil || !ok {
|
|
return ""
|
|
}
|
|
var v struct {
|
|
By string `json:"by"`
|
|
}
|
|
_ = json.Unmarshal(raw, &v)
|
|
return v.By
|
|
}
|