Files
mesh-catalog/modules/zfs/cmd/zfs-tools/hand.go
T
2026-11-13 00:41:21 +01:00

115 lines
4.1 KiB
Go

package main
import (
"context"
"fmt"
"strings"
"time"
)
// The hand acts (design 42 zfs-owns-zfs §23, §24; A-12, A-17, A-18).
//
// A hand act writes its act into the state `schedule` under its own key {by, why, at} and starts the unit of
// the scheduled run through `<node>/node-service-manager.start` with {scope, unit}; the run reads the key,
// acts under the same lock and guards, and deletes it. The tool waits at most HandWait for the act's event; past
// it, it answers "started", naming the act and that `zfs_module_events` reads its outcome, and never "done" for
// what it did not see end. The `why` is kept in the key.
// HandWait is how long a hand act's tool waits for the act's event (A-12).
const HandWait = 30 * time.Second
// The acts, as the keys of the state `schedule`.
const (
ActSnapshots = "snapshots"
ActPrune = "prune"
ActApply = "apply"
)
// handEnds are the events that end each act.
var handEnds = map[string][]string{
ActSnapshots: {EventTakenPerson, EventSkippedPerson, EventRunSkipped},
ActPrune: {EventRemovedPerson, EventSkippedPerson, EventRunSkipped},
ActApply: {EventApplied, EventRefused, EventWaiting},
}
// StartArgs are what `node-service-manager.start` is called with to run the scheduled run now.
func StartArgs() map[string]any { return map[string]any{"scope": "system", "unit": RunService} }
// HandAnswer is what a hand act's tool answers.
type HandAnswer struct {
Act string `json:"act"`
// Outcome is "seen" (the act's event came) or "started" (it was started, its end not seen).
Outcome string `json:"outcome"`
Event map[string]any `json:"event,omitempty"`
Said string `json:"said"`
}
func whyOf(why string) error {
switch {
case why == "":
return fmt.Errorf("say why: a hand act is a person's, and the why is kept with it")
case strings.ContainsAny(why, "\n\r") || len(why) > MaxWhy:
return fmt.Errorf("say why in one line of at most %d characters", MaxWhy)
}
return nil
}
// HandAct starts one act of the scheduled run by hand.
func (m *Machine) HandAct(ctx context.Context, act, by, why string) (HandAnswer, error) {
a := HandAnswer{Act: act}
if _, ok := handEnds[act]; !ok {
return a, fmt.Errorf("%q is not an act of the run: snapshots, prune or apply", act)
}
if err := whyOf(why); err != nil {
return a, err
}
if m.States == nil || m.Call == nil {
return a, fmt.Errorf("this process reaches neither the module's state nor the service manager, so %s was not started", act)
}
if err := m.States.Put(StateSchedule, act, map[string]any{"by": by, "why": why, "at": m.Now().UTC()}); err != nil {
return a, fmt.Errorf("the act %s could not be written to the state %s: %v", act, StateSchedule, err)
}
if _, err := m.Call("seat:node-service-manager.start@"+m.Node, StartArgs()); err != nil {
_ = m.States.Delete(StateSchedule, act)
return a, fmt.Errorf("the service manager did not start %s: %v", RunService, err)
}
if m.Await != nil {
if ev, ok := m.Await(handEnds[act], HandWait); ok {
a.Outcome, a.Event = "seen", ev
a.Said = fmt.Sprintf("The act %s ran: %v.", act, ev)
return a, nil
}
}
a.Outcome = "started"
a.Said = fmt.Sprintf("Started the act %s (%s) and did not see it end within %s: zfs_module_events reads its outcome.", act, RunService, HandWait)
return a, nil
}
// Pause writes `paused` {by, why, at} in the state `schedule`: the run then takes and removes nothing (§24).
func (m *Machine) Pause(by, why string) error {
if err := whyOf(why); err != nil {
return err
}
if m.States == nil {
return fmt.Errorf("this process reaches no state, so the schedule was not paused")
}
return m.States.Put(StateSchedule, KeyPaused, map[string]any{"by": by, "why": why, "at": m.Now().UTC()})
}
// Resume deletes `paused`.
func (m *Machine) Resume() error {
if m.States == nil {
return fmt.Errorf("this process reaches no state, so the schedule was not resumed")
}
return m.States.Delete(StateSchedule, KeyPaused)
}
// Paused says whether the schedule is paused.
func (m *Machine) Paused() bool {
if m.States == nil {
return false
}
_, ok, err := m.States.Get(StateSchedule, KeyPaused)
return err == nil && ok
}