Merge pull request 'pacman's mirror list keeps a fallback tail; a failed check leads the merge ask's headline (ADR 0586, issues 1876, 1166)' (#589) from fix/mirror-tail-headline into main
This commit was merged in pull request #589.
This commit is contained in:
@@ -169,6 +169,16 @@ func approveCaveat(c checkSaid) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mergeHeadline opens the ask's headline: "Merge into main: ", and "Check failed - merge into main: " once the check failed,
|
||||||
|
// since a merge is not gated on the check and the headline is the phone's only warning (issue 1166). A passed or still
|
||||||
|
// running check keeps the plain headline. The prefix leads, so phone's cut of a long headline never drops it.
|
||||||
|
func mergeHeadline(c checkSaid) string {
|
||||||
|
if c.Key == checkSaidFail {
|
||||||
|
return "Check failed \u2014 merge into main: "
|
||||||
|
}
|
||||||
|
return "Merge into main: "
|
||||||
|
}
|
||||||
|
|
||||||
// composeMergeAsk is the ask for a delivery, or for a group's members together.
|
// composeMergeAsk is the ask for a delivery, or for a group's members together.
|
||||||
// check is the state of the check at the heads it covers (checkAt): the ask says "passed its check" only when it did, and
|
// check is the state of the check at the heads it covers (checkAt): the ask says "passed its check" only when it did, and
|
||||||
// otherwise that Approve merges before the check ends, or although it failed (issue 1733).
|
// otherwise that Approve merges before the check ends, or although it failed (issue 1733).
|
||||||
@@ -193,7 +203,7 @@ func composeMergeAsk(id string, d *Delivery, group string, members []*Delivery,
|
|||||||
pullSaid(m), m.Title, shortOf(m.Commit, 12), issuesSaid([]*Delivery{m}), words, touches))
|
pullSaid(m), m.Title, shortOf(m.Commit, 12), issuesSaid([]*Delivery{m}), words, touches))
|
||||||
}
|
}
|
||||||
if group == "" {
|
if group == "" {
|
||||||
a.Headline = phone("Merge into main: "+pullSaid(d)+" "+d.Title, asks.HeadlineLength)
|
a.Headline = phone(mergeHeadline(check)+pullSaid(d)+" "+d.Title, asks.HeadlineLength)
|
||||||
summary := fmt.Sprintf("%s %s%s. Approve merges it into main at %s%s; Decline leaves it unmerged.",
|
summary := fmt.Sprintf("%s %s%s. Approve merges it into main at %s%s; Decline leaves it unmerged.",
|
||||||
pullSaid(d), d.Title, checkClause(check, " "), shortOf(d.Commit, 12), approveCaveat(check))
|
pullSaid(d), d.Title, checkClause(check, " "), shortOf(d.Commit, 12), approveCaveat(check))
|
||||||
if unreviewed != "" {
|
if unreviewed != "" {
|
||||||
@@ -210,7 +220,7 @@ func composeMergeAsk(id string, d *Delivery, group string, members []*Delivery,
|
|||||||
for _, m := range members {
|
for _, m := range members {
|
||||||
names = append(names, pullSaid(m))
|
names = append(names, pullSaid(m))
|
||||||
}
|
}
|
||||||
a.Headline = phone("Merge into main: group "+group+" ("+strings.Join(names, ", ")+")", asks.HeadlineLength)
|
a.Headline = phone(mergeHeadline(check)+"group "+group+" ("+strings.Join(names, ", ")+")", asks.HeadlineLength)
|
||||||
caveat := approveCaveat(check)
|
caveat := approveCaveat(check)
|
||||||
if caveat != "" {
|
if caveat != "" {
|
||||||
caveat = "," + caveat
|
caveat = "," + caveat
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||||
sdk "git.novox.be/novox/mesh-sdk/go/delivery"
|
sdk "git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
@@ -42,6 +43,17 @@ func TestAReadyGroupWritesOneAskKeyWithTodaysWordsAndCallsNoRouter(t *testing.T)
|
|||||||
!strings.Contains(ask.Ask.Details, "mesh-catalog#2") || len(ask.Ask.Options) != 2 || ask.Ask.Who != asks.Operator {
|
!strings.Contains(ask.Ask.Details, "mesh-catalog#2") || len(ask.Ask.Options) != 2 || ask.Ask.Who != asks.Operator {
|
||||||
t.Fatalf("the ask is not today's merge ask: %+v", ask.Ask)
|
t.Fatalf("the ask is not today's merge ask: %+v", ask.Ask)
|
||||||
}
|
}
|
||||||
|
// A group's headline says a failed check first (issue 1166); a passed or a running one keeps the plain words.
|
||||||
|
members := []*Delivery{{Repository: "novox/mesh-controller", Number: 1}, {Repository: "novox/mesh-catalog", Number: 2}}
|
||||||
|
for _, c := range []struct{ key, head string }{
|
||||||
|
{checkSaidFail, "Check failed \u2014 merge into main: group feat"},
|
||||||
|
{checkSaidPass, "Merge into main: group feat"},
|
||||||
|
{"running", "Merge into main: group feat"},
|
||||||
|
} {
|
||||||
|
if h := composeMergeAsk("id", members[0], "feat/g", members, nil, checkSaid{Key: c.key, Words: "w"}, time.Time{}).Headline; !strings.HasPrefix(h, c.head) {
|
||||||
|
t.Fatalf("a %s check gives the headline %q, wanted it to start %q", c.key, h, c.head)
|
||||||
|
}
|
||||||
|
}
|
||||||
// The act the Approve binds is the one the teller checks and the controller verifies: this ask's router id, the option, and
|
// The act the Approve binds is the one the teller checks and the controller verifies: this ask's router id, the option, and
|
||||||
// the heads as `owner/repo@<first 12 characters>` to the whole commit.
|
// the heads as `owner/repo@<first 12 characters>` to the whole commit.
|
||||||
approve, _ := ask.Ask.Option(OptionApprove)
|
approve, _ := ask.Ask.Option(OptionApprove)
|
||||||
|
|||||||
@@ -80,8 +80,9 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
|
|||||||
after time.Duration
|
after time.Duration
|
||||||
asks, open int
|
asks, open int
|
||||||
want, not []string
|
want, not []string
|
||||||
|
head string // the ask's headline starts with it
|
||||||
}{
|
}{
|
||||||
{name: "gated, passed", gated: true, state: "pass", asks: 1,
|
{name: "gated, passed", gated: true, state: "pass", asks: 1, head: "Merge into main: app#7 Fix it",
|
||||||
want: []string{"app#7 Fix it passed its check. Approve merges it into main at " + readyHead[:12] + ";"},
|
want: []string{"app#7 Fix it passed its check. Approve merges it into main at " + readyHead[:12] + ";"},
|
||||||
not: []string{"before its check ends", "no verdict yet"}},
|
not: []string{"before its check ends", "no verdict yet"}},
|
||||||
{name: "ungated, no verdict", asks: 0, want: []string{waitsCheckBeforeAsking + "its check has no verdict yet"}, not: []string{"anyway"}},
|
{name: "ungated, no verdict", asks: 0, want: []string{waitsCheckBeforeAsking + "its check has no verdict yet"}, not: []string{"anyway"}},
|
||||||
@@ -98,6 +99,7 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
|
|||||||
{name: "ungated, passed, its verdict gone", state: "pass", then: "gone", asks: 1, open: -1,
|
{name: "ungated, passed, its verdict gone", state: "pass", then: "gone", asks: 1, open: -1,
|
||||||
want: []string{waitsCheckBeforeAsking + "its check has no verdict yet"}, not: []string{"anyway"}},
|
want: []string{waitsCheckBeforeAsking + "its check has no verdict yet"}, not: []string{"anyway"}},
|
||||||
{name: "ungated, failed", state: "fail", failed: "TestThing: wanted 1, got 2", asks: 1,
|
{name: "ungated, failed", state: "fail", failed: "TestThing: wanted 1, got 2", asks: 1,
|
||||||
|
head: "Check failed \u2014 merge into main: app#7 Fix it",
|
||||||
want: []string{"its check failed: TestThing: wanted 1, got 2", "although its check failed"}, not: []string{"passed", "anyway"}},
|
want: []string{"its check failed: TestThing: wanted 1, got 2", "although its check failed"}, not: []string{"passed", "anyway"}},
|
||||||
} {
|
} {
|
||||||
t.Run(row.name, func(t *testing.T) {
|
t.Run(row.name, func(t *testing.T) {
|
||||||
@@ -143,6 +145,9 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if h := v.op.ask(open[0]).Headline; row.head != "" && !strings.HasPrefix(h, row.head) {
|
||||||
|
t.Fatalf("the headline is %q; wanted it to start %q", h, row.head)
|
||||||
|
}
|
||||||
e := v.op.ask(open[0]).Explanation
|
e := v.op.ask(open[0]).Explanation
|
||||||
for _, w := range row.want {
|
for _, w := range row.want {
|
||||||
if !strings.Contains(e, w) {
|
if !strings.Contains(e, w) {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ var busFields = map[string][]string{
|
|||||||
"Record": {"node", "run", "at", "installed", "kernel", "failed_units", "dkms"},
|
"Record": {"node", "run", "at", "installed", "kernel", "failed_units", "dkms"},
|
||||||
"DKMSModule": {"module", "version", "kernels"},
|
"DKMSModule": {"module", "version", "kernels"},
|
||||||
"Round": {"round", "opened", "opened_by", "order", "mirrors", "mirrors_note"},
|
"Round": {"round", "opened", "opened_by", "order", "mirrors", "mirrors_note"},
|
||||||
"MirrorList": {"servers", "ranked_at", "ranked_by", "options", "newest_sync", "round"},
|
"MirrorList": {"servers", "ranked_at", "ranked_by", "options", "newest_sync", "freshest", "round"},
|
||||||
"RoundNode": {"round", "node", "run", "pass"},
|
"RoundNode": {"round", "node", "run", "pass"},
|
||||||
"Pass": {"reason", "ask", "asked", "answer", "answered"},
|
"Pass": {"reason", "ask", "asked", "answer", "answered"},
|
||||||
"Ask": {"id", "node", "act", "at", "of", "remove_added", "round", "pass", "reason"},
|
"Ask": {"id", "node", "act", "at", "of", "remove_added", "round", "pass", "reason"},
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -165,20 +166,48 @@ func ParseReflectorInfo(text string) []rankedMirror {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// Freshest keeps, in their order, only the mirrors whose last sync is no older than the newest found (ADR 0585 §2), so
|
// TailLength is how many of the next freshest mirrors follow the freshest ones in the list (ADR 0586 §1).
|
||||||
// a mirror lagging behind the others is never in the list; one whose last sync is unknown is left out.
|
const TailLength = 4
|
||||||
func Freshest(ranked []rankedMirror) (servers []string, newest time.Time) {
|
|
||||||
|
// Freshest orders the mirrors by last sync, newest first, in reflector's order among equals, and keeps every mirror
|
||||||
|
// whose last sync equals the newest found (the freshest, count of them) followed by up to TailLength of the next
|
||||||
|
// freshest (ADR 0586 §1, which changes ADR 0585 §2); one whose last sync is unknown (zero) is left out.
|
||||||
|
func Freshest(ranked []rankedMirror) (servers []string, newest time.Time, freshest int) {
|
||||||
|
var known []rankedMirror
|
||||||
for _, m := range ranked {
|
for _, m := range ranked {
|
||||||
if m.LastSync.After(newest) {
|
if !m.LastSync.IsZero() {
|
||||||
newest = m.LastSync
|
known = append(known, m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, m := range ranked {
|
sort.SliceStable(known, func(i, j int) bool { return known[i].LastSync.After(known[j].LastSync) })
|
||||||
if !m.LastSync.IsZero() && !m.LastSync.Before(newest) {
|
if len(known) == 0 {
|
||||||
servers = append(servers, m.URL)
|
return nil, newest, 0
|
||||||
|
}
|
||||||
|
newest = known[0].LastSync
|
||||||
|
for _, m := range known {
|
||||||
|
if m.LastSync.Equal(newest) {
|
||||||
|
freshest++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, m := range known {
|
||||||
|
if i >= freshest+TailLength {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
servers = append(servers, m.URL)
|
||||||
|
}
|
||||||
|
return servers, newest, freshest
|
||||||
|
}
|
||||||
|
|
||||||
|
// withoutUnknownSync zeroes the last sync of every mirror that reflector printed as the moment it ran: --info writes an
|
||||||
|
// unknown last sync (None) through gmtime(None), which is the current time, so a last sync at or after start (the
|
||||||
|
// second the run began) cannot be a real one, since the status reflector reads is older than the run.
|
||||||
|
func withoutUnknownSync(ranked []rankedMirror, start time.Time) {
|
||||||
|
start = start.UTC().Truncate(time.Second)
|
||||||
|
for i := range ranked {
|
||||||
|
if !ranked[i].LastSync.Before(start) {
|
||||||
|
ranked[i].LastSync = time.Time{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return servers, newest
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// reflectorRun runs reflector with args on this machine, within RankTimeout; its cache in the temporary directory, since
|
// reflectorRun runs reflector with args on this machine, within RankTimeout; its cache in the temporary directory, since
|
||||||
@@ -197,11 +226,12 @@ func RankMirrors(conf []byte, node string, now time.Time, round string) (MirrorL
|
|||||||
return MirrorList{}, failure("reflector", "reflector", r)
|
return MirrorList{}, failure("reflector", "reflector", r)
|
||||||
}
|
}
|
||||||
ranked := ParseReflectorInfo(r.Stdout)
|
ranked := ParseReflectorInfo(r.Stdout)
|
||||||
servers, newest := Freshest(ranked)
|
withoutUnknownSync(ranked, now)
|
||||||
|
servers, newest, freshest := Freshest(ranked)
|
||||||
if err := checkServers(servers); err != nil {
|
if err := checkServers(servers); err != nil {
|
||||||
return MirrorList{}, fmt.Errorf("reflector ranked no list the mesh can use (%d mirrors read): %w", len(ranked), err)
|
return MirrorList{}, fmt.Errorf("reflector ranked no list the mesh can use (%d mirrors read): %w", len(ranked), err)
|
||||||
}
|
}
|
||||||
return MirrorList{Servers: servers, RankedAt: now.UTC(), RankedBy: node, Options: opts, Newest: newest, Round: round}, nil
|
return MirrorList{Servers: servers, RankedAt: now.UTC(), RankedBy: node, Options: opts, Newest: newest, Freshest: freshest, Round: round}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readMirrorList is the shared list and its revision; nil when there is none yet.
|
// readMirrorList is the shared list and its revision; nil when there is none yet.
|
||||||
@@ -379,6 +409,10 @@ func (m *Machine) MirrorsView(refresh bool) (map[string]any, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
out["shared"], out["revision"] = list, rev
|
out["shared"], out["revision"] = list, rev
|
||||||
|
if list != nil {
|
||||||
|
n := min(max(list.Freshest, 1), len(list.Servers))
|
||||||
|
out["freshest"], out["tail"] = list.Servers[:n], list.Servers[n:]
|
||||||
|
}
|
||||||
text, ferr := m.ReadFile(Mirrorlist)
|
text, ferr := m.ReadFile(Mirrorlist)
|
||||||
if ferr != nil {
|
if ferr != nil {
|
||||||
out["file_error"] = ferr.Error()
|
out["file_error"] = ferr.Error()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -306,7 +307,10 @@ https://fr.mirror.example/archlinux/$repo/os/$arch
|
|||||||
last_sync : None
|
last_sync : None
|
||||||
`
|
`
|
||||||
|
|
||||||
// ADR 0585 §2, §4: the ranking keeps only mirrors as fresh as the newest, in reflector's order, with the module's options
|
// rankedAt is after every last sync in reflectorInfo: reflector ran then.
|
||||||
|
var rankedAt = time.Date(2026, 11, 1, 15, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
// ADR 0586 §1, ADR 0585 §4: the ranking keeps the mirrors at the newest sync and a tail of the next freshest, in reflector's order, with the module's options
|
||||||
// and never its --save; a ranking that fails or keeps none is no list. pacman_mirrors answers the shared list, its
|
// and never its --save; a ranking that fails or keeps none is no list. pacman_mirrors answers the shared list, its
|
||||||
// ranker and age and whether this node's file matches it (the file as found when not); with refresh it asks this node's
|
// ranker and age and whether this node's file matches it (the file as found when not); with refresh it asks this node's
|
||||||
// keeper to rank again, refused while a round runs, naming it.
|
// keeper to rank again, refused while a round runs, naming it.
|
||||||
@@ -320,18 +324,19 @@ func TestTheMeshsMirrorListIsRankedFreshAndAnsweredWithThisNodesFile(t *testing.
|
|||||||
saved := reflectorRun
|
saved := reflectorRun
|
||||||
reflectorRun = func(_ context.Context, args ...string) Ran { asked = args; return answer }
|
reflectorRun = func(_ context.Context, args ...string) Ran { asked = args; return answer }
|
||||||
defer func() { reflectorRun = saved }()
|
defer func() { reflectorRun = saved }()
|
||||||
l, err := RankMirrors([]byte(conf), "node-a", t0, "2026-W43")
|
l, err := RankMirrors([]byte(conf), "node-a", rankedAt, "2026-W43")
|
||||||
if err != nil || strings.Join(l.Servers, " ") != "https://nl.mirror.example/archlinux/$repo/os/$arch https://de.mirror.example/$repo/os/$arch" ||
|
if err != nil || strings.Join(l.Servers, " ") != "https://nl.mirror.example/archlinux/$repo/os/$arch https://de.mirror.example/$repo/os/$arch https://be.mirror.example/archlinux/$repo/os/$arch" || l.Freshest != 2 ||
|
||||||
!l.Newest.Equal(time.Date(2026, 11, 1, 14, 32, 1, 0, time.UTC)) || l.RankedBy != "node-a" || l.Round != "2026-W43" ||
|
!l.Newest.Equal(time.Date(2026, 11, 1, 14, 32, 1, 0, time.UTC)) || l.RankedBy != "node-a" || l.Round != "2026-W43" ||
|
||||||
strings.Join(asked, " ") != "--country Belgium,Netherlands --sort rate --info" {
|
strings.Join(asked, " ") != "--country Belgium,Netherlands --sort rate --info" {
|
||||||
t.Fatalf("ranked %+v (%v), reflector asked %v", l, err, asked)
|
t.Fatalf("ranked %+v (%v), reflector asked %v", l, err, asked)
|
||||||
}
|
}
|
||||||
for _, a := range []Ran{{Status: 1, Stderr: "error: no mirrors"}, {Stdout: "https://x.example/$repo/os/$arch\nlast_sync : None\n"}} {
|
for _, a := range []Ran{{Status: 1, Stderr: "error: no mirrors"}, {Stdout: "https://x.example/$repo/os/$arch\nlast_sync : None\n"}} {
|
||||||
answer = a
|
answer = a
|
||||||
if _, err := RankMirrors([]byte(conf), "node-a", t0, ""); err == nil {
|
if _, err := RankMirrors([]byte(conf), "node-a", rankedAt, ""); err == nil {
|
||||||
t.Errorf("a ranking from %+v is a list", a)
|
t.Errorf("a ranking from %+v is a list", a)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
checkFreshest(t)
|
||||||
if checkServers([]string{"https://ok.example/$repo/os/$arch\nServer = https://evil.example/$repo/os/$arch"}) == nil {
|
if checkServers([]string{"https://ok.example/$repo/os/$arch\nServer = https://evil.example/$repo/os/$arch"}) == nil {
|
||||||
t.Error("a server carrying a second line is taken")
|
t.Error("a server carrying a second line is taken")
|
||||||
}
|
}
|
||||||
@@ -343,7 +348,7 @@ func TestTheMeshsMirrorListIsRankedFreshAndAnsweredWithThisNodesFile(t *testing.
|
|||||||
return Ran{Stdout: "LoadState=loaded\nUnitFileState=disabled\nActiveState=inactive\n"}
|
return Ran{Stdout: "LoadState=loaded\nUnitFileState=disabled\nActiveState=inactive\n"}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
m.Node, m.States = "node-a", readerOf(states)
|
m.Node, m.States = "node-a", readerOf(states)
|
||||||
m.Now = func() time.Time { return t0.Add(90 * time.Minute) }
|
m.Now = func() time.Time { return rankedAt.Add(90 * time.Minute) }
|
||||||
m.ReadFile = func(string) ([]byte, error) { return file, nil }
|
m.ReadFile = func(string) ([]byte, error) { return file, nil }
|
||||||
v, err := m.MirrorsView(false)
|
v, err := m.MirrorsView(false)
|
||||||
if err != nil || v["file_matches"] != true || v["ranked_by"] != "node-a" || v["age"] != "1h30m0s" || v["file"] != nil ||
|
if err != nil || v["file_matches"] != true || v["ranked_by"] != "node-a" || v["age"] != "1h30m0s" || v["file"] != nil ||
|
||||||
@@ -382,3 +387,46 @@ func TestConfigIsReadAsPacmanConfResolvesIt(t *testing.T) {
|
|||||||
t.Fatalf("%+v", c)
|
t.Fatalf("%+v", c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ADR 0586 §1: the freshest first, then up to four of the next freshest, newest first; an unknown last sync is left out,
|
||||||
|
// whether reflector wrote None or the moment it ran (gmtime(None)).
|
||||||
|
func checkFreshest(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
at := func(m int) time.Time { return time.Date(2026, 11, 1, 14, m, 0, 0, time.UTC) }
|
||||||
|
mk := func(syncs ...int) []rankedMirror {
|
||||||
|
var r []rankedMirror
|
||||||
|
for i, m := range syncs {
|
||||||
|
mm := rankedMirror{URL: fmt.Sprintf("m%d", i)}
|
||||||
|
if m >= 0 {
|
||||||
|
mm.LastSync = at(m)
|
||||||
|
}
|
||||||
|
r = append(r, mm)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
for _, c := range []struct {
|
||||||
|
name string
|
||||||
|
ranked []rankedMirror
|
||||||
|
servers string
|
||||||
|
fresh int
|
||||||
|
}{
|
||||||
|
{"all fresh", mk(30, 30, 30), "m0 m1 m2", 3},
|
||||||
|
{"one fresh and a tail of four", mk(10, 30, 20, 25, 5, 1), "m1 m3 m2 m0 m4", 1},
|
||||||
|
{"more than four candidates", mk(30, 29, 28, 27, 26, 25, 24), "m0 m1 m2 m3 m4", 1},
|
||||||
|
{"two fresh and the tail", mk(30, 20, 30, 10, 11, 12, 13), "m0 m2 m1 m6 m5 m4", 2},
|
||||||
|
{"unknown last sync left out", mk(30, -1, 20, -1), "m0 m2", 1},
|
||||||
|
{"none known", mk(-1, -1), "", 0},
|
||||||
|
} {
|
||||||
|
got, _, fresh := Freshest(c.ranked)
|
||||||
|
if strings.Join(got, " ") != c.servers || fresh != c.fresh {
|
||||||
|
t.Errorf("%s: kept %v, %d freshest; want %q, %d", c.name, got, fresh, c.servers, c.fresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// reflector prints an unknown last sync as the moment it ran: at or after the start of the run is no real sync.
|
||||||
|
start := time.Date(2026, 11, 1, 15, 0, 0, 500, time.UTC)
|
||||||
|
r := []rankedMirror{{"a", at(30)}, {"b", start.Truncate(time.Second)}, {"c", start.Add(time.Second)}}
|
||||||
|
withoutUnknownSync(r, start)
|
||||||
|
if r[0].LastSync.IsZero() || !r[1].LastSync.IsZero() || !r[2].LastSync.IsZero() {
|
||||||
|
t.Errorf("unknown syncs not detected: %+v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -334,14 +334,15 @@ type Round struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MirrorList is `mirrors.list`, the mesh's one mirror list (ADR 0585 §1): the servers in order, when and by which
|
// MirrorList is `mirrors.list`, the mesh's one mirror list (ADR 0585 §1): the servers in order, when and by which
|
||||||
// node they were ranked, the options reflector ranked them with, the newest last sync found among them (only mirrors as
|
// node they were ranked, the options reflector ranked them with, the newest last sync found among them (the mirrors at it come first,
|
||||||
// fresh as it are kept, §2), and the round it was ranked for (none for a refresh).
|
// then a tail of up to four fresher-than-the-rest, ADR 0586), and the round it was ranked for (none for a refresh).
|
||||||
type MirrorList struct {
|
type MirrorList struct {
|
||||||
Servers []string `json:"servers"`
|
Servers []string `json:"servers"`
|
||||||
RankedAt time.Time `json:"ranked_at"`
|
RankedAt time.Time `json:"ranked_at"`
|
||||||
RankedBy string `json:"ranked_by"`
|
RankedBy string `json:"ranked_by"`
|
||||||
Options []string `json:"options"`
|
Options []string `json:"options"`
|
||||||
Newest time.Time `json:"newest_sync"`
|
Newest time.Time `json:"newest_sync"`
|
||||||
|
Freshest int `json:"freshest"` // how many of the first servers are at the newest sync; the rest are the fallback tail (ADR 0586)
|
||||||
Round string `json:"round,omitempty"`
|
Round string `json:"round,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user