pacman falls back to the controller's mirror for every node when the engine's state cannot be read, and says the engine's error (ADR 0565 clause 6, review round 2)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-01 09:54:07 +01:00
parent 1db0181920
commit 4a6395feae
2 changed files with 69 additions and 9 deletions
+34 -9
View File
@@ -118,12 +118,19 @@ func (m *Machine) installedOn(node string) ([]nodeInstalled, error) {
}
// declaredOn is what each node's modules declare present, by node and package, with the declaring modules, and the source
// that answered for each node: its engine's key, else the controller's mirror; a node neither holds is missing.
func (m *Machine) declaredOn(nodes []string) (map[string]map[string][]string, map[string]string, error) {
// that answered for each node: its engine's key, else the controller's mirror; a node neither holds is missing. When the
// engine's state cannot be read at all (its bucket is not on the bus yet, or the read is refused) the mirror answers for
// every node and the engine's error is returned as a word to say beside the sources; it is an error only when the mirror
// cannot be read either (ADR 0565 clause 6).
func (m *Machine) declaredOn(nodes []string) (map[string]map[string][]string, map[string]string, string, error) {
out := map[string]map[string][]string{}
from := map[string]string{}
engineErr := ""
for _, source := range []string{SourceEngine, SourceController} {
r := m.States(source)
read := map[string]map[string][]string{}
readFrom := map[string]string{}
var failed error
for _, n := range nodes {
if from[n] != "" {
continue
@@ -131,21 +138,32 @@ func (m *Machine) declaredOn(nodes []string) (map[string]map[string][]string, ma
var v declaredValue
found, err := readValue(r, n, &v)
if err != nil {
return nil, nil, err
failed = err
break
}
if !found {
continue
}
from[n] = source
out[n] = map[string][]string{}
readFrom[n] = source
read[n] = map[string][]string{}
for _, p := range v.Packages {
if !p.Absent {
out[n][p.Package] = p.Modules
read[n][p.Package] = p.Modules
}
}
}
if failed != nil {
if source == SourceEngine {
engineErr = SourceEngine + " cannot be read: " + failed.Error()
continue
}
return nil, nil, "", failed
}
for n, d := range read {
out[n], from[n] = d, readFrom[n]
}
}
return out, from, nil
return out, from, engineErr, nil
}
// NodeView is one node in the view.
@@ -198,7 +216,7 @@ func (m *Machine) Packages(name string, limit int) (map[string]any, error) {
for _, ni := range all {
nodes = append(nodes, ni.Node)
}
declared, sources, err := m.declaredOn(nodes)
declared, sources, engineErr, err := m.declaredOn(nodes)
if err != nil {
return nil, err
}
@@ -220,7 +238,11 @@ func (m *Machine) Packages(name string, limit int) (map[string]any, error) {
}
on = append(on, p)
}
return map[string]any{"package": name, "nodes": on}, nil
answer := map[string]any{"package": name, "nodes": on}
if engineErr != "" {
answer["declared_engine_error"] = engineErr
}
return answer, nil
}
views := []NodeView{}
versions := map[string]map[string]string{}
@@ -269,6 +291,9 @@ func (m *Machine) Packages(name string, limit int) (map[string]any, error) {
}
}
out := map[string]any{"nodes": views}
if engineErr != "" {
out["declared_engine_error"] = engineErr
}
for key, list := range map[string][]Spread{"differing": differing, "some_nodes": some} {
b := bound(key, list, limit)
out[key] = b[key]
@@ -6,10 +6,13 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// viewStates is two nodes' packages on the bus: node-a current and declared, node-b over the cap since a while,
@@ -66,6 +69,31 @@ func TestOnePackageOnEveryNodeWithWhoDeclaresIt(t *testing.T) {
on2[1].DeclaredFrom != SourceController || strings.Join(on2[1].DeclaredBy, ",") != "legacy" {
t.Errorf("fallback: %+v", on2)
}
// The engine's state cannot be read (its bucket is not on the bus yet): the mirror answers for every node, and the
// answer says why beside declared_from; a mirror that cannot be read either is an error.
read := m.States
m.States = func(name string) StateReader {
if name == SourceEngine {
return failingReader{errors.New("is issued and its bucket is not on the bus yet")}
}
return read(name)
}
r3, err3 := m.Packages("zsh", 500)
on3, _ := r3["nodes"].([]PackageOnNode)
if err3 != nil || len(on3) != 2 || on3[0].DeclaredFrom != SourceController || strings.Join(on3[0].DeclaredBy, ",") != "stale" ||
on3[1].DeclaredFrom != SourceController || !strings.Contains(fmt.Sprint(r3["declared_engine_error"]), "not on the bus yet") {
t.Errorf("engine unreadable: %+v %v", r3, err3)
}
m.States = func(name string) StateReader {
if name == SourceEngine || name == SourceController {
return failingReader{errors.New("refused")}
}
return read(name)
}
if _, err := m.Packages("zsh", 500); err == nil {
t.Error("neither state could be read and no error was said")
}
m.States = read
if a.Node != "node-a" || !a.Installed || a.Version != "5.9-2" || !a.Explicit || a.Repository != "extra" ||
strings.Join(a.DeclaredBy, ",") != "login,shell" || !a.Current {
t.Errorf("node-a: %+v", a)
@@ -174,3 +202,10 @@ func TestANodeArgumentIsOneKeyToken(t *testing.T) {
t.Errorf("%q %v", n, err)
}
}
// failingReader is a state that cannot be read.
type failingReader struct{ err error }
func (f failingReader) Get(string) (*stdio.StateEntry, error) { return nil, f.err }
func (f failingReader) Keys() ([]string, error) { return nil, f.err }
func (f failingReader) History(string) ([]stdio.StateRevision, error) { return nil, f.err }