Compare commits

...
Author SHA1 Message Date
jschoubben 289d1e8fa6 Format mesh-delivery-view's read.go as gofmt writes it (issue 1126, the group check's failure)
mesh/delivery ready: it is merged on the Approve
mesh/check pass 289d1e8fa69a on bab2e1391ae5 | unit 2860 run, 1141 kept, 36 skipped | integration 2887 run, 1241 kept, 29 skipped | over bound: unit (…
mesh/delivery-group group fix/1126-shadow-bodies ready: every member ready, and composed together they pass
2026-10-28 01:49:54 +01:00
jschoubben 968b9bf2e6 mesh-delivery-view: a node's candidate answer is shown as the node wrote it; pin mesh-sdk 362a3f1 (issue 1126, probe #532)
mesh/check failed (2 tests): unit: modules/mesh-delivery-view.gofmt: not as gofmt writes them (run gofmt -w): modules/mesh-delivery-view/internal/view…
mesh/delivery-group group fix/1126-shadow-bodies rejected: composed together: the checker's verdict at the members' heads (46e1e846f8db): unit: modules/mesh-de…
mesh/delivery closed: a newer head of the same pull request
The bodies of each difference never reached the answer shown: the view decoded
the node's answer with its mesh-sdk, which did not know the field, and encoded
it again. The answer now rides through as stored; a decoded copy only says its
state.
2026-10-28 01:39:34 +01:00
mesh-tools bab2e1391a Merge pull request 'mesh-grants: stop rewriting the shadow user list every 3 s when nothing changed (issue 1126, group fix/1126-switch-safe)' (#91) from fix/1126-switch-safe into main 2026-10-27 23:52:23 +00:00
jschoubben 663c9cbc34 mesh-grants: pin mesh-sdk 7359d18, the group's head (issue 1126)
mesh/delivery merged: source.main moved to novox/mesh-delivery@bab2e1391ae5
mesh/delivery-group group fix/1126-switch-safe merged: every member is merged
mesh/check pass 663c9cbc348d on a49fd9cc65f7 | unit 2992 run, 1008 kept, 36 skipped | integration 3025 run, 1102 kept, 29 skipped | over bound: unit (…
2026-10-28 00:34:13 +01:00
jschoubben 3d916a685e mesh-grants: the same list said again moves neither the sealed key nor the shadow list (issue 1126)
Live since the take-over the shadow list was written every 3 s (revision 99270
to 99357 in 90 s, nothing differing): the sealed key read back says [] for the
nil left-out a pass composes, so every pass wrote the key again; each write
moved the bus node's report of what it loaded, whose revision and time were
inputs of the shadow list, and woke the next pass. The bus itself reloads only
when the list's bytes change (its log: two configurations alternating with the
placement, f8567865 and 473f9ad3), so this loop was load, not the reload.
2026-10-28 00:20:44 +01:00
9 changed files with 84 additions and 22 deletions
@@ -154,7 +154,10 @@ func TestModulesToolAnswersEveryKeyAsStored(t *testing.T) {
return raw, revs[key], nil
}}
p.HavesState = fake{
"ace.node": `{"v":1,"composed":"x","candidates":{"feat/p":{"head":"` + h2 + `","verdict":"same","same":["plex"]}}}`,
// ace's answer as the node wrote it, with what this view's mesh-sdk may not know (issue 1126, probe #532: the
// bodies of each difference were dropped on the way): shown as stored.
"ace.node": `{"v":1,"composed":"x","candidates":{"feat/p":{"head":"` + h2 + `","verdict":"same","same":["plex"],` +
`"differs":[],"note":"took over a minute","future":{"bodies":[{"id":"x","composed":"<masked>"}]}}}}`,
"g14.node": `{"v":1,"composed":"x","candidates":{"feat/p":{"head":"` + h1 + `","revision":2,"verdict":"same"}}}`,
}.reader().State
out, err = run(t, p, view.PlacementReadTool, map[string]any{})
@@ -164,6 +167,9 @@ func TestModulesToolAnswersEveryKeyAsStored(t *testing.T) {
answered := map[string]string{}
for _, c := range out.(map[string]any)["candidates"].([]view.CandidateRow) {
answered[c.Node] = c.Answered
if c.Node == "ace" && (!strings.Contains(string(c.Answer), `"future":{"bodies"`) || !strings.Contains(string(c.Answer), `"note":"took over a minute"`)) {
t.Errorf("ace's answer is not shown as stored: %s", c.Answer)
}
}
if want := map[string]string{"ace": "same", "g14": view.AnswerStale, "novox": view.AnswerNone}; fmt.Sprint(answered) != fmt.Sprint(want) {
t.Errorf("answers %v, want %v", answered, want)
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-delivery-view
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027193820-8ad9372b1952
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028003907-362a3f1378aa
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
+2
View File
@@ -1,5 +1,7 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027193820-8ad9372b1952 h1:TmveT3Bija643RGWkilvkzP/h8UWlVvHFZZtQfJK5oU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027193820-8ad9372b1952/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028003907-362a3f1378aa h1:DFYX7pIluzeeEyM+YXGeB6GvEnOYyRhz/1Ke/LFa8+8=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028003907-362a3f1378aa/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -726,14 +726,16 @@ const (
// CandidateRow is one placement candidate and the node's answer to it.
type CandidateRow struct {
Group string `json:"group"`
Node string `json:"node"`
Pull delivery.Head `json:"pull"`
Revision uint64 `json:"revision"`
Placement json.RawMessage `json:"placement,omitempty"` // the candidate value as stored
Removes bool `json:"removes,omitempty"` // the head removes the node's file
Answered string `json:"answered"` // same, differs, refused, stale or none
Answer *delivery.CandidateAnswer `json:"answer,omitempty"`
Group string `json:"group"`
Node string `json:"node"`
Pull delivery.Head `json:"pull"`
Revision uint64 `json:"revision"`
Placement json.RawMessage `json:"placement,omitempty"` // the candidate value as stored
Removes bool `json:"removes,omitempty"` // the head removes the node's file
Answered string `json:"answered"` // same, differs, refused, stale or none
// Answer is the node's answer as its node wrote it, whole: what this view's mesh-sdk does not know rides through (issue
// 1126, probe #532: each difference's bodies were dropped when the answer was decoded and encoded again here).
Answer json.RawMessage `json:"answer,omitempty"`
}
// PlacementCandidates is every placement candidate, narrowed by node and by a module its placement names, with each
@@ -773,8 +775,8 @@ func (r Reader) PlacementCandidates(node, module string) ([]CandidateRow, error)
placed map[string]bool
}
groups := map[string]*group{}
answers := map[string]map[string]delivery.CandidateAnswer{} // node, then group
answerOf := func(n, g string) (delivery.CandidateAnswer, bool) {
answers := map[string]map[string]storedAnswer{} // node, then group
answerOf := func(n, g string) (storedAnswer, bool) {
if _, read := answers[n]; !read {
answers[n] = r.candidateAnswers(n)
}
@@ -818,8 +820,8 @@ func (r Reader) PlacementCandidates(node, module string) ([]CandidateRow, error)
continue
}
if a, ok := answerOf(n, gname); ok && err == nil {
row.Answer = &a
row.Answered = AnswerOf(c.Pull.Head, rev, true, a)
row.Answer = a.raw
row.Answered = AnswerOf(c.Pull.Head, rev, true, a.a)
}
rows = append(rows, row)
}
@@ -841,8 +843,8 @@ func (r Reader) PlacementCandidates(node, module string) ([]CandidateRow, error)
}
row := CandidateRow{Group: gname, Node: n, Pull: g.pull, Answered: AnswerNone}
if a, ok := answerOf(n, gname); ok {
row.Answer = &a
row.Answered = AnswerOf(g.pull.Head, 0, false, a)
row.Answer = a.raw
row.Answered = AnswerOf(g.pull.Head, 0, false, a.a)
}
rows = append(rows, row)
}
@@ -859,8 +861,15 @@ func AnswerOf(head string, rev uint64, keyed bool, a delivery.CandidateAnswer) s
return a.Verdict
}
// candidateAnswers is a node's answers to its candidates, by group, from its `<node>.node` have; none when it has none.
func (r Reader) candidateAnswers(node string) map[string]delivery.CandidateAnswer {
// storedAnswer is a node's answer to one group: as stored, and read for its head, revision and verdict.
type storedAnswer struct {
raw json.RawMessage
a delivery.CandidateAnswer
}
// candidateAnswers is a node's answers to its candidates, by group, from its `<node>.node` have, each as stored; none
// when it has none.
func (r Reader) candidateAnswers(node string) map[string]storedAnswer {
if r.HavesState.Get == nil {
return nil
}
@@ -872,9 +881,21 @@ func (r Reader) candidateAnswers(node string) map[string]delivery.CandidateAnswe
if err != nil || len(raw) == 0 {
return nil
}
h, err := delivery.DecodeHaveNode(raw)
if err != nil {
if _, err := delivery.DecodeHaveNode(raw); err != nil {
return nil
}
return h.Candidates
var stored struct {
Candidates map[string]json.RawMessage `json:"candidates"`
}
if json.Unmarshal(raw, &stored) != nil {
return nil
}
out := make(map[string]storedAnswer, len(stored.Candidates))
for g, ans := range stored.Candidates {
var a delivery.CandidateAnswer
if json.Unmarshal(ans, &a) == nil {
out[g] = storedAnswer{raw: ans, a: a}
}
}
return out
}
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-grants
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027201213-7a8e9f970648
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027232145-7359d187db8d
github.com/nats-io/nats.go v1.54.0
)
+2
View File
@@ -10,6 +10,8 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027200317-538ffbaa085b h1:LuBK5dtB
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027200317-538ffbaa085b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027201213-7a8e9f970648 h1:XoMsgg9kSgjQWa86NZM8h8OpxJ2kovxcMS2C0i6WSVU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027201213-7a8e9f970648/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027232145-7359d187db8d h1:DhR7DRKaF0OtA5l/ikIk7gZGeIM1lRz65PUE1DGEkvU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027232145-7359d187db8d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
+15
View File
@@ -1302,6 +1302,21 @@ func TestGrants(t *testing.T) {
if e := effect(); !strings.HasPrefix(e, "switched") {
t.Errorf("effect once the bus node loaded the written list = %s", e)
}
// Steady (issue 1126, 2026-10-28 00:16: the shadow list moved every 3 s, 99270 to 99357, with nothing
// differing): the bus node saying again that it loaded the same list, at a later time and the key's
// later revision, moves neither the shadow list nor the sealed key.
_, sealedRev, _ := p.Sealed()
p.Compose()
for i := 0; i < 3; i++ {
withKey(delivery.UserListWritten{Source: delivery.GrantsUserListSource, Revision: sealedRev + uint64(i+1), Wanted: g.Digest,
Digest: g.Digest, Users: g.Users, Follows: delivery.FollowGrants, At: time.Date(2026, 10, 28, 0, 16, i, 0, time.UTC)})
if res := p.Compose(); res.Written {
t.Errorf("pass %d: the shadow list is written again for the bus node saying the same list again (revision %d)", i, res.Revision)
}
if _, rev, _ := p.Sealed(); rev != sealedRev {
t.Errorf("pass %d: the sealed key is written again with nothing changed: revision %d, was %d", i, rev, sealedRev)
}
}
withKey(loadedBefore)
// After take-over a list composed past a failed read, or one dropping a user of the list in force for
// want of a hash, is not sealed: the list in force stays, and the pass says why.
@@ -77,6 +77,13 @@ type UserListState struct {
func (s UserListState) inputs() string {
s.At, s.Inputs = time.Time{}, ""
// When the bus node said it loaded a list is no input: the same list said again, at a later time and the key's later
// revision, composes nothing new (issue 1126: written again every 3 s, 2026-10-28).
if s.Loaded != nil {
l := *s.Loaded
l.Revision, l.At = 0, time.Time{}
s.Loaded = &l
}
raw, _ := json.Marshal(s)
sum := sha256.Sum256(raw)
return "sha256:" + hex.EncodeToString(sum[:])
@@ -177,7 +177,16 @@ func (p *Part) sayHeld(why string) {
const HeldCondition = "mesh-grants.user-list-held"
func sameSealed(a, b delivery.GrantsUserList) bool {
// Its time is no change, and no user left out is the same written as [] or null: the key read back says [] for the
// nil a pass composes, which read as a change wrote the key again at every pass, and each write moved the bus node's
// report and woke the next pass (issue 1126: the shadow list's revision 99270 to 99357 in 90 s, 2026-10-28).
a.At, b.At = b.At, a.At
if len(a.LeftOut) == 0 {
a.LeftOut = nil
}
if len(b.LeftOut) == 0 {
b.LeftOut = nil
}
x, _ := json.Marshal(a)
y, _ := json.Marshal(b)
return string(x) == string(y)