From ac5d257a1c9cd245afd89e768e16683f80e490eb Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 22:04:23 +0200 Subject: [PATCH] Let no run read from pacman's log, nor a run whose transaction ended, hold an asked upgrade back; read one run of the addressed machine whole (issue 1841) Every run imported from the log ends at finished or failed and is never checked after, so the one-run-at-a-time rule counted each as still going: novox's ask of 2026-10-22 22:00 was refused on its install-time run 20220123T194900Z, and every node holds such runs. The rule now holds back a new run only for a run the mesh ran whose transaction may still go on. pacman_upgrades refused a run without its node, and the console takes node out of the arguments of an address that names the machine, so no run could be read whole: a run without a node is now that machine's. --- modules/pacman/cmd/pacman-tools/main.go | 9 +- modules/pacman/cmd/pacman-tools/rollback.go | 2 +- .../pacman/cmd/pacman-tools/stale_run_test.go | 115 ++++++++++++++++++ .../pacman/cmd/pacman-tools/transaction.go | 18 ++- 4 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 modules/pacman/cmd/pacman-tools/stale_run_test.go diff --git a/modules/pacman/cmd/pacman-tools/main.go b/modules/pacman/cmd/pacman-tools/main.go index 1df5256b..e10ccd46 100644 --- a/modules/pacman/cmd/pacman-tools/main.go +++ b/modules/pacman/cmd/pacman-tools/main.go @@ -248,12 +248,12 @@ func tools(m *Machine) []stdio.Tool { Name: "pacman_upgrades", Description: "Every full upgrade and rollback of every node, from the bus (ADR 0508 §1): each run's node, kind, what started it, " + "its round, when it began and ended, its outcome (refused, finished, failed or running) and whether its after-upgrade check passed; " + - "narrowed to a node and to days, newest first. With a node and a run, that run whole, every step with its time.", + "narrowed to a node and to days, newest first. With a run, that run whole, every step with its time: the node's named, else this one's.", Input: schema(map[string]any{ "node": map[string]any{"type": "string", "description": "only this node's runs"}, "from": sinceArg, "until": map[string]any{"type": "string", "description": "a day, YYYY-MM-DD, the last one listed"}, - "run": map[string]any{"type": "string", "description": "one run, by its id, with node"}, + "run": map[string]any{"type": "string", "description": "one run, by its id: of node, else of this node"}, "limit": limitArg(50, 1000), }), Run: func(args map[string]any) (any, error) { @@ -269,6 +269,11 @@ func tools(m *Machine) []stdio.Tool { if err != nil { return nil, err } + if v[3] != "" && v[0] == "" { + // An address naming the machine has its node taken out of the arguments (the console): + // the run is this machine's (issue 1841). + v[0] = thisNode() + } return ListUpgrades(upgradeState(StateRuns), v[0], v[1], v[2], v[3], n) }, }, diff --git a/modules/pacman/cmd/pacman-tools/rollback.go b/modules/pacman/cmd/pacman-tools/rollback.go index 690e0bb5..f7d315b9 100644 --- a/modules/pacman/cmd/pacman-tools/rollback.go +++ b/modules/pacman/cmd/pacman-tools/rollback.go @@ -228,7 +228,7 @@ func (u *Upgrades) StartRollback(of string, removeAdded bool, ask string) (Run, if o.Run.Run == r.Run.Run { return r.Run, fmt.Errorf("run %s of %s is there already: one run a second", o.Run.Run, u.Node) } - if !ended(o.Run) && o.From != FromLog { + if holdsTheDatabase(o.Run) { return r.Run, u.refuse(r, fmt.Sprintf("run %s is still going (its last step is %s): one run at a time", o.Run.Run, lastStep(o.Run))) } } diff --git a/modules/pacman/cmd/pacman-tools/stale_run_test.go b/modules/pacman/cmd/pacman-tools/stale_run_test.go new file mode 100644 index 00000000..39d9ecdf --- /dev/null +++ b/modules/pacman/cmd/pacman-tools/stale_run_test.go @@ -0,0 +1,115 @@ +package main + +// A run read from pacman's log, or a run whose transaction ended, never holds a new run back (issue 1841, the +// refusal of 2026-10-22 22:00 on novox: "run 20220123T194900Z is still going (its last step is finished): one +// run at a time"); and pacman_upgrades reads one run whole at an address that names the machine. + +import ( + "strings" + "testing" + "time" +) + +// novoxLogHead is the first twelve lines of novox's /var/log/pacman.log, captured 2026-10-22 22:10 with +// node-reads.read: the install's `pacman -Syu` with nothing to do, which became run 20220123T194900Z, its +// last step finished at the moment it began (pacman_upgrades on novox, 2026-10-22: from log, outcome +// finished, began 2022-01-23T19:49:00Z, ended 2022-01-23T20:49:00+01:00). +const novoxLogHead = "[2022-01-23T20:49:00+0100] [PACMAN] Running 'pacman -Syu'\n" + + "[2022-01-23T20:49:00+0100] [PACMAN] synchronizing package lists\n" + + "[2022-01-23T20:49:00+0100] [PACMAN] starting full system upgrade\n" + + "[2022-01-23T20:50:45+0100] [PACMAN] Running 'pacman -S nano'\n" + + "[2022-01-23T20:50:49+0100] [ALPM] transaction started\n" + + "[2022-01-23T20:50:50+0100] [ALPM] installed nano (6.0-1)\n" + + "[2022-01-23T20:50:50+0100] [ALPM] transaction completed\n" + + "[2022-01-23T20:50:50+0100] [ALPM] running '30-systemd-update.hook'...\n" + + "[2022-01-23T20:55:31+0100] [PACMAN] Running 'pacman -S docker'\n" + + "[2022-01-23T20:55:36+0100] [ALPM] transaction started\n" + + "[2022-01-23T20:55:36+0100] [ALPM] installed bridge-utils (1.7.1-1)\n" + + "[2022-01-23T20:55:36+0100] [ALPM] installed libtool (2.4.6+42+gb88cebd5-16)\n" + +// 1841/AC20: given the run read from novox's log, when an upgrade is asked, then it starts. +func TestARunReadFromTheLogIsHistoryAndNeverHoldsAnAsk(t *testing.T) { + k := newKeeperBed(t) + past := PastUpgrades("node-a", novoxLogHead) + if len(past) != 1 || past[0].Run != "20220123T194900Z" || steps(past[0]) != "started finished" || past[0].From != FromLog { + t.Fatalf("the captured log reads as the record novox holds: %+v", past) + } + if _, err := k.runs.Create(NodeKey("node-a", past[0].Run), past[0]); err != nil { + t.Fatal(err) + } + if _, err := k.u.StartUpgrade(FromAsk, "", "a1"); err != nil { + t.Fatal(err) + } + var r Run + k.runs.value("node-a.20261004T120000Z", &r) + if lastStep(r) == StepRefused || len(k.root.started) != 1 { + t.Errorf("a run read from the log held the ask back: %s %+v", steps(r), r.Steps) + } +} + +// 1841/AC21: given a run the mesh ran whose last step is finished, failed or refused (no after-upgrade check +// yet, no end time of the run itself), when another run would start, then the earlier one does not hold it; +// a run whose transaction may still go on (no step, recorded, checked before, started) does. +func TestARunWhoseTransactionEndedNeverHoldsTheNext(t *testing.T) { + at := time.Date(2026, 10, 22, 20, 0, 0, 0, time.UTC) + for last, holds := range map[string]bool{ + StepFinished: false, StepFailed: false, StepRefused: false, StepCheckedAfter: false, + "": true, StepRecorded: true, StepCheckedBefore: true, StepStarted: true, + } { + r := Run{Node: "node-a", Run: "20261022T200000Z", Kind: RunUpgrade, From: FromAsk} + if last != "" { + r.Steps = []Step{{Step: last, At: at}} + } + if got := holdsTheDatabase(r); got != holds { + t.Errorf("last step %q: holds %v, want %v", last, got, holds) + } + r.From = FromLog + if holdsTheDatabase(r) { + t.Errorf("a run read from the log, last step %q, holds the next", last) + } + } + + k := newKeeperBed(t) + done := Run{Node: "node-a", Run: "20261004T110000Z", Kind: RunUpgrade, From: FromAsk, + Steps: []Step{{Step: StepStarted, At: k.now}, {Step: StepFinished, At: k.now}}} + if _, err := k.runs.Create(NodeKey("node-a", done.Run), done); err != nil { + t.Fatal(err) + } + if _, err := k.u.StartUpgrade(FromAsk, "", "a1"); err != nil { + t.Fatal(err) + } + var r Run + k.runs.value("node-a.20261004T120000Z", &r) + if lastStep(r) == StepRefused { + t.Errorf("a finished run held the next: %+v", r.Steps) + } +} + +// 1841/AC22: given an address that names the machine (the console takes `node` out of the arguments), when +// pacman_upgrades is asked for one run, then that machine's run is answered whole. +func TestPacmanUpgradesReadsOneRunOfThisNodeWithoutANode(t *testing.T) { + runs := newBucket() + past := PastUpgrades("node-a", novoxLogHead)[0] + if _, err := runs.Create(NodeKey("node-a", past.Run), past); err != nil { + t.Fatal(err) + } + savedState, savedNode := upgradeState, thisNode + upgradeState = func(string) Bucket { return runs } + thisNode = func() string { return "node-a" } + defer func() { upgradeState, thisNode = savedState, savedNode }() + for _, tool := range tools(machine(nil, 1000)) { + if tool.Name != "pacman_upgrades" { + continue + } + out, err := tool.Run(map[string]any{"run": past.Run}) + if err != nil { + t.Fatalf("one run without a node: %v", err) + } + whole := out.(map[string]any)["run"].(Run) + if whole.Node != "node-a" || steps(whole) != "started finished" || !strings.Contains(whole.Steps[1].Why, "no transaction") { + t.Errorf("one run whole: %+v", whole) + } + return + } + t.Fatal("no pacman_upgrades") +} diff --git a/modules/pacman/cmd/pacman-tools/transaction.go b/modules/pacman/cmd/pacman-tools/transaction.go index df04b961..13fcd6ca 100644 --- a/modules/pacman/cmd/pacman-tools/transaction.go +++ b/modules/pacman/cmd/pacman-tools/transaction.go @@ -63,6 +63,22 @@ func ended(r Run) bool { return false } +// holdsTheDatabase says whether a run holds a new run back (one run at a time, ADR 0508 §8): only a run the +// mesh ran whose transaction may still go on. A run read from pacman's log is history (ADR 0508 §1), and a run +// whose last step is finished, failed, refused or checked after has no transaction left: its after-upgrade +// check, still to be written by Follow, reads and changes nothing (issue 1841: every run read from the log +// ended at finished and held every ask back). +func holdsTheDatabase(r Run) bool { + if r.From == FromLog { + return false + } + switch lastStep(r) { + case StepFinished, StepFailed, StepRefused, StepCheckedAfter: + return false + } + return true +} + // ownRuns are the node's runs, oldest first, each with the revision of its key. func (u *Upgrades) ownRuns() ([]openRun, error) { keys, err := u.Runs.Keys() @@ -117,7 +133,7 @@ func (u *Upgrades) startUpgrade(r *openRun) error { if o.Run.Run == r.Run.Run { return fmt.Errorf("run %s of %s is there already: one run a second", o.Run.Run, u.Node) } - if !ended(o.Run) { + if holdsTheDatabase(o.Run) { return u.refuse(r, fmt.Sprintf("run %s is still going (its last step is %s): one run at a time", o.Run.Run, lastStep(o.Run))) } }