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:
@@ -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)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user