pacman keeps one mirror list for the mesh, ranked in the write that opens each round and written by every node (ADR 0585, issue 1876)

Each node's reflector ranked its own list on its own timer, and on 2026-10-31 one node's
lagging first mirror left it a Node.js version behind the rest. The list is now the
module's state mirrors, key list: the node that opens a week's round ranks it with the
module's reflector options, keeps only mirrors whose last sync is no older than the
newest found, and the round's key names the list's revision. Every keeper writes
/etc/pacman.d/mirrorlist whole from the key through the root helper's new verb
mirrorlist, when the key changes and when the file differs, and never writes an empty
or unreadable list. reflector.timer is declared stopped and disabled, and reflector.conf
saves nothing. pacman_mirrors answers the shared list, its ranker and age and whether
this node's file matches; with refresh it asks this node's keeper to rank again,
refused while a round runs, naming it. The round's integration test proves the opening
write and both nodes' files.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-01 16:18:22 +01:00
parent 37613ec6a9
commit 2b495c4818
19 changed files with 770 additions and 111 deletions
@@ -25,6 +25,7 @@ import (
type countingRoot struct {
mu sync.Mutex
started []string
mirrors []string
}
func (r *countingRoot) Transaction(kind, run string, ignore []string) error {
@@ -36,6 +37,20 @@ func (r *countingRoot) Transaction(kind, run string, ignore []string) error {
func (r *countingRoot) Rollback(string, []RollbackItem) error { return nil }
func (r *countingRoot) Journal(string, int) ([]string, error) { return nil, nil }
func (r *countingRoot) Maps() ([]Mapped, error) { return []Mapped{}, nil }
// Mirrorlist keeps the mirror list written last (ADR 0585 §3).
func (r *countingRoot) Mirrorlist(servers []string) error {
r.mu.Lock()
defer r.mu.Unlock()
r.mirrors = append([]string(nil), servers...)
return nil
}
func (r *countingRoot) mirrorlist() []string {
r.mu.Lock()
defer r.mu.Unlock()
return r.mirrors
}
func (r *countingRoot) count() int {
r.mu.Lock()
defer r.mu.Unlock()
@@ -69,20 +84,30 @@ func startRoundNode(t *testing.T, ctx context.Context, url, node string, order [
began := time.Now()
m.Now = func() time.Time { return t0.Add(time.Since(began)) }
m.ReadFile = func(path string) ([]byte, error) {
if path == OSRelease {
switch path {
case OSRelease:
return []byte("7.2.8-arch1-2\n"), nil
case ReflectorConf:
return []byte("--sort rate\n"), nil
}
return nil, errNoFile
}
n := &roundNode{root: &countingRoot{}, signals: make(chan Signal)}
k := &Keeper{Config: KeeperConfig{UpgradeOrder: order, RecordDays: 180, RoundStartMinutes: 120}, Machine: m, Clock: realClock{},
Watcher: fakeWatcher{n.signals}, Logf: func(f string, a ...any) { t.Logf(node+": "+f, a...) }, MakeHooks: keeperHooks,
Root: n.root, Check: func(Run, *Record) Check { return Check{Passed: passes} }}
Root: n.root, Check: func(Run, *Record) Check { return Check{Passed: passes} },
// The mesh's mirrors, ranked by the node that opens the round (ADR 0585 §2): reflector stood in for.
Rank: func(_ []byte, round string) (MirrorList, error) {
return MirrorList{Servers: roundMirrors, RankedAt: t0, RankedBy: node, Round: round}, nil
}}
account := process.Account{URL: url, Node: node, Module: "pacman", User: node + ".pacman.keeper", Password: "pw"}
go func() { _ = runKeeper(ctx, account, k) }()
return n
}
// roundMirrors is the mirror list the round's opener ranks.
var roundMirrors = []string{"https://nl.mirror.example/archlinux/$repo/os/$arch", "https://de.mirror.example/$repo/os/$arch"}
func roundBus(t *testing.T) (string, map[string]jetstream.KeyValue) {
t.Helper()
url := newTestBus(t)
@@ -152,6 +177,22 @@ func TestTheRoundGoesNodeByNodeOnARealBus(t *testing.T) {
if a.root.count() != 1 {
t.Errorf("node-a ran again: %v", a.root.started)
}
// ADR 0585 §2, §3: the round opened naming the revision of the list node-a ranked in the write that opened it, and
// both nodes wrote that list whole to their mirror list file, from the bus.
e, err := buckets[StateRounds].Get(context.Background(), round)
var opened Round
if err == nil {
_ = json.Unmarshal(e.Value(), &opened)
}
l, lerr := buckets[StateMirrors].Get(context.Background(), MirrorsKey)
if err != nil || lerr != nil || opened.Mirrors == 0 || opened.Mirrors != l.Revision() || !strings.Contains(string(l.Value()), `"round":"`+round+`"`) {
t.Errorf("the round opened with mirrors %d (%q), the list %v (%v, %v)", opened.Mirrors, opened.MirrorsNote, l, err, lerr)
}
for _, n := range []*roundNode{a, b} {
eventually(t, "each node wrote the mesh's mirror list", 10*time.Second, func() bool {
return strings.Join(n.root.mirrorlist(), " ") == strings.Join(roundMirrors, " ")
})
}
}
func TestAFailedCheckStopsTheRoundOnARealBus(t *testing.T) {