issue 1840/S7: prove updates is read at start, after a transaction, after the mirrors and daily, and written only on change

A failed read keeps the last list and says reachable false, so a list the node can no longer confirm is
never shown as current, and a first read that fails is never an empty list taken for an answer.
This commit is contained in:
2026-10-22 14:33:20 +02:00
parent 5cc401683c
commit 56b285d3e8
@@ -0,0 +1,147 @@
package main
// What an upgrade would change (novox/hq ADR 0507 §4; acceptance 1840/AC4): read at the keeper's start, after a
// transaction, after the mirror list is rewritten and a day after the last read; written only when the list or
// reachable changed; a failed read says reachable false and keeps the last list.
import (
"context"
"encoding/json"
"fmt"
"strings"
"sync"
"testing"
"time"
)
func updatesOf(t *testing.T, s *fakeStore) UpdatesValue {
t.Helper()
e, _ := s.Get("node-a")
if e == nil {
t.Fatal("no updates.node-a")
}
var v UpdatesValue
if err := json.Unmarshal(e.Value, &v); err != nil {
t.Fatal(err)
}
return v
}
func (d *pacmanDB) checkupdatesRuns() int {
d.mu.Lock()
defer d.mu.Unlock()
n := 0
for _, c := range d.commands {
if c == "checkupdates" {
n++
}
}
return n
}
func TestUpdatesAreWrittenOnlyWhenTheListOrReachableChanged(t *testing.T) {
db := &pacmanDB{sl: syncDBs, checkup: Ran{Stdout: "zsh 5.9-1 -> 5.9-2\nlinux 6.17.1-1 -> 6.17.2-1\n"}}
db.set("glibc 2.42-1\n", "")
k, states, _, _, _ := newTestKeeper(db)
up := states.store(StateUpdates)
k.refreshUpdates()
if w := up.Writes(); strings.Join(w, ",") != "put node-a" {
t.Fatalf("writes: %v", w)
}
if v := updatesOf(t, up); !v.Reachable || fmt.Sprint(v.Packages) != "[{linux 6.17.1-1 6.17.2-1} {zsh 5.9-1 5.9-2}]" {
t.Fatalf("%+v", v)
}
// the same answer: nothing written.
k.refreshUpdates()
if w := up.Writes(); len(w) != 0 {
t.Fatalf("the same list was written again: %v", w)
}
// the mirrors cannot be reached: reachable false, the last list kept.
db.setUpdates(Ran{Status: 1, Stderr: "==> ERROR: Cannot fetch updates\n"})
k.refreshUpdates()
if w := up.Writes(); len(w) != 1 {
t.Fatalf("writes: %v", w)
}
if v := updatesOf(t, up); v.Reachable || len(v.Packages) != 2 {
t.Fatalf("a failed read keeps the list and says reachable false: %+v", v)
}
k.refreshUpdates()
if w := up.Writes(); len(w) != 0 {
t.Fatalf("still unreachable, and written again: %v", w)
}
// reached again with nothing to do (status 2): an empty list, reachable.
db.setUpdates(Ran{Status: 2})
k.refreshUpdates()
if v := updatesOf(t, up); !v.Reachable || v.Packages == nil || len(v.Packages) != 0 {
t.Fatalf("%+v", v)
}
}
func TestAFirstReadThatFailsSaysUnreachableNeverAnEmptyListAsAnAnswer(t *testing.T) {
db := &pacmanDB{sl: syncDBs, checkup: Ran{Status: 1, Stderr: "==> ERROR: Cannot fetch updates\n"}}
k, states, _, _, _ := newTestKeeper(db)
k.refreshUpdates()
if v := updatesOf(t, states.store(StateUpdates)); v.Reachable {
t.Fatalf("%+v", v)
}
raw, _ := states.store(StateUpdates).Get("node-a")
if string(raw.Value) != `{"reachable":false,"packages":[]}` {
t.Errorf("%s", raw.Value)
}
}
// Read at the start, after a transaction, after the mirror list is rewritten, and a day after the last read
// with nothing else asking; never in between.
func TestUpdatesAreReadAtStartAfterATransactionAfterTheMirrorsAndDaily(t *testing.T) {
db := &pacmanDB{sl: syncDBs, checkup: Ran{Status: 2}}
db.set("glibc 2.42-1\n", "")
k, states, _, clock, w := newTestKeeper(db)
var mu sync.Mutex
var seen []string
k.Hooks.AfterTransaction = []func(*Keeper, Transaction) error{func(*Keeper, Transaction) error {
mu.Lock()
seen = append(seen, "transaction")
mu.Unlock()
return nil
}}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
done := make(chan error, 1)
go func() { done <- k.Run(ctx) }()
reads := func(want int) {
t.Helper()
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(time.Millisecond) {
if db.checkupdatesRuns() == want {
return
}
}
t.Fatalf("checkupdates ran %d times, want %d", db.checkupdatesRuns(), want)
}
reads(1)
db.setUpdates(Ran{Stdout: "zsh 5.9-1 -> 5.9-2\n"})
clock.Advance(time.Hour)
w.c <- MirrorsRewritten
reads(2)
db.setUpdates(Ran{Status: 2})
w.c <- LockReleased
reads(3)
waitFor(t, &mu, &seen, "transaction")
clock.Advance(UpdatesEvery - time.Minute)
time.Sleep(50 * time.Millisecond)
if n := db.checkupdatesRuns(); n != 3 {
t.Fatalf("read before a day passed: %d", n)
}
db.setUpdates(Ran{Stdout: "glibc 2.42-1 -> 2.42-2\n"})
clock.Advance(time.Minute)
reads(4)
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(time.Millisecond) {
if e, _ := states.store(StateUpdates).Get("node-a"); e != nil && strings.Contains(string(e.Value), "glibc") {
break
}
}
if w := states.store(StateUpdates).Writes(); len(w) != 4 {
t.Errorf("each read that changed the list wrote it, and only those: %v", w)
}
cancel()
<-done
}