Merge pull request 'Let a run read from a node's pacman log never hold back a new upgrade, so the mesh can upgrade nodes at all (issue 1841)' (#454) from fix/1841-stale-run into main

This commit was merged in pull request #454.
This commit is contained in:
2026-10-22 20:14:07 +00:00
4 changed files with 140 additions and 4 deletions
+7 -2
View File
@@ -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)
},
},
+1 -1
View File
@@ -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)))
}
}
@@ -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 the control-node: "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"
)
// installLogHead is the first twelve lines of the control-node'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 there, 2026-10-22: from log, outcome finished,
// began 2022-01-23T19:49:00Z, ended 2022-01-23T20:49:00+01:00).
const installLogHead = "[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 the control-node's log, when an upgrade is asked, then it starts.
func TestARunReadFromTheLogIsHistoryAndNeverHoldsAnAsk(t *testing.T) {
k := newKeeperBed(t)
past := PastUpgrades("node-a", installLogHead)
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 the control-node 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", installLogHead)[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")
}
+17 -1
View File
@@ -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)))
}
}