GR10: the grants part holds each entry left without a grant as a granting wait, read and released by its tools
Kept in its own state, clock-from the start of this version's first pass, the bound 10 minutes. The seat's wait-held and wait-ended events and acting-part-late are not built (see the report). Builds: design 47 G §276, §266, §269 (G-20 A-7); A-6 needs no change: the part reads the wants view and consumes no wanted Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -248,6 +248,16 @@ func toolsOf(p *grants.Part) []stdio.Tool {
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
return p.AskFirstViewAgain(stdio.CallerOf(a), grants.Answer(a, "state"), grants.Answer(a, "why"), recordHandAct)
|
||||
}},
|
||||
{Name: grants.ToolWaits, Description: "The grants part's granting waits (design 47 G §276): each entry held because its grant is not " +
|
||||
"composed yet by this version of the part, with its step, the acting part, its version, since when, its bound (10 minutes) and " +
|
||||
"clock-from (the start of this version's first pass), and which have passed their bound. Read-only.",
|
||||
Run: func(map[string]any) (any, error) { return p.WaitsOf() }},
|
||||
{Name: grants.ToolRelease, Description: "Release one held granting wait by a person's act with why, recorded as a hand act " +
|
||||
"(the seat mesh-hand-acts) before it is released (design 47 G §276). grants_waits lists them.",
|
||||
Input: schema(map[string]string{"module": "the held entry's module", "why": "why, kept in the hand act"}, "module", "why"),
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
return p.ReleaseWait(stdio.CallerOf(a), grants.Answer(a, "module"), grants.Answer(a, "why"), recordHandAct)
|
||||
}},
|
||||
{Name: grants.ToolRunning, Description: "What each node-engine said it runs (novox/hq design 47, ADR 0660 §2): the last event " +
|
||||
"running of each node and module, as the grants part keeps it in its state running (module, node, the build's " +
|
||||
"fingerprint, the digest of that build's manifest, when it was seen running healthy). A seat's holder is read from " +
|
||||
|
||||
@@ -13,6 +13,7 @@ require (
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhealers v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshgrants v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient v0.0.0-00010101000000-000000000000
|
||||
|
||||
@@ -2566,11 +2566,33 @@ func TestGrants(t *testing.T) {
|
||||
if p.Compose(); len(said) != 1 {
|
||||
t.Errorf("the same missing grant was said again: %+v", said)
|
||||
}
|
||||
// The entry left without a grant is a granting wait (design 47 G §276, A-7): kept in the part's own state, with its
|
||||
// bound of 10 minutes counted from the start of this version's first pass; a release is a hand act.
|
||||
waits, err := p.WaitsOf()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w := waits.(grants.WaitsSaid).Waits; len(w) != 1 || w[0].Module != "mod-y" || w[0].Bound != "10m0s" || w[0].Step != "grant" || w[0].ClockFrom.IsZero() {
|
||||
t.Fatalf("granting waits = %+v, want mod-y's, bound 10m0s, step grant", w)
|
||||
}
|
||||
if _, err := p.ReleaseWait("op", "mod-y", " ", nil); err == nil {
|
||||
t.Error("a release with no why was taken")
|
||||
}
|
||||
var acts []string
|
||||
if _, err := p.ReleaseWait("op", "mod-y", "why", func(what, why string) error { acts = append(acts, what); return nil }); err != nil || len(acts) != 1 {
|
||||
t.Errorf("release: %v, hand acts %v", err, acts)
|
||||
}
|
||||
if w := func() []grants.HeldWait { x, _ := p.WaitsOf(); return x.(grants.WaitsSaid).Waits }(); len(w) != 1 || !w[0].Released {
|
||||
t.Errorf("after the release: %+v", w)
|
||||
}
|
||||
b.put(grants.ReadModules, "mod-y", meshcatalog.ModulesByModuleValue{V: keys.Version, Fp2: fp, Manifest: meshcatalogclient.ManifestDigest([]byte(yManifest)),
|
||||
Repository: "novox/y", Commit: "c"})
|
||||
if res := p.Compose(); len(res.Grants.Missing) != 0 || len(said) != 2 || said[1].State != occ.StateCleared {
|
||||
t.Errorf("once mod-y has a grant: missing %v, said %+v", res.Grants.Missing, said)
|
||||
}
|
||||
if x, _ := p.WaitsOf(); len(x.(grants.WaitsSaid).Waits) != 0 {
|
||||
t.Errorf("the wait of mod-y outlived its grant: %+v", x)
|
||||
}
|
||||
})
|
||||
|
||||
// Issue 1966 (the registry delivery's second bootstrap loop): an assigner that serves no first views answers no
|
||||
|
||||
@@ -424,6 +424,7 @@ func (p *Part) writeGrants(res *Result, in Inputs, recs buscompose.Records, comp
|
||||
done := &GrantsWritten{Wanted: len(want), Missing: MissingGrants(in, want, drop, skip)}
|
||||
res.Grants = done
|
||||
p.sayMissing(done.Missing)
|
||||
p.settleWaits(res, in, done.Missing)
|
||||
byUser := map[string]User{}
|
||||
for _, u := range composed {
|
||||
byUser[u.User] = u
|
||||
|
||||
@@ -222,6 +222,8 @@ type Part struct {
|
||||
bindMu sync.Mutex
|
||||
// runningRead is the whole read of the stream running at the start (running.go), guarded by mu.
|
||||
runningRead *StreamRead
|
||||
// firstPass is when this version's first pass began (waits.go), guarded by mu.
|
||||
firstPass time.Time
|
||||
}
|
||||
|
||||
// keepBrokers keeps the modules whose manifest claims the seat mesh-broker: the only emitters whose created, recreated
|
||||
|
||||
@@ -30,6 +30,9 @@ const (
|
||||
ToolFirstViewAgain = "grants_first_view_again"
|
||||
// ToolRunning reads the state running (running.go).
|
||||
ToolRunning = "grants_running"
|
||||
// ToolWaits and ToolRelease read and release the granting waits (waits.go).
|
||||
ToolWaits = "grants_waits"
|
||||
ToolRelease = "grants_release"
|
||||
// ToolSeatList and ToolSeatListAgain read and read again the seat list the part composes from (mesh-sdk seatlist.Tools).
|
||||
ToolSeatList = "grants_seat_list"
|
||||
ToolSeatListAgain = "grants_seat_list_again"
|
||||
@@ -37,7 +40,7 @@ const (
|
||||
|
||||
// Tools is every tool, in the manifest's order.
|
||||
var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolAccounts, ToolConsumerMatch,
|
||||
ToolAcceptHashes, ToolFirstViews, ToolFirstViewAgain, ToolRunning, ToolSeatList, ToolSeatListAgain}
|
||||
ToolAcceptHashes, ToolFirstViews, ToolFirstViewAgain, ToolRunning, ToolWaits, ToolRelease, ToolSeatList, ToolSeatListAgain}
|
||||
|
||||
// WritesGrants is what grants says of the part's own grants: written once the list the bus loaded carries a build's
|
||||
// accounts, and the one key the assigner reads (ADR 0571 clause 9, which changes ADR 0547 clause 5).
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package grants
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshgrants"
|
||||
)
|
||||
|
||||
// **The granting wait** (design 47 G §276, §266, §269; G-20 A-7). The grants part holds the entries of a revision whose
|
||||
// accounts its new version has not yet composed: each module the pass leaves without a grant (MissingGrants) is one held
|
||||
// entry, kept in the part's own state on the bus under KeyWaits. The bound's clock, `clock-from`, is the start of this
|
||||
// version's first pass, which the part knows itself, so it hears no `build.ended`; the bound is GrantingBound, a constant of
|
||||
// the part. A held entry ends when its grant is there (`ran`) or when a person releases it (`released`).
|
||||
|
||||
// KeyWaits is the key of the part's own state user-list where it keeps the granting waits.
|
||||
const KeyWaits = "waits"
|
||||
|
||||
// GrantingBound is §266's bound of the granting step.
|
||||
const GrantingBound = 10 * time.Minute
|
||||
|
||||
// HeldWait is one granting wait as the part keeps it.
|
||||
type HeldWait struct {
|
||||
meshgrants.Wait
|
||||
// Released says a person released the entry (grants_release): it is held no more.
|
||||
Released bool `json:"released,omitempty"`
|
||||
}
|
||||
|
||||
// WaitsState is the value of KeyWaits, by the held module.
|
||||
type WaitsState struct {
|
||||
V int `json:"v"`
|
||||
Waits map[string]HeldWait `json:"waits"`
|
||||
}
|
||||
|
||||
func (p *Part) readWaits() (WaitsState, error) {
|
||||
st := WaitsState{V: 1, Waits: map[string]HeldWait{}}
|
||||
raw, _, found, err := p.Store.Get(StateUserList, KeyWaits)
|
||||
if err != nil {
|
||||
return st, fmt.Errorf("its own %s %s cannot be read: %w", StateUserList, KeyWaits, err)
|
||||
}
|
||||
if found {
|
||||
if err := json.Unmarshal(raw, &st); err != nil {
|
||||
return st, fmt.Errorf("its own %s %s does not read: %w", StateUserList, KeyWaits, err)
|
||||
}
|
||||
if st.Waits == nil {
|
||||
st.Waits = map[string]HeldWait{}
|
||||
}
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// settleWaits keeps the granting waits of this pass: a module left without a grant is a held entry, begun at this pass; one
|
||||
// that has its grant is ended as `ran`. A released entry stays released while it is still left without one.
|
||||
func (p *Part) settleWaits(res *Result, in Inputs, missing []string) {
|
||||
if p.Store == nil {
|
||||
return
|
||||
}
|
||||
now := p.now()
|
||||
p.mu.Lock()
|
||||
if p.firstPass.IsZero() {
|
||||
p.firstPass = now
|
||||
}
|
||||
clock := p.firstPass
|
||||
p.mu.Unlock()
|
||||
st, err := p.readWaits()
|
||||
if err != nil {
|
||||
res.Errors = append(res.Errors, err.Error())
|
||||
return
|
||||
}
|
||||
held := map[string]bool{}
|
||||
for _, m := range missing {
|
||||
module, _, _ := strings.Cut(m, ": ")
|
||||
held[module] = true
|
||||
}
|
||||
changed := false
|
||||
for module := range held {
|
||||
if _, ok := st.Waits[module]; ok {
|
||||
continue
|
||||
}
|
||||
st.Waits[module] = HeldWait{Wait: meshgrants.Wait{Bound: GrantingBound.String(), ClockFrom: clock, Fp: in.Fingerprints[module],
|
||||
Module: module, Part: "mesh-grants", Since: now, Step: meshgrants.WaitStepGrant, Version: in.Fingerprints["mesh-grants"]}}
|
||||
changed = true
|
||||
}
|
||||
for module := range st.Waits {
|
||||
if !held[module] {
|
||||
delete(st.Waits, module)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return
|
||||
}
|
||||
data, _ := json.Marshal(st)
|
||||
p.changed(res, StateUserList, KeyWaits, data)
|
||||
}
|
||||
|
||||
// WaitsSaid is what the tool grants_waits answers: each held granting wait, and which have passed their bound.
|
||||
type WaitsSaid struct {
|
||||
Waits []HeldWait `json:"waits"`
|
||||
// Late is the held entries whose bound, counted from clock-from, has passed.
|
||||
Late []string `json:"late,omitempty"`
|
||||
}
|
||||
|
||||
// WaitsOf reads the granting waits (grants_waits).
|
||||
func (p *Part) WaitsOf() (any, error) {
|
||||
st, err := p.readWaits()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := WaitsSaid{Waits: []HeldWait{}}
|
||||
for _, module := range sortedKeys(st.Waits) {
|
||||
w := st.Waits[module]
|
||||
out.Waits = append(out.Waits, w)
|
||||
if !w.Released && !w.ClockFrom.Add(GrantingBound).After(p.now()) {
|
||||
out.Late = append(out.Late, module)
|
||||
}
|
||||
}
|
||||
sort.Strings(out.Late)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ReleaseWait releases one held entry by a person's act with why, recorded as a hand act through record (the seat
|
||||
// mesh-hand-acts) before anything is changed (grants_release).
|
||||
func (p *Part) ReleaseWait(caller, module, why string, record func(what, why string) error) (any, error) {
|
||||
if strings.TrimSpace(why) == "" {
|
||||
return nil, errors.New("grants_release needs why: it is recorded as a hand act")
|
||||
}
|
||||
st, err := p.readWaits()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w, ok := st.Waits[module]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("the grants part holds no granting wait for %q: grants_waits lists them", module)
|
||||
}
|
||||
if record != nil {
|
||||
if err := record("grants_release "+module, why); err != nil {
|
||||
return nil, fmt.Errorf("the hand act could not be recorded, so nothing was released: %w", err)
|
||||
}
|
||||
}
|
||||
w.Released, w.By, w.How = true, caller+": "+why, meshgrants.WaitHowReleased
|
||||
st.Waits[module] = w
|
||||
data, _ := json.Marshal(st)
|
||||
if _, err := p.Store.Put(StateUserList, KeyWaits, data); err != nil {
|
||||
return nil, fmt.Errorf("the release could not be kept: %w", err)
|
||||
}
|
||||
return map[string]any{"released": module, "by": w.By}, nil
|
||||
}
|
||||
@@ -63,6 +63,8 @@
|
||||
"grants_first_views",
|
||||
"grants_first_view_again",
|
||||
"grants_running",
|
||||
"grants_waits",
|
||||
"grants_release",
|
||||
"grants_seat_list",
|
||||
"grants_seat_list_again"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user