Merge pull request 'mesh-grants says loudly when it holds the user list back, and clears it after a restart (issue 1945)' (#69) from fix/grants-hold-loud into main

This commit was merged in pull request #69.
This commit is contained in:
2026-10-26 12:44:36 +00:00
3 changed files with 63 additions and 1 deletions
+23
View File
@@ -961,6 +961,17 @@ func TestGrants(t *testing.T) {
if res := p.Compose(); res.Sealed == nil || !strings.Contains(res.Sealed.Why, "a read of this pass failed") {
t.Errorf("a pass past a failed read = %+v", res.Sealed)
}
// While the hold lasts the warning stands, said once however many passes find it; it names why.
p.Compose()
nHeld := 0
for _, c := range said {
if c.Key == grants.HeldCondition && c.State == asks.StateRaised && c.Severity == asks.SeverityWarning && strings.Contains(c.Summary, "a read of this pass failed") {
nHeld++
}
}
if nHeld != 1 {
t.Errorf("the held list raised the warning %d times, want once: %+v", nHeld, said)
}
b.refused[grants.ReadAccounts] = false
if now, _, _ := p.Sealed(); now.Digest != g.Digest || now.Object != g.Object {
t.Errorf("the list in force moved past a failed read: %s, was %s", now.Digest, g.Digest)
@@ -990,6 +1001,18 @@ func TestGrants(t *testing.T) {
if p.Compose(); func() bool { now, _, _ := p.Sealed(); return now.Digest != g.Digest }() {
t.Errorf("the list in force moved with every read and hash back")
}
if last := said[len(said)-1]; last.Key != grants.HeldCondition || last.State != asks.StateCleared {
t.Errorf("the held warning is not cleared once nothing is held: %+v", last)
}
// A part started while the warning stood (the said state is in memory only) clears it on its first pass.
var fresh []asks.Condition
fp := partOn(b)
fp.Objects = p.Objects
fp.Asker, fp.Say = fa, func(c asks.Condition) error { fresh = append(fresh, c); return nil }
fp.Compose()
if len(fresh) == 0 || fresh[0].Key != grants.HeldCondition || fresh[0].State != asks.StateCleared {
t.Errorf("a fresh part's first pass does not say the held warning is cleared: %+v", fresh)
}
}
})
}
@@ -143,6 +143,12 @@ type Part struct {
made creations
// askSeen is the revision of the last ask to compose (KeyAsked) this part acted on.
askSeen uint64
// heldSaid is the reason of the held-list condition last said (empty: none raised); heldKnown is whether one was said
// since the start, so the first pass after a start always says its state (a condition raised before a restart is cleared
// by it). holdMu guards both.
holdMu sync.Mutex
heldSaid string
heldKnown bool
}
// Ask is the value of KeyAsked: a tool's ask to compose now, and, from grants_assert, to create one module's missing
+34 -1
View File
@@ -7,6 +7,7 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -80,7 +81,7 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut []string
key = in.Loaded.Key
}
digest := delivery.UserListDigest(list)
why := ""
why, held := "", ""
switch {
case p.Objects == nil:
// The tools: the process writes; the follow alone may change here, from a tool's view of the switch, and is not.
@@ -94,6 +95,7 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut []string
// After take-over the list in force is never replaced by one composed past a failed read, or one that drops a user
// of it for want of a hash: that user's module would be locked out of the bus (review of the switch).
why = p.holdsBack(in, leftOut, bus)
held = why
case cur.Digest != digest || cur.SealedTo != delivery.SealingKeyID(key) || cur.Node != in.BusNode:
object, err := delivery.SealUserList(list, key)
if err == nil {
@@ -109,6 +111,7 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut []string
}
next.Digest, next.Object, next.SealedTo, next.Node = digest, delivery.UserListObjectName(digest), delivery.SealingKeyID(key), in.BusNode
}
p.sayHeld(held)
if next.Digest == digest {
next.Users, next.LeftOut = users, leftOut
}
@@ -143,6 +146,36 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut []string
"left_out": len(next.LeftOut), "follow": next.Follow, "sealed_to": next.Node})
}
// sayHeld raises the warning condition mesh-grants.user-list-held while the list in force is kept (why, which words the pass's
// `held: ...`), and clears it on the first pass that holds nothing back: the list advanced, or the reason went. The refusal
// condition's way (sayRefused): said once per reason, again when the reason changes, and tried again when a say failed.
func (p *Part) sayHeld(why string) {
if p.Say == nil {
return
}
p.holdMu.Lock()
defer p.holdMu.Unlock()
if p.heldKnown && why == p.heldSaid {
return
}
c := asks.Condition{Key: HeldCondition, Subject: asks.Scope{Scope: "module", ID: "mesh-grants"}}
if why == "" {
c.State = asks.StateCleared
} else {
reason := strings.TrimPrefix(why, "held: ")
c.Severity, c.State = asks.SeverityWarning, asks.StateRaised
c.Summary, c.Headline, c.Needs = "the user list is held: "+reason, "The user list in force is held back", "a look at why"
c.Explanation = asks.NeedsYou + "a look at why. The bus node keeps following the list written before, not the one composed now: " + reason
c.Resolved = "a pass finds nothing to hold back and the list advances"
}
if p.Say(c) == nil {
p.heldSaid, p.heldKnown = why, true
}
}
// HeldCondition is the key of the warning raised while a list in force is held back after take-over.
const HeldCondition = "mesh-grants.user-list-held"
func sameSealed(a, b delivery.GrantsUserList) bool {
a.At, b.At = b.At, a.At
x, _ := json.Marshal(a)