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