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",
|
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, " +
|
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; " +
|
"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{
|
Input: schema(map[string]any{
|
||||||
"node": map[string]any{"type": "string", "description": "only this node's runs"},
|
"node": map[string]any{"type": "string", "description": "only this node's runs"},
|
||||||
"from": sinceArg,
|
"from": sinceArg,
|
||||||
"until": map[string]any{"type": "string", "description": "a day, YYYY-MM-DD, the last one listed"},
|
"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),
|
"limit": limitArg(50, 1000),
|
||||||
}),
|
}),
|
||||||
Run: func(args map[string]any) (any, error) {
|
Run: func(args map[string]any) (any, error) {
|
||||||
@@ -269,6 +269,11 @@ func tools(m *Machine) []stdio.Tool {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
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 {
|
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)
|
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)))
|
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
|
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.
|
// ownRuns are the node's runs, oldest first, each with the revision of its key.
|
||||||
func (u *Upgrades) ownRuns() ([]openRun, error) {
|
func (u *Upgrades) ownRuns() ([]openRun, error) {
|
||||||
keys, err := u.Runs.Keys()
|
keys, err := u.Runs.Keys()
|
||||||
@@ -117,7 +133,7 @@ func (u *Upgrades) startUpgrade(r *openRun) error {
|
|||||||
if o.Run.Run == r.Run.Run {
|
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)
|
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)))
|
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