issue 1841/S5: join the upgrade parts to the keeper's loop, so a node's log, records, unfinished runs and asks are taken up by its own keeper
At its start the keeper reads the log into runs once, prunes old records and carries unfinished runs on; after every released lock it carries them on again; it takes each ask at <node>.* and prunes records daily. The root helper's client is a constructor (newRootActs) the helper replaces; until then every act is refused, named.
This commit is contained in:
@@ -33,8 +33,8 @@ package main
|
||||
// it next wants to be called (a zero time: not again). It is how a hook keeps a time (a round's opening, a
|
||||
// stall) without polling.
|
||||
//
|
||||
// A hook's error is logged and the keeper goes on: a fault in one part never stops the view. keeperHooks names
|
||||
// the hooks the keeper runs with; it is the one place a part is added.
|
||||
// A hook's error is logged and the keeper goes on: a fault in one part never stops the view. keeperHooks builds
|
||||
// the hooks the keeper runs with, once connected; it is the one place a part is added.
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -140,9 +140,60 @@ type Hooks struct {
|
||||
Wakes []Wake
|
||||
}
|
||||
|
||||
// keeperHooks is the hooks the keeper runs with: the one place a part of the keeper is added (ADR 0508's
|
||||
// records, runs, rounds and asks).
|
||||
var keeperHooks = func() Hooks { return Hooks{} }
|
||||
// keeperHooks is the hooks the keeper runs with, built once the keeper is connected: the one place a part of
|
||||
// the keeper is added. Today: the upgrades of ADR 0508 (upgrades.go, record.go, transaction.go), for this node.
|
||||
//
|
||||
// - Start: the package manager's log read into `runs` once (ImportLog), records past record-days pruned
|
||||
// (PruneRecords), and every unfinished run carried on (Follow), a run a keeper that went away left included;
|
||||
// - AfterTransaction: every unfinished run carried on (Follow): a released lock may be a run's transaction ending;
|
||||
// - Changes: `asks` at `<node>.*`, each change handed to TakeAsk;
|
||||
// - Wakes: PruneRecords once a day.
|
||||
func keeperHooks(k *Keeper) Hooks {
|
||||
u := &Upgrades{Node: k.Node, Config: k.Config, Machine: k.Machine, Root: newRootActs(), Events: k.Events,
|
||||
Installed: k.State(StateInstalled), Updates: k.State(StateUpdates), Runs: k.State(StateRuns),
|
||||
Records: k.State(StateRecords), Asks: k.State(StateAsks)}
|
||||
return Hooks{
|
||||
Start: []func(*Keeper) error{
|
||||
func(k *Keeper) error {
|
||||
n, err := u.ImportLog()
|
||||
if n > 0 {
|
||||
k.logf("[pacman] keeper: %d past upgrades read from the package manager's log into runs", n)
|
||||
}
|
||||
return err
|
||||
},
|
||||
func(*Keeper) error { _, err := u.PruneRecords(); return err },
|
||||
func(*Keeper) error { return u.Follow() },
|
||||
},
|
||||
AfterTransaction: []func(*Keeper, Transaction) error{
|
||||
func(*Keeper, Transaction) error { return u.Follow() },
|
||||
},
|
||||
Changes: []StateHook{{State: StateAsks, Key: k.Node + ".*", Handle: func(_ *Keeper, c stdio.StateChange) error {
|
||||
return u.TakeAsk(c)
|
||||
}}},
|
||||
Wakes: []Wake{func(k *Keeper, now time.Time) time.Time {
|
||||
if !now.Equal(k.started) {
|
||||
if _, err := u.PruneRecords(); err != nil {
|
||||
k.logf("[pacman] keeper: pruning records: %v", err)
|
||||
}
|
||||
}
|
||||
return now.Add(UpdatesEvery)
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// newRootActs is the client of the module's root helper (ADR 0510), which carries out the acts that need root;
|
||||
// the helper's socket client replaces it. Until then every act is refused, named.
|
||||
var newRootActs = func() RootActs { return noRoot{} }
|
||||
|
||||
type noRoot struct{}
|
||||
|
||||
func (noRoot) Transaction(kind, run string, ignore []string) error {
|
||||
return errors.New("the root helper's client is not built in yet: no transaction can start")
|
||||
}
|
||||
|
||||
func (noRoot) Journal(unit string, n int) ([]string, error) {
|
||||
return nil, errors.New("the root helper's client is not built in yet: no journal can be read")
|
||||
}
|
||||
|
||||
// Keeper is the module's process on one node.
|
||||
type Keeper struct {
|
||||
@@ -161,7 +212,12 @@ type Keeper struct {
|
||||
// DBChanged is when the local package database last changed, the time of a value read at the start.
|
||||
DBChanged func() time.Time
|
||||
Logf func(format string, a ...any)
|
||||
// Hooks are the parts joining the loop; MakeHooks, when set, builds them once the keeper is connected
|
||||
// (its State and Events set), replacing Hooks.
|
||||
Hooks Hooks
|
||||
MakeHooks func(k *Keeper) Hooks
|
||||
|
||||
started time.Time
|
||||
|
||||
nextUpdates time.Time
|
||||
wakes []time.Time
|
||||
@@ -180,7 +236,11 @@ type change struct {
|
||||
|
||||
// Run reads the machine into the bus and then follows it until ctx ends or the watch fails.
|
||||
func (k *Keeper) Run(ctx context.Context) error {
|
||||
at := k.Clock.Now()
|
||||
if k.MakeHooks != nil {
|
||||
k.Hooks = k.MakeHooks(k)
|
||||
}
|
||||
k.started = k.Clock.Now()
|
||||
at := k.started
|
||||
if k.DBChanged != nil {
|
||||
if t := k.DBChanged(); !t.IsZero() {
|
||||
at = t
|
||||
@@ -213,7 +273,7 @@ func (k *Keeper) Run(ctx context.Context) error {
|
||||
return fmt.Errorf("watching %s %q: %w", h.State, h.Key, err)
|
||||
}
|
||||
}
|
||||
now := k.Clock.Now()
|
||||
now := k.started
|
||||
k.wakes = make([]time.Time, len(k.Hooks.Wakes))
|
||||
for i, w := range k.Hooks.Wakes {
|
||||
k.wakes[i] = w(k, now)
|
||||
@@ -480,7 +540,7 @@ func keeper() int {
|
||||
Node: account.Node, Config: cfg, Machine: ThisMachine(), Clock: realClock{}, Watcher: w, Logf: logf,
|
||||
Locked: func() bool { _, err := os.Stat(filepath.Join(PacmanDBDir, LockName)); return err == nil },
|
||||
DBChanged: func() time.Time { return modified(filepath.Join(PacmanDBDir, "local")) },
|
||||
Hooks: keeperHooks(),
|
||||
MakeHooks: keeperHooks,
|
||||
}
|
||||
logf("[pacman] keeper of %s is starting as %s", account.Node, account.User)
|
||||
if err := runKeeper(ctx, account, k); err != nil && ctx.Err() == nil {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
// The upgrade parts joined to the keeper's loop (keeperHooks): the log read into runs and old records pruned at
|
||||
// the start, an ask for this node taken from `asks`, and the records pruned again a day later.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTheKeeperRunsTheUpgradePartsAtItsStartOnItsAsksAndDaily(t *testing.T) {
|
||||
db := &pacmanDB{sl: syncDBs, checkup: Ran{Status: 2}}
|
||||
db.set("glibc 2.42-1\n", "")
|
||||
k, states, _, clock, _ := newTestKeeper(db)
|
||||
log := capturedLog(t)
|
||||
k.Machine.ReadFile = func(path string) ([]byte, error) {
|
||||
if path == PacmanLog {
|
||||
return []byte(log), nil
|
||||
}
|
||||
return nil, errNoFile
|
||||
}
|
||||
k.MakeHooks = keeperHooks
|
||||
k.Logf = t.Logf
|
||||
k.Config = KeeperConfig{RecordDays: 180}
|
||||
records := states.store(StateRecords)
|
||||
records.Put(NodeKey("node-a", "20250101T040000Z"), Record{Node: "node-a", Run: "20250101T040000Z"})
|
||||
records.Put(NodeKey("node-b", "20250101T040000Z"), Record{Node: "node-b", Run: "20250101T040000Z"})
|
||||
done := Run{Node: "node-a", Run: "20261003T040000Z", Kind: RunUpgrade, From: FromAsk, Ask: "ask-1",
|
||||
Steps: []Step{{Step: StepRefused, At: t0, Why: "a test"}}}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ended := make(chan error, 1)
|
||||
go func() { ended <- k.Run(ctx) }()
|
||||
until := func(what string, ok func() bool) {
|
||||
t.Helper()
|
||||
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(time.Millisecond) {
|
||||
if ok() {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("never: %s", what)
|
||||
}
|
||||
until("the log read into runs", func() bool {
|
||||
keys, _ := states.store(StateRuns).Keys()
|
||||
return len(keys) > 1
|
||||
})
|
||||
until("the old record pruned at the start", func() bool {
|
||||
e, _ := records.Get(NodeKey("node-a", "20250101T040000Z"))
|
||||
return e == nil
|
||||
})
|
||||
if e, _ := records.Get(NodeKey("node-b", "20250101T040000Z")); e == nil {
|
||||
t.Error("another node's record was pruned")
|
||||
}
|
||||
|
||||
// a run an ask started, ended; its ask left behind by a keeper that went away.
|
||||
states.store(StateRuns).Put(NodeKey("node-a", done.Run), done)
|
||||
asks := states.store(StateAsks)
|
||||
asks.Put(NodeKey("node-a", "ask-1"), Ask{ID: "ask-1", Node: "node-a", Act: ActUpgrade, At: t0})
|
||||
until("the ask a run names deleted", func() bool {
|
||||
e, _ := asks.Get(NodeKey("node-a", "ask-1"))
|
||||
return e == nil
|
||||
})
|
||||
|
||||
records.Put(NodeKey("node-a", "20250102T040000Z"), Record{Node: "node-a", Run: "20250102T040000Z"})
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
if e, _ := records.Get(NodeKey("node-a", "20250102T040000Z")); e == nil {
|
||||
t.Fatal("pruned before a day passed")
|
||||
}
|
||||
clock.Advance(UpdatesEvery)
|
||||
until("the old record pruned a day later", func() bool {
|
||||
e, _ := records.Get(NodeKey("node-a", "20250102T040000Z"))
|
||||
return e == nil
|
||||
})
|
||||
cancel()
|
||||
if err := <-ended; err == nil || !strings.Contains(err.Error(), "canceled") {
|
||||
t.Errorf("Run: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,7 @@ func TestTheKeeperPutsTheNodesPackagesOnARealBusAndAnotherInstanceReadsThem(t *t
|
||||
db.set("glibc 2.42-1\nzsh 5.9-1\n", "zsh\n")
|
||||
signals := make(chan Signal)
|
||||
k := &Keeper{Config: KeeperConfig{}, Machine: machine(db.runner(), 1000), Clock: realClock{},
|
||||
Watcher: fakeWatcher{signals}, Logf: t.Logf, Hooks: keeperHooks()}
|
||||
Watcher: fakeWatcher{signals}, Logf: t.Logf, MakeHooks: keeperHooks}
|
||||
runCtx, cancel := context.WithCancel(ctx)
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- runKeeper(runCtx, account, k) }()
|
||||
|
||||
Reference in New Issue
Block a user