Merge pull request 'Without the controller: mesh-grants makes consumers, enrolment issues, the watchman keeps dead letters, and the controller cannot be placed again (issue 1126)' (#137) from fix/1126-gates-delivery into main
This commit was merged in pull request #137.
This commit is contained in:
@@ -559,6 +559,7 @@ func (p *passer) addNode(ctx context.Context, prop delivery.PlacementProposal, r
|
||||
return
|
||||
}
|
||||
laid, findings := delivery.LayChanges(current, prop.Changes)
|
||||
findings = append(findings, assign.ControllerRefusals(prop.Changes, laid)...)
|
||||
node := prop.Changes[0].Node
|
||||
if prop.Changes[0].Op == delivery.OpRemoveNode {
|
||||
p.removeNode(ctx, prop, current, findings)
|
||||
|
||||
@@ -2,6 +2,8 @@ package assign
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -53,6 +55,7 @@ func Propose(in ProposeIn) (delivery.PlacementProposal, map[string]*delivery.Pla
|
||||
}
|
||||
laid, lay := delivery.LayChanges(in.Current, in.Changes)
|
||||
findings = append(findings, lay...)
|
||||
findings = append(findings, ControllerRefusals(in.Changes, laid)...)
|
||||
all := map[string]delivery.Placement{}
|
||||
for node, pl := range in.Current {
|
||||
all[node] = pl
|
||||
@@ -105,6 +108,37 @@ func Propose(in ProposeIn) (delivery.PlacementProposal, map[string]*delivery.Pla
|
||||
return p, out, findings
|
||||
}
|
||||
|
||||
// ControllerModule is the controller's module, which nothing places again (issue 1126).
|
||||
const ControllerModule = "mesh-controller"
|
||||
|
||||
// ControllerEnded is why a placement of the controller is refused: the parts that acted only once its lease was handed
|
||||
// (consumers, the user list, enrolment, dead letters) now act without reading the lease, so a controller placed again
|
||||
// would write beside them, silently.
|
||||
const ControllerEnded = "the controller ended by its unassignment on 2026-11-02; placing it again would make two writers (issue 1126)"
|
||||
|
||||
// ControllerRefusals names every change, and every node as laid, that places the controller: by a place, by a seed's
|
||||
// whole placement, or in what the changes lay. None when nothing does.
|
||||
func ControllerRefusals(changes []delivery.PlacementProposalChange, laid map[string]delivery.Placement) []string {
|
||||
var out []string
|
||||
nodes := map[string]bool{}
|
||||
for i, c := range changes {
|
||||
places := c.Module == ControllerModule && c.Op != delivery.OpTakeOff
|
||||
if c.Whole != nil && slices.Contains(c.Whole.Assigned(), ControllerModule) {
|
||||
places = true
|
||||
}
|
||||
if places {
|
||||
out = append(out, fmt.Sprintf("change %d places %s on %s: %s", i+1, ControllerModule, c.Node, ControllerEnded))
|
||||
nodes[c.Node] = true
|
||||
}
|
||||
}
|
||||
for _, node := range slices.Sorted(maps.Keys(laid)) {
|
||||
if !nodes[node] && slices.Contains(laid[node].Assigned(), ControllerModule) {
|
||||
out = append(out, fmt.Sprintf("%s would run %s: %s", node, ControllerModule, ControllerEnded))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Write is one node a warrant writes (§5): the node, its node key `<node>.node` (the commit of the node's keys,
|
||||
// mesh-sdk delivery.PlacementKeys.Writes), the revision of the node's placement it is written at (0: it has none, so
|
||||
// it is created) and its value (nil: it is deleted).
|
||||
|
||||
@@ -37,12 +37,17 @@ func TestThePlacementSeatDecides(t *testing.T) {
|
||||
in ProposeIn
|
||||
want string
|
||||
}{
|
||||
"a module not held": {in(delivery.PlacementProposalChange{Op: delivery.OpPlace, Node: "laptop", Module: "ghost"}), "the indexer holds no module ghost"},
|
||||
"a node with no record": {in(delivery.PlacementProposalChange{Op: delivery.OpPlace, Node: "stranger", Module: "web"}), "stranger is a node with no record"},
|
||||
"nothing changes": {in(delivery.PlacementProposalChange{Op: delivery.OpPlace, Node: "anchor", Module: "web"}), "leave every placement as it is"},
|
||||
"no why": {func() ProposeIn { i := in(move...); i.Why = " "; return i }(), "says why"},
|
||||
"no caller": {func() ProposeIn { i := in(move...); i.By = ""; return i }(), "names no caller"},
|
||||
"a seed by hand": {in(delivery.PlacementProposalChange{Op: delivery.OpSeed, Node: "laptop", Whole: &laptop}), "gone with adopt"},
|
||||
"a module not held": {in(delivery.PlacementProposalChange{Op: delivery.OpPlace, Node: "laptop", Module: "ghost"}), "the indexer holds no module ghost"},
|
||||
"a node with no record": {in(delivery.PlacementProposalChange{Op: delivery.OpPlace, Node: "stranger", Module: "web"}), "stranger is a node with no record"},
|
||||
"nothing changes": {in(delivery.PlacementProposalChange{Op: delivery.OpPlace, Node: "anchor", Module: "web"}), "leave every placement as it is"},
|
||||
"no why": {func() ProposeIn { i := in(move...); i.Why = " "; return i }(), "says why"},
|
||||
"no caller": {func() ProposeIn { i := in(move...); i.By = ""; return i }(), "names no caller"},
|
||||
"a seed by hand": {in(delivery.PlacementProposalChange{Op: delivery.OpSeed, Node: "laptop", Whole: &laptop}), "gone with adopt"},
|
||||
// 1126: the controller ended by its unassignment; placed again it would be a second writer beside the parts that
|
||||
// act without its lease.
|
||||
"the controller placed again": {in(delivery.PlacementProposalChange{Op: delivery.OpPlace, Node: "laptop", Module: "mesh-controller"}), "ended by its unassignment"},
|
||||
"the controller seeded whole": {in(delivery.PlacementProposalChange{Op: delivery.OpSeed, Node: "anchor", Whole: &delivery.Placement{V: delivery.Version, Node: "anchor",
|
||||
Decided: anchor.Decided, Modules: []delivery.PlacedModule{{Module: "mesh-controller"}}}}), "two writers (issue 1126)"},
|
||||
"a pin of nobody placed": {in(delivery.PlacementProposalChange{Op: delivery.OpPin, Node: "laptop", Provision: "db", Provider: &delivery.Provider{Node: "laptop", Module: "store"}}), "not placed there"},
|
||||
} {
|
||||
if _, _, f := Propose(c.in); !slices.ContainsFunc(f, func(s string) bool { return strings.Contains(s, c.want) }) {
|
||||
|
||||
@@ -140,9 +140,9 @@ func Tools(states func(string) state, ask func(socket.Ask) (json.RawMessage, err
|
||||
{Name: verb(VerbDeadLetters), Run: dead,
|
||||
Description: "What a consumer gave up on, kept in DEAD_LETTERS (novox/hq ADR 0564 §5, ADR 0264, ADR 0376): newest first, " +
|
||||
"for one consumer (its name or <stream>.<consumer>), at most limit; one whole by id (a restricted one's body " +
|
||||
"withheld). Who keeps them now: the controller until the lease names no controller, the watchman from then. " +
|
||||
"Delivering again and dropping (act deliver or drop, with why) are refused here until the hand-act log's home " +
|
||||
"(issue 1953): before the lease write use mesh-controller.dead-letters.",
|
||||
"withheld). The watchman keeps them once the controller's consumer on DEAD_LETTER_NOTICES is gone, and says the " +
|
||||
"notices wait while it stands. Delivering again and dropping (act deliver or drop, with why) are refused here until " +
|
||||
"the hand-act log's home (issue 1953).",
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{
|
||||
"consumer": map[string]any{"type": "string", "description": "only this consumer's, its name or <stream>.<consumer>"},
|
||||
"limit": map[string]any{"type": "integer", "description": "at most this many, newest first"},
|
||||
|
||||
@@ -117,12 +117,8 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8
|
||||
kvs[c.Bucket] = kv
|
||||
}
|
||||
|
||||
// The controller's lease, its consumer on DEAD_LETTER_NOTICES, the dead letters' streams and a source stream with one
|
||||
// message a consumer will give up on (1952/WM5).
|
||||
lease, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: buscompose.LeaseBucket})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The controller's consumer on DEAD_LETTER_NOTICES, the dead letters' streams and a source stream with one message a
|
||||
// consumer will give up on (1952/WM5); no lease bucket at all (issue 1126).
|
||||
for _, c := range []jetstream.StreamConfig{
|
||||
{Name: buscompose.DeadLetterNoticesStream, Subjects: []string{buscompose.MaxDeliveriesAdvisories}, Retention: jetstream.WorkQueuePolicy},
|
||||
{Name: buscompose.DeadLettersStream, Subjects: []string{"mesh.events.dead.>"}, Duplicates: 24 * time.Hour, AllowMsgTTL: true},
|
||||
@@ -269,15 +265,8 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8
|
||||
})
|
||||
until("node n1's silence", func() bool { return finding("machine.n1.silent") })
|
||||
|
||||
// 1952/WM5: while the lease names a controller the watchman takes no notice; handed away while the controller's consumer
|
||||
// stands, it says the notices wait; the consumer removed (the lease write's step), it makes its own and keeps a notice
|
||||
// with its headers.
|
||||
if _, err := js.Consumer(ctx, buscompose.DeadLetterNoticesStream, deadletters.Consumer); err == nil {
|
||||
t.Fatal("the watchman made its consumer while the lease names a controller")
|
||||
}
|
||||
if _, err := lease.Put(ctx, buscompose.ControllerLeaseKey, []byte(`{"handed-to":"mesh-vault","handed-by":"operator","why":"the lease write"}`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 1952/WM5, 1126/A2/AC2: with no lease written, while the controller's consumer stands the watchman says the notices
|
||||
// wait; the consumer removed, it makes its own and keeps a notice with its headers.
|
||||
waits := watch.FindingKey(watch.ScopeBus, "DEAD_LETTER_NOTICES.controller", watch.KindNoticesWaiting)
|
||||
until("the notices said waiting behind the controller's consumer", func() bool { return finding(waits) })
|
||||
if _, err := js.Consumer(ctx, buscompose.DeadLetterNoticesStream, deadletters.Consumer); err == nil {
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||
|
||||
@@ -171,7 +170,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// Its own presence, pulses and stuck rows are what it keeps: each stops the start, naming its grant. The assigner's wants
|
||||
// and the node-engine's haves only make the pulses of a move: without one the watchman watches presence and the build
|
||||
// seats' steps, and says that moves are not watched (issue 1945).
|
||||
var kvs struct{ presence, pulse, stuck, findings, wants, haves, lease jetstream.KeyValue }
|
||||
var kvs struct{ presence, pulse, stuck, findings, wants, haves jetstream.KeyValue }
|
||||
for _, b := range []struct {
|
||||
name string
|
||||
into *jetstream.KeyValue
|
||||
@@ -186,7 +185,6 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
kvs.wants, kvs.haves = nil, nil // a want is a pulse only against the have that ends it
|
||||
}
|
||||
|
||||
kvs.lease = process.OptionalBucket(ctx, js, buscompose.LeaseBucket, "the lease is read as naming a controller, so the dead letters stay the controller's", logger.Printf)
|
||||
w := watch.New(&kvOut{ctx: ctx, presence: kvs.presence, pulse: kvs.pulse, stuk: kvs.stuck, findings: kvs.findings}, time.Now)
|
||||
w.Logf(logger.Printf)
|
||||
// A test shortens how long a move may take; the mesh never sets it.
|
||||
@@ -379,23 +377,6 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
errs <- follow(ctx, provWatch, func(e jetstream.KeyValueEntry) { w.Provision(e.Key(), e.Value(), opOf(e)) })
|
||||
}()
|
||||
}
|
||||
if kvs.lease != nil {
|
||||
lw, err := kvs.lease.Watch(ctx, buscompose.ControllerLeaseKey)
|
||||
if err != nil {
|
||||
process.NotWatched(logger.Printf, buscompose.LeaseBucket, err, "the lease is read as naming a controller, so the dead letters stay the controller's")
|
||||
} else {
|
||||
go func() {
|
||||
errs <- follow(ctx, lw, func(e jetstream.KeyValueEntry) {
|
||||
var raw []byte
|
||||
if opOf(e) == watch.Put {
|
||||
raw = e.Value()
|
||||
}
|
||||
on, _ := buscompose.LeaseHandedAway(raw, len(raw) > 0, nil)
|
||||
dead.lease(on)
|
||||
})
|
||||
}()
|
||||
}
|
||||
}
|
||||
if l, err := socket.Listen(socket.PathOf(getenv)); err != nil {
|
||||
logger.Printf("the tools' socket cannot be opened (%v): dead-letters and resync answer nothing until the watchman starts again", err)
|
||||
} else {
|
||||
|
||||
@@ -105,7 +105,7 @@ func heard(w *watch.Watchman, dead *deadKeeper, m *nats.Msg, logf func(string, .
|
||||
return
|
||||
}
|
||||
if dead.isTheControllersNotices(a.ID) {
|
||||
dead.check() // the lease write's step removed it: the watchman may take the notices now
|
||||
dead.check() // the controller's consumer removed: the watchman may take the notices now
|
||||
w.RemovalGone(watch.RemovalConsumer)
|
||||
return
|
||||
}
|
||||
@@ -164,17 +164,16 @@ func hear(conn *nats.Conn, w *watch.Watchman, dead *deadKeeper, logf func(string
|
||||
return conn.Flush()
|
||||
}
|
||||
|
||||
// deadKeeper keeps what consumers gave up on from the lease write (ADR 0564 §5): it counts DEAD_LETTERS into findings,
|
||||
// and takes DEAD_LETTER_NOTICES on its own consumer only once the lease names no controller and `controller` is gone.
|
||||
// deadKeeper keeps what consumers gave up on (ADR 0564 §5): it counts DEAD_LETTERS into findings, and takes
|
||||
// DEAD_LETTER_NOTICES on its own consumer once `controller` is gone (issue 1126: no lease is read).
|
||||
type deadKeeper struct {
|
||||
ctx context.Context
|
||||
js jetstream.JetStream
|
||||
w *watch.Watchman
|
||||
logf func(string, ...any)
|
||||
|
||||
mu sync.Mutex
|
||||
handedAway bool
|
||||
taking jetstream.ConsumeContext
|
||||
mu sync.Mutex
|
||||
taking jetstream.ConsumeContext
|
||||
}
|
||||
|
||||
// ages pulses each consumer's restricted letters by when they age out, read from DEAD_LETTERS' headers (at the start and
|
||||
@@ -225,38 +224,20 @@ func (d *deadKeeper) count() {
|
||||
d.w.DeadLettersHeld(held, deadletters.ConsumerInWords)
|
||||
}
|
||||
|
||||
// lease is the lease's change heard: handed away or not.
|
||||
func (d *deadKeeper) lease(handedAway bool) {
|
||||
d.mu.Lock()
|
||||
d.handedAway = handedAway
|
||||
d.mu.Unlock()
|
||||
d.check()
|
||||
}
|
||||
|
||||
func (d *deadKeeper) away() bool {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
return d.handedAway
|
||||
}
|
||||
|
||||
// isTheControllersNotices says an advisory is about the controller's consumer on DEAD_LETTER_NOTICES once the lease is
|
||||
// handed away: its removal is the lease write's step, not a loss.
|
||||
// isTheControllersNotices says an advisory is about the controller's consumer on DEAD_LETTER_NOTICES: its removal is the
|
||||
// controller's end, not a loss.
|
||||
func (d *deadKeeper) isTheControllersNotices(id string) bool {
|
||||
return d.away() && id == buscompose.DeadLetterNoticesStream+"."+buscompose.ControllerName
|
||||
return id == buscompose.DeadLetterNoticesStream+"."+buscompose.ControllerName
|
||||
}
|
||||
|
||||
// check takes the notices, or says they wait, as Gate has it.
|
||||
func (d *deadKeeper) check() {
|
||||
away := d.away()
|
||||
stands := true
|
||||
if away {
|
||||
var err error
|
||||
if stands, err = deadletters.ControllerStands(d.ctx, d.js); err != nil {
|
||||
d.logf("whether the controller's consumer on %s stands cannot be read (%v): it is taken to stand", buscompose.DeadLetterNoticesStream, err)
|
||||
stands = true
|
||||
}
|
||||
stands, err := deadletters.ControllerStands(d.ctx, d.js)
|
||||
if err != nil {
|
||||
d.logf("whether the controller's consumer on %s stands cannot be read (%v): it is taken to stand", buscompose.DeadLetterNoticesStream, err)
|
||||
stands = true
|
||||
}
|
||||
take, waiting := deadletters.Gate(away, stands)
|
||||
take, waiting := deadletters.Gate(stands)
|
||||
d.w.NoticesWaiting(waiting)
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
@@ -269,7 +250,7 @@ func (d *deadKeeper) check() {
|
||||
return
|
||||
}
|
||||
d.taking = cc
|
||||
d.logf("the lease names no controller and its consumer on %s is gone: the watchman keeps the dead letters", buscompose.DeadLetterNoticesStream)
|
||||
d.logf("the controller's consumer on %s is gone: the watchman keeps the dead letters", buscompose.DeadLetterNoticesStream)
|
||||
}
|
||||
|
||||
// answer is the program's answer to the tools' ask on its socket.
|
||||
@@ -289,7 +270,7 @@ func answer(a socket.Ask, w *watch.Watchman, dead *deadKeeper, ask func([]byte)
|
||||
if strings.TrimSpace(a.Why) == "" {
|
||||
return nil, errors.New("a dead letter is delivered again or dropped only with why")
|
||||
}
|
||||
return nil, deadletters.Refusal(dead.away(), a.Act)
|
||||
return nil, deadletters.Refusal(a.Act)
|
||||
case a.Act != "":
|
||||
return nil, fmt.Errorf("%q is not deliver or drop", a.Act)
|
||||
case a.ID > 0:
|
||||
@@ -299,11 +280,7 @@ func answer(a socket.Ask, w *watch.Watchman, dead *deadKeeper, ask func([]byte)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keeper := "the controller (it holds its lease)"
|
||||
if dead.away() {
|
||||
keeper = "the watchman"
|
||||
}
|
||||
return map[string]any{"dead-letters": list, "total": total, "kept-by": keeper}, nil
|
||||
return map[string]any{"dead-letters": list, "total": total, "kept-by": "the watchman"}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("refused: the watchman's socket answers %q and %q", socket.AskDeadLetters, socket.AskResync)
|
||||
}
|
||||
|
||||
@@ -305,27 +305,23 @@ func Named(ctx context.Context, js jetstream.JetStream, id uint64) (DeadLetter,
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Refusal is why the watchman delivers nothing again and drops nothing: before the lease write the controller keeps them
|
||||
// and its verb acts; from it the act waits for the hand-act log's home (issue 1953), where a person's act is recorded.
|
||||
// act is deliver or drop.
|
||||
func Refusal(handedAway bool, act string) error {
|
||||
// Refusal is why the watchman delivers nothing again and drops nothing: the act waits for the hand-act log's home (issue
|
||||
// 1953), where a person's act is recorded. act is deliver or drop.
|
||||
func Refusal(act string) error {
|
||||
done := map[string]string{"deliver": "delivered again", "drop": "dropped"}[act]
|
||||
if done == "" {
|
||||
return fmt.Errorf("%q is not deliver or drop", act)
|
||||
}
|
||||
if !handedAway {
|
||||
return fmt.Errorf("the controller still holds its lease and keeps the dead letters: %s it with "+
|
||||
"mesh-controller.dead-letters (novox/hq ADR 0564 §5); nothing is %s here", act, done)
|
||||
}
|
||||
return fmt.Errorf("a dead letter is %s only as a person's act recorded in the hand-act log, whose home is not "+
|
||||
"delivered yet (issue 1953): nothing is %s", done, done)
|
||||
}
|
||||
|
||||
// Gate is whether the watchman takes the notices itself: only when the lease names no controller (handedAway) and the
|
||||
// controller's consumer is gone. waiting says the lease is handed away while `controller` still stands, which is the
|
||||
// watchman's finding: each notice waits in the queue until the lease write's step removes it.
|
||||
func Gate(handedAway, controllerStands bool) (take, waiting bool) {
|
||||
return handedAway && !controllerStands, handedAway && controllerStands
|
||||
// Gate is whether the watchman takes the notices itself: once the controller's consumer is gone. waiting says `controller`
|
||||
// still stands, which is the watchman's finding: each notice waits in the queue until that consumer is removed. The lease
|
||||
// is not read: the controller ended by its unassignment, never by the lease write (issue 1126), and mesh-sdk's
|
||||
// buscompose.LeaseHandedAway answers false for the absent key, which would keep the notices the controller's for good.
|
||||
func Gate(controllerStands bool) (take, waiting bool) {
|
||||
return !controllerStands, controllerStands
|
||||
}
|
||||
|
||||
// ControllerStands asks the bus whether the controller's consumer on DEAD_LETTER_NOTICES stands.
|
||||
|
||||
@@ -8,36 +8,32 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// 1952/WM5, ADR 0564 §5: the watchman makes no consumer while `controller` exists or the lease names a controller, makes
|
||||
// one once both are false, and says the wait meanwhile; it delivers and drops nothing, naming who does; and a kept message
|
||||
// reads as the controller's copies do, a restricted one's age and work queue kept.
|
||||
func TestTheWatchmanKeepsTheDeadLettersOnlyFromTheLeaseWrite(t *testing.T) {
|
||||
// 1952/WM5, ADR 0564 §5, 1126/A2/AC2: with no lease read at all (the controller ended by its unassignment), the watchman
|
||||
// makes no consumer while `controller` stands on DEAD_LETTER_NOTICES and says the wait, and makes its own once it is gone;
|
||||
// it delivers and drops nothing, naming why; and a kept message reads as the controller's copies do, a restricted one's
|
||||
// age and work queue kept.
|
||||
func TestTheWatchmanKeepsTheDeadLetters(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
handedAway, controllerStands, take, waiting bool
|
||||
controllerStands, take, waiting bool
|
||||
}{
|
||||
{false, true, false, false},
|
||||
{false, false, false, false},
|
||||
{true, true, false, true},
|
||||
{true, false, true, false},
|
||||
{true, false, true},
|
||||
{false, true, false},
|
||||
} {
|
||||
take, waiting := Gate(c.handedAway, c.controllerStands)
|
||||
take, waiting := Gate(c.controllerStands)
|
||||
if take != c.take || waiting != c.waiting {
|
||||
t.Errorf("Gate(%v, %v) = %v, %v", c.handedAway, c.controllerStands, take, waiting)
|
||||
t.Errorf("Gate(%v) = %v, %v", c.controllerStands, take, waiting)
|
||||
}
|
||||
}
|
||||
for _, c := range []struct {
|
||||
away bool
|
||||
act string
|
||||
says string
|
||||
}{
|
||||
{false, "deliver", "mesh-controller.dead-letters"},
|
||||
{false, "drop", "nothing is dropped"},
|
||||
{true, "drop", "issue 1953"},
|
||||
{true, "deliver", "delivered again"},
|
||||
{true, "send", "not deliver or drop"},
|
||||
{"drop", "issue 1953"},
|
||||
{"deliver", "delivered again"},
|
||||
{"send", "not deliver or drop"},
|
||||
} {
|
||||
if err := Refusal(c.away, c.act); err == nil || !strings.Contains(err.Error(), c.says) {
|
||||
t.Errorf("Refusal(%v, %s) = %v, want it to say %q", c.away, c.act, err, c.says)
|
||||
if err := Refusal(c.act); err == nil || !strings.Contains(err.Error(), c.says) {
|
||||
t.Errorf("Refusal(%s) = %v, want it to say %q", c.act, err, c.says)
|
||||
}
|
||||
}
|
||||
at := time.Date(2026, 10, 29, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
@@ -90,7 +90,7 @@ func TestManifestStateAndReads(t *testing.T) { // 1135/AC6
|
||||
}
|
||||
reads := slices.Clone(m.Reads)
|
||||
slices.Sort(reads)
|
||||
if !reflect.DeepEqual(reads, []string{"mesh-build-go.steps", "mesh-build-image.steps", "mesh-build-node.steps", "mesh-controller.lease",
|
||||
if !reflect.DeepEqual(reads, []string{"mesh-build-go.steps", "mesh-build-image.steps", "mesh-build-node.steps",
|
||||
"mesh-delivery-assigner.wants", "mesh-delivery-indexer.modules", "mesh-host.health", "mesh-provisioner.provisions", "node-engine.have"}) {
|
||||
t.Errorf("reads = %v", reads)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
"mesh-build-go.steps",
|
||||
"mesh-build-image.steps",
|
||||
"mesh-build-node.steps",
|
||||
"mesh-controller.lease",
|
||||
"mesh-delivery-assigner.wants",
|
||||
"mesh-delivery-indexer.modules",
|
||||
"mesh-host.health",
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
@@ -159,11 +158,11 @@ func checkTunnelKey(k string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// issuing says whether issue is on: only while the controller's lease is handed to a holder that is no controller (ADR 0587
|
||||
// §4), and why either way.
|
||||
// issuing says whether issue is on, and why: always, since the controller ended by its unassignment, never by the lease
|
||||
// write (issue 1126), so the seat is the only issuer (ADR 0587 §4). The lease is no longer read: mesh-sdk's
|
||||
// buscompose.LeaseHandedAway answers false for the absent key, which would keep issuing off for good.
|
||||
func (h *Holder) issuing() (bool, string) {
|
||||
raw, found, err := h.Read.Get("mesh-controller.lease", buscompose.ControllerLeaseKey)
|
||||
return buscompose.LeaseHandedAway(raw, found, err)
|
||||
return true, "the seat is the only issuer: the controller ended by its unassignment (issue 1126)"
|
||||
}
|
||||
|
||||
// openEnrolments reads every node key of the bucket.
|
||||
|
||||
@@ -220,11 +220,9 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
return kv
|
||||
}
|
||||
enrolments := mk(delivery.EnrolmentsBucket, true)
|
||||
logins, lease, placement := mk(delivery.NodeLoginsBucket, false), mk("mesh-controller_lease", false), mk("mesh-delivery-assigner_placement", false)
|
||||
// No controller lease bucket at all: the controller ended by its unassignment (issue 1126).
|
||||
logins, placement := mk(delivery.NodeLoginsBucket, false), mk("mesh-delivery-assigner_placement", false)
|
||||
have := mk("node-engine_have", false)
|
||||
if _, err := lease.Put(ctx, "holder", []byte(`{"handed-to":"mesh-grants","handed-by":"operator"}`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := placement.Put(ctx, "hub.node", []byte(hubPlacement)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ type IssueAnswer struct {
|
||||
}
|
||||
|
||||
// Issue opens an enrolment (ADR 0587 §4): it refuses at once, naming every finding, a name that is no node name or that a
|
||||
// node record, a placement key or an open enrolment holds, and every call before the lease write; chooses the lowest free
|
||||
// node record, a placement key or an open enrolment holds; chooses the lowest free
|
||||
// address and reserves it by creating its key (the bus refuses a key that exists, so two issues never get one address);
|
||||
// writes the key at stage asked; and asks the operator, on a channel that proves who answered. The token is made only on
|
||||
// the operator's yes (Warranted).
|
||||
@@ -68,10 +68,6 @@ func (h *Holder) Issue(caller, node, why, overlayKey string, lasts time.Duration
|
||||
if lasts < 0 || lasts > delivery.MostTokenLife {
|
||||
findings = append(findings, fmt.Sprintf("the expiry %s is outside zero to %s", lasts, delivery.MostTokenLife))
|
||||
}
|
||||
if on, reason := h.issuing(); !on {
|
||||
findings = append(findings, "issuing is off until the controller's lease is written to a holder that is no controller: "+reason+
|
||||
"; until then the controller's `token issue` is the only issuer (ADR 0587 §4)")
|
||||
}
|
||||
placements, perr := h.placements()
|
||||
if perr != nil {
|
||||
findings = append(findings, "placement cannot be read: "+perr.Error())
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
|
||||
var yes = asks.Option{ID: "yes"}
|
||||
|
||||
// 774/S55/AC1: issue refuses, naming every finding, what it must: the lease with the controller, a bad name, a name a node
|
||||
// record, a placement key or an open enrolment holds, and an expiry past a day.
|
||||
// 774/S55/AC1: issue refuses, naming every finding, what it must: a bad name, a name a node record, a placement key or an
|
||||
// open enrolment holds, and an expiry past a day; never the controller's lease, which no rig writes (issue 1126).
|
||||
func issueRefusesNamingEveryFinding(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
@@ -24,8 +24,6 @@ func issueRefusesNamingEveryFinding(t *testing.T) {
|
||||
lasts time.Duration
|
||||
want []string
|
||||
}{
|
||||
{"the lease names the controller", func(r *rig) { r.rd["mesh-controller.lease"]["holder"] = `{"instance":"novox.mesh-controller"}` }, "ace", 0,
|
||||
[]string{"issuing is off until the controller's lease is written", "token issue"}},
|
||||
{"a bad name and a long expiry", nil, "Ace.1", 48 * time.Hour, []string{"not a valid node name", "outside zero to 24h0m0s"}},
|
||||
{"a placement key and a node record", func(r *rig) {
|
||||
r.rd["mesh-delivery-assigner.placement"]["ace.node"] = `{"v":1,"node":"ace","modules":[]}`
|
||||
|
||||
@@ -201,7 +201,7 @@ type said struct {
|
||||
Body json.RawMessage
|
||||
}
|
||||
|
||||
// rig is a holder over fakes, with the lease handed to mesh-grants, one hub in placement and a clock the test moves.
|
||||
// rig is a holder over fakes, with no controller lease at all (issue 1126), one hub in placement and a clock the test moves.
|
||||
type rig struct {
|
||||
mu sync.Mutex
|
||||
t *testing.T
|
||||
@@ -230,7 +230,6 @@ const hubPlacement = `{"v":1,"node":"hub","decided":{"hub":true,"endpoint":"192.
|
||||
func newRig(t *testing.T) *rig {
|
||||
r := &rig{t: t, st: newMem(), vault: &fakeVault{fail: map[string]bool{}}, asker: &fakeAsker{}, now: time.Date(2026, 11, 2, 10, 0, 0, 0, time.UTC), loaded: true}
|
||||
r.rd = memRead{
|
||||
"mesh-controller.lease": {"holder": `{"handed-to":"mesh-grants","handed-by":"operator"}`},
|
||||
"mesh-delivery-assigner.placement": {"hub.node": hubPlacement},
|
||||
"node-engine.have": {"hub.node": hubRecord},
|
||||
}
|
||||
|
||||
@@ -59,12 +59,11 @@ func tokensAnswersEachStageAndRelaysTheSealedTokenOfOneNode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Before the lease write issuing is off, and says why.
|
||||
r.rd["mesh-controller.lease"]["holder"] = `{"instance":"novox.mesh-controller"}`
|
||||
off, _ := r.h.Tokens("", false)
|
||||
mustContain(t, off.Issuing.Why, "controller holds the lease")
|
||||
if off.Issuing.On {
|
||||
t.Error("issuing is on while the controller holds the lease")
|
||||
// 1126/A2/AC1: with no lease key issuing is on, and says why.
|
||||
on, _ := r.h.Tokens("", false)
|
||||
mustContain(t, on.Issuing.Why, "unassignment")
|
||||
if !on.Issuing.On {
|
||||
t.Errorf("issuing is off with no lease key: %s", on.Issuing.Why)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +82,7 @@ func theManifestSaysWhatTheCodeDoes(t *testing.T) {
|
||||
Serves []string `json:"serves"`
|
||||
} `json:"claims"`
|
||||
Emits []string `json:"emits"`
|
||||
Reads []string `json:"reads"`
|
||||
State []json.RawMessage `json:"state"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
@@ -92,6 +92,9 @@ func theManifestSaysWhatTheCodeDoes(t *testing.T) {
|
||||
!slices.Equal(m.Claims[0].Serves, Verbs) || len(m.Emits) != 0 {
|
||||
t.Errorf("module %q runs as %q, claims %+v, emits %v", m.Module, m.Runs, m.Claims, m.Emits)
|
||||
}
|
||||
if slices.Contains(m.Reads, "mesh-controller.lease") {
|
||||
t.Error("the manifest reads the controller's lease, which the code no longer reads (issue 1126)")
|
||||
}
|
||||
var heard []string
|
||||
for _, e := range EndEvents {
|
||||
heard = append(heard, Seat+"."+e)
|
||||
|
||||
@@ -48,8 +48,7 @@ func Tools(h *Holder) []stdio.Tool {
|
||||
{Name: IssueTool,
|
||||
Description: "Open an enrolment: let a new machine join the mesh (novox/hq ADR 0587 §4). Takes the node's name, a why and an " +
|
||||
"expiry (one hour unless given, at most 24 hours). Refused at once, naming every finding, for a name that is no node name " +
|
||||
"or that a node record, a placement key or an open enrolment holds, and for every call before the controller's lease is " +
|
||||
"written to a holder that is no controller. Otherwise it chooses the lowest free private-network address, reserves it, " +
|
||||
"or that a node record, a placement key or an open enrolment holds. Otherwise it chooses the lowest free private-network address, reserves it, " +
|
||||
"writes the enrolment at stage asked and asks the operator on a channel that proves who answered; only their yes adds the " +
|
||||
"node to placement with the machine's tunnel key (overlay_key, as `nox-mesh-host key` prints it there), so the hub peers " +
|
||||
"it, and then makes the token, which carries the first tunnel (the vault makes it, this seat never holds it). The caller is the one the runtime proves.",
|
||||
@@ -107,8 +106,7 @@ func Tools(h *Holder) []stdio.Tool {
|
||||
Run: func(a map[string]any) (any, error) { return h.Again(arg(a, "node")) }},
|
||||
{Name: TokensTool,
|
||||
Description: "What the seat holds (novox/hq ADR 0587 §8): every open enrolment, or one node's, with its stage (asked, live or " +
|
||||
"redeeming), address, expiry, ask, revision and age; whether issuing is on and why (it is on only from the controller's " +
|
||||
"lease write); the bucket's fill. With a node at stage live, the token sealed to the operator key, asked of the vault at " +
|
||||
"redeeming), address, expiry, ask, revision and age; whether issuing is on and why; the bucket's fill. With a node at stage live, the token sealed to the operator key, asked of the vault at " +
|
||||
"this moment and relayed as the vault sealed it: the one route by which the operator gets a token, which this seat never " +
|
||||
"reads and keeps nowhere. With history=true the ended enrolments its events say. A machine stuck at redeeming shows here " +
|
||||
"with its age; revoke ends it. Read-only.",
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
"seat:mesh-vault.sign"
|
||||
],
|
||||
"reads": [
|
||||
"mesh-controller.lease",
|
||||
"mesh-delivery-assigner.placement",
|
||||
"mesh-grants.user-list",
|
||||
"mesh-vault.signers",
|
||||
|
||||
@@ -118,8 +118,8 @@ func toolsOf(p *grants.Part) []stdio.Tool {
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
return p.TakeOver(stdio.CallerOf(a), grants.Answer(a, "why"))
|
||||
}},
|
||||
{Name: grants.ToolHandBack, Description: "Ask the operator to switch the bus node back to the controller's list (ADR 0547 clause 8), " +
|
||||
"by their yes on their channel; in shadow there is nothing to hand back, and it says so. Every act is recorded.",
|
||||
{Name: grants.ToolHandBack, Description: "Gone: the controller ended by its unassignment (issue 1126), so nothing writes its user list and the grants " +
|
||||
"part's list is the only one; it says so and asks nothing (ADR 0547 §11).",
|
||||
Input: schema(map[string]string{"why": "why, kept with the act"}, "why"),
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
return p.HandBack(stdio.CallerOf(a), grants.Answer(a, "why"))
|
||||
@@ -129,15 +129,14 @@ func toolsOf(p *grants.Part) []stdio.Tool {
|
||||
"wrote it to its state accounts, which the vault alone reads: user, kind, node, module, process, the bus's address and " +
|
||||
"fingerprint, a module's claims with the verbs each seat promises, and when the key was written; one user's or all; " +
|
||||
"what the last pass wrote, deleted or kept; and the durable consumers the accounts need as the last pass found them " +
|
||||
"(missing, differing and why, created; whether the lease is handed away). Never a password. Read-only: " +
|
||||
"(missing, differing and why, created). Never a password. Read-only: " +
|
||||
"grants_compose_now writes them again.",
|
||||
Input: schema(map[string]string{"user": "one bus user, e.g. ace.mesh-build-go"}),
|
||||
Run: func(a map[string]any) (any, error) { return p.Accounts(grants.Answer(a, "user")) }},
|
||||
{Name: grants.ToolConsumerMatch, Description: "Bring one durable consumer an account needs (a module's on EVENTS, a process's on " +
|
||||
"its work queue; novox/hq ADR 0561 §6) to the configuration the bus composer composes, by hand: the recovery when " +
|
||||
"the condition mesh-grants.consumer-differs names it. Asks the part's process, which alone may, and keeps where the " +
|
||||
"consumer delivers from. Refused while the controller holds its lease (the consumers are the controller's then) and " +
|
||||
"for a consumer the records do not compose. grants_user_list's last pass says what became of it.",
|
||||
"consumer delivers from. Refused for a consumer the records do not compose. grants_user_list's last pass says what became of it.",
|
||||
Input: schema(map[string]string{"stream": "the stream, e.g. EVENTS", "consumer": "the consumer, e.g. ace_mesh-build-go"}, "stream", "consumer"),
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
return p.MatchConsumer(grants.Answer(a, "stream"), grants.Answer(a, "consumer"))
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -540,7 +539,7 @@ func TestGrants(t *testing.T) {
|
||||
gap string
|
||||
}{
|
||||
{"every record on the bus", func(*fakeBus) {},
|
||||
[]string{"a.mod-x", "a.nats", "b.mod-y", "controller", "node.a", "node.b"}, "enrolment tokens"},
|
||||
[]string{"a.mod-x", "a.nats", "b.mod-y", "controller", "node.a", "node.b"}, "people"},
|
||||
{"a manifest missing is a gap and its user is left out", func(b *fakeBus) { delete(b.states[grants.ReadModules], "mod-y") },
|
||||
[]string{"a.mod-x", "a.nats", "controller", "node.a", "node.b"}, "manifest of mod-y"},
|
||||
{"a want's built manifest is composed, not the trunk's; the list's records carry both (issue 774)", func(b *fakeBus) {
|
||||
@@ -551,7 +550,7 @@ func TestGrants(t *testing.T) {
|
||||
b.put(grants.ReadWants, "want.a.mod-x", delivery.Want{V: delivery.Version, FP: fp, Composed: "sha256:" + strings.Repeat("ef", 32)})
|
||||
key, _ := delivery.BuilderBuildKey("mod-x", fp)
|
||||
b.put(grants.ReadBuilds, key, map[string]any{"v": delivery.Version, "state": "done", "manifest": digest})
|
||||
}, []string{"a.mod-x", "a.nats", "b.mod-y", "controller", "node.a", "node.b"}, "enrolment tokens"},
|
||||
}, []string{"a.mod-x", "a.nats", "b.mod-y", "controller", "node.a", "node.b"}, "people"},
|
||||
{"placement refused is a gap; the machines come from the haves", func(b *fakeBus) { b.refused[grants.ReadPlacement] = true },
|
||||
[]string{"controller", "node.a", "node.b"}, grants.ReadPlacement},
|
||||
} {
|
||||
@@ -1316,7 +1315,7 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
// Written in shadow, the bus node told to follow the controller's list; every user left out while the vault holds
|
||||
// no hash; a tool's part (no object bucket) writes none.
|
||||
if g, _, had := p.Sealed(); !had || g.Follow != delivery.FollowController || g.Revision != 1 || g.Node != "a" ||
|
||||
if g, _, had := p.Sealed(); !had || g.Follow != delivery.FollowController || !g.LeaseHanded || g.Revision != 1 || g.Node != "a" ||
|
||||
g.Users != 0 || len(g.LeftOut) != 6 || len(objs.objects) != 1 || !strings.Contains(open(g), "accounts {") {
|
||||
t.Errorf("the sealed list in shadow = %+v, %d objects", g, len(objs.objects))
|
||||
}
|
||||
@@ -1328,14 +1327,15 @@ func TestGrants(t *testing.T) {
|
||||
} else if _, _, had := tp.Sealed(); had {
|
||||
t.Error("a tool's part wrote the sealed list")
|
||||
}
|
||||
if len(first.Published) != 3 || len(b.publishes) != 3 {
|
||||
t.Errorf("the first composition published %v, want the three assignments' memberships", first.Published)
|
||||
// Written to the state alone, never said on their subject, with no lease key (1126/A1).
|
||||
if len(first.Published) != 3 || first.MembershipsWritten != 3 || len(b.publishes) != 0 {
|
||||
t.Errorf("the first composition published %v (%d on the subject), want the three assignments' memberships in the state alone", first.Published, len(b.publishes))
|
||||
}
|
||||
// The second pass says each membership unchanged (published at the first); the third writes nothing at all.
|
||||
if again := p.Compose(); again.Written || again.Revision != 1 || len(again.Published) != 0 || again.AssertionsWritten != 0 {
|
||||
t.Errorf("a composition with no input changed wrote or published: %+v", again)
|
||||
}
|
||||
if third := p.Compose(); third.Written || third.MembershipsWritten != 0 || len(third.Published) != 0 || len(b.publishes) != 3 {
|
||||
if third := p.Compose(); third.Written || third.MembershipsWritten != 0 || len(third.Published) != 0 || len(b.publishes) != 0 {
|
||||
t.Errorf("a membership equal to the one last published was published again, or a state written: %+v", third)
|
||||
}
|
||||
// mod-x placed on b too: b.mod-x is added and its first membership published; a.mod-x's loses the module's plain
|
||||
@@ -1402,8 +1402,6 @@ func TestGrants(t *testing.T) {
|
||||
var out any
|
||||
if act == "take-over" {
|
||||
out, err = p.TakeOver("shanks.node-tools", "a row")
|
||||
} else {
|
||||
out, err = p.HandBack("shanks.node-tools", "a row")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", act, err)
|
||||
@@ -1452,7 +1450,6 @@ func TestGrants(t *testing.T) {
|
||||
}, []string{"a.mod-x: 1 read"}},
|
||||
{"a yes with only added rights and every hash held records the switch, the ask naming them", "take-over", yes, settled(lacks), nil, nil, grants.TookOver, "recorded",
|
||||
nil, []string{"adds 1 right to 1 user and takes none away", "a.mod-x: 1 read"}},
|
||||
{"a yes to hand back records the switch back", "hand-back", yes, func() {}, nil, nil, grants.Shadow, "recorded", nil, nil},
|
||||
} {
|
||||
t.Run(row.name, func(t *testing.T) {
|
||||
row.setup()
|
||||
@@ -1522,8 +1519,8 @@ func TestGrants(t *testing.T) {
|
||||
// The key the bus node follows turns with the switch at once, and the state says switched only once the bus
|
||||
// node loaded the grants part's written list.
|
||||
g, _, _ := p.Sealed()
|
||||
if want := map[string]string{grants.TookOver: delivery.FollowGrants, grants.Shadow: delivery.FollowController}[p.SwitchState()]; g.Follow != want {
|
||||
t.Errorf("the key says follow %s with the switch %s", g.Follow, p.SwitchState())
|
||||
if want := map[string]string{grants.TookOver: delivery.FollowGrants, grants.Shadow: delivery.FollowController}[p.SwitchState()]; g.Follow != want || !g.LeaseHanded {
|
||||
t.Errorf("the key says follow %s, lease handed %v, with the switch %s and no lease key (1126/A1/AC2)", g.Follow, g.LeaseHanded, p.SwitchState())
|
||||
}
|
||||
effect := func() string { st, _ := p.TakeOverState(); return fmt.Sprint(st.(map[string]any)["effect"]) }
|
||||
if row.outcome == "recorded" && row.act == "take-over" {
|
||||
@@ -1663,10 +1660,11 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Run("hand-back in shadow is refused at once, with nothing asked", func(t *testing.T) {
|
||||
// 1126/A1: the controller ended by its unassignment, so nothing writes its list and a hand-back is gone.
|
||||
t.Run("hand-back is gone at once, with nothing asked", func(t *testing.T) {
|
||||
n := len(fa.asks)
|
||||
if _, err := p.HandBack("shanks.node-tools", "a row"); err == nil || !strings.Contains(err.Error(), "nothing to hand back") {
|
||||
t.Errorf("hand-back in shadow = %v", err)
|
||||
if _, err := p.HandBack("shanks.node-tools", "a row"); err == nil || !strings.HasPrefix(err.Error(), "gone") {
|
||||
t.Errorf("hand-back = %v", err)
|
||||
}
|
||||
p.Compose()
|
||||
if len(fa.asks) != n {
|
||||
@@ -1674,6 +1672,8 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
})
|
||||
t.Run("an ask that can stay open is closed every way and can be asked again", func(t *testing.T) {
|
||||
// The rows above end taken over and a hand-back is gone (1126/A1): asked again from a part in shadow.
|
||||
delete(b.own[grants.StateTakeover], grants.KeySwitch)
|
||||
// A warrant the process missed is caught up from the router's record, whatever its outcome.
|
||||
id := asked("take-over")
|
||||
p.Compose()
|
||||
@@ -1843,11 +1843,10 @@ func TestGrants(t *testing.T) {
|
||||
if row.names == "" && row.last != nil && !strings.Contains(string(state), "10.0.0.1") {
|
||||
t.Errorf("the private network of the last membership is not carried: %s", state)
|
||||
}
|
||||
// Written to the state alone, never said on its subject (1126/A1).
|
||||
got, err := membership.DecodeGrantsMembership(state)
|
||||
var sent membership.Membership
|
||||
_ = json.Unmarshal(b.stream["mesh.assignment."+key], &sent)
|
||||
if err != nil || !reflect.DeepEqual(got, sent) || bytes.Contains(state, []byte(`"membership"`)) {
|
||||
t.Errorf("the state is not the bare membership published (%v): %s", err, state)
|
||||
if err != nil || got.Node != "a" || got.Module != "mod-x" || bytes.Contains(state, []byte(`"membership"`)) || len(b.stream) != 0 {
|
||||
t.Errorf("the state is not the bare membership published, or its subject said (%v, %d said): %s", err, len(b.stream), state)
|
||||
}
|
||||
if !strings.Contains(pass, row.names) {
|
||||
t.Errorf("the pass does not name %q: %s", row.names, pass)
|
||||
@@ -1866,23 +1865,6 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
// The assignments stream removed (nats_stream_remove at the lease write) and its subject refused: every membership
|
||||
// is still written to the state, where each runtime reads it, and the refusal is said (issue 774).
|
||||
t.Run("the stream gone and its subject refused: memberships still written, the refusal said", func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.loaded = shadowOf(t, b)
|
||||
b.refusePub = true
|
||||
p := partOn(b)
|
||||
refused := p.Compose()
|
||||
if !slices.Equal(refused.Published, []string{"a.mod-x", "a.nats", "b.mod-y"}) || refused.MembershipsWritten != 3 ||
|
||||
!strings.Contains(strings.Join(refused.Errors, "\n"), "Permissions Violation") {
|
||||
t.Errorf("with the stream gone = %+v: every membership written, the refusal said", refused)
|
||||
}
|
||||
b.refusePub = false
|
||||
if next := p.Compose(); len(next.Published) != 0 || next.MembershipsWritten != 0 {
|
||||
t.Errorf("the next event published %v again", next.Published)
|
||||
}
|
||||
})
|
||||
t.Run("a tool answers while a pass waits on the bus", func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.loaded = shadowOf(t, b)
|
||||
@@ -2251,7 +2233,7 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"handed-to": "the operator's hand-over"})
|
||||
// With no lease key (1126/A1).
|
||||
// The private network the mesh provides is placed with no fingerprint by construction: never a missing grant.
|
||||
b.assign("b", "mesh-wireguard")
|
||||
fp := "src2:d4afe7a1abab5ccdbef6d8629b2245006a2aec60290f430f7b4756e2930ed60c"
|
||||
@@ -2422,7 +2404,6 @@ func TestGrants(t *testing.T) {
|
||||
b := fixture()
|
||||
b.manifestOf("mesh-enrolment", enrolmentManifest)
|
||||
b.assign("a", "mesh-enrolment")
|
||||
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"handed-to": "the operator's hand-over"})
|
||||
issued := time.Date(2026, 11, 2, 1, 0, 0, 0, time.UTC)
|
||||
for node, stage := range map[string]string{"lab": delivery.EnrolmentLive, "pi": delivery.EnrolmentAsked} {
|
||||
raw, err := delivery.EncodeEnrolment(delivery.Enrolment{Node: node, Address: "10.0.0.7", Why: "joins",
|
||||
@@ -2545,27 +2526,21 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
// 774/AC6 (novox/hq ADR 0561 §6): the durable consumers an account needs, with the controller's configuration, are
|
||||
// compared in shadow while the controller holds the lease, and created, only where the bus lacks them, from the write
|
||||
// that hands the lease away; one the bus holds is never changed.
|
||||
t.Run("account consumers: compared while the controller holds the lease, created once it is handed away", func(t *testing.T) {
|
||||
// 774/AC6 (novox/hq ADR 0561 §6), 1126/A1/AC1: the durable consumers an account needs, with the composed
|
||||
// configuration, are created where the bus lacks them with no lease key at all (the controller ended by its
|
||||
// unassignment, never by the lease write); one the bus holds is never changed.
|
||||
t.Run("account consumers: created where the bus lacks them, with no lease key", func(t *testing.T) {
|
||||
const cManifest = `{"module":"mod-c","version":"1","own-secrets":{"broker":"/var/lib/c/broker"},"consumes":["mod-y.done"]}`
|
||||
b := fixture()
|
||||
b.manifestOf("mod-c", cManifest)
|
||||
b.assign("b", "mod-c")
|
||||
b.loaded = shadowOf(t, b)
|
||||
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"instance": "novox/1/2026-10-27", "epoch": 57})
|
||||
p := partOn(b)
|
||||
p.Consumers = b
|
||||
res := p.Compose()
|
||||
if res.Consumers == nil || res.Consumers.On || !slices.Contains(res.Consumers.Missing, "EVENTS.b_mod-c") || len(b.consumerCreates) != 0 {
|
||||
t.Fatalf("while the controller holds the lease: %+v, created %v", res.Consumers, b.consumerCreates)
|
||||
}
|
||||
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"handed-to": "the operator's hand-over"})
|
||||
b.consumers = map[string][]map[string]any{"EVENTS": {{"name": "b_other", "filters": []string{"x"}, "max_deliver": 5}}}
|
||||
res = p.Compose()
|
||||
if !res.Consumers.On || !slices.Equal(res.Consumers.Created, []string{"EVENTS.b_mod-c"}) {
|
||||
t.Fatalf("once handed away: %+v", res.Consumers)
|
||||
res := p.Compose()
|
||||
if res.Consumers == nil || !slices.Equal(res.Consumers.Created, []string{"EVENTS.b_mod-c"}) {
|
||||
t.Fatalf("with no lease key: %+v", res.Consumers)
|
||||
}
|
||||
var made map[string]any
|
||||
for _, c := range b.consumers["EVENTS"] {
|
||||
@@ -2581,7 +2556,7 @@ func TestGrants(t *testing.T) {
|
||||
t.Errorf("a consumer the bus holds was asked again: %+v, %v", res.Consumers, b.consumerCreates)
|
||||
}
|
||||
// A held consumer that differs is never changed by a pass: it is raised as a condition naming it, and brought to
|
||||
// the composed configuration only by hand (grants_consumer_match), never while the controller holds the lease.
|
||||
// the composed configuration only by hand (grants_consumer_match), with no lease key too.
|
||||
var said []asks.Condition
|
||||
p.Say = func(c asks.Condition) error {
|
||||
if c.Key != grants.GrantMissingCondition && c.Key != grants.RefusedCondition { // their own rows
|
||||
@@ -2596,11 +2571,6 @@ func TestGrants(t *testing.T) {
|
||||
!strings.Contains(said[0].Explanation, "EVENTS.b_mod-c") {
|
||||
t.Fatalf("a differing consumer: %+v, said %+v", res.Consumers, said)
|
||||
}
|
||||
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"instance": "novox/2/2026-10-27", "epoch": 58})
|
||||
if _, err := p.MatchConsumer("EVENTS", "b_mod-c"); err == nil || !strings.Contains(err.Error(), "controller holds the lease") {
|
||||
t.Errorf("matched by hand under the controller's lease: %v", err)
|
||||
}
|
||||
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"handed-to": "the operator's hand-over"})
|
||||
if _, err := p.MatchConsumer("EVENTS", "b_nothing"); err == nil {
|
||||
t.Error("a consumer the records do not compose was asked to match")
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ type ConsumerCreator interface {
|
||||
|
||||
// ConsumersState is what one pass found and did of the account consumers.
|
||||
type ConsumersState struct {
|
||||
// On says the lease is handed away, so the part creates what the bus lacks; Why says why it is on or off.
|
||||
// On says the part creates what the bus lacks: always, since the controller ended by its unassignment (issue 1126);
|
||||
// Why says so.
|
||||
On bool `json:"on"`
|
||||
Why string `json:"why"`
|
||||
// Wanted is how many the records compose; Missing those the bus lacks (`<stream>.<name>`), Differ those it holds
|
||||
@@ -88,14 +89,13 @@ func heldConsumers(c Caller, stream string) (map[string]heldConsumer, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// leaseHandedAway reads the lease as the part's reads give it.
|
||||
func leaseHandedAway(r Reader) (bool, string) {
|
||||
raw, found, err := r.Get(ReadLease, LeaseKey)
|
||||
return buscompose.LeaseHandedAway(raw, found, err)
|
||||
}
|
||||
// ConsumersOn is why the part creates and matches the account consumers with no lease read: the controller ended by its
|
||||
// unassignment, never by the lease write, so its lease names no holder for good and the part is their only maker
|
||||
// (issue 1126; mesh-sdk's buscompose.LeaseHandedAway answers false for an absent key, so it is no longer asked here).
|
||||
const ConsumersOn = "the grants part makes and matches the account consumers: the controller ended by its unassignment (issue 1126)"
|
||||
|
||||
// consumers compares the account consumers the records compose with those the bus holds, and creates the missing ones
|
||||
// once the lease is handed away and the part holds a creator.
|
||||
// where the part holds a creator.
|
||||
func (p *Part) consumers(res *Result, recs buscompose.Records, match string) {
|
||||
users, err := buscompose.Users(recs)
|
||||
if err != nil {
|
||||
@@ -103,7 +103,7 @@ func (p *Part) consumers(res *Result, recs buscompose.Records, match string) {
|
||||
return
|
||||
}
|
||||
st := &ConsumersState{}
|
||||
st.On, st.Why = leaseHandedAway(p.Read)
|
||||
st.On, st.Why = true, ConsumersOn
|
||||
res.Consumers = st
|
||||
wanted := buscompose.AccountConsumers(users)
|
||||
st.Wanted = len(wanted)
|
||||
@@ -154,9 +154,8 @@ func (p *Part) consumers(res *Result, recs buscompose.Records, match string) {
|
||||
}
|
||||
|
||||
// sayDiffer raises the warning condition mesh-grants.consumer-differs while an account consumer the part owns (from the
|
||||
// lease write) differs from its composed configuration, naming each with the fields that differ, and clears it when they
|
||||
// match. Said once per set of differences, again when it changes. Before the lease write the consumers are the
|
||||
// controller's, and a difference is only said in the pass.
|
||||
// controller's end) differs from its composed configuration, naming each with the fields that differ, and clears it when
|
||||
// they match. Said once per set of differences, again when it changes.
|
||||
func (p *Part) sayDiffer(st *ConsumersState) {
|
||||
if p.Say == nil {
|
||||
return
|
||||
@@ -190,15 +189,12 @@ func (p *Part) sayDiffer(st *ConsumersState) {
|
||||
const ConsumerDiffersCondition = "mesh-grants.consumer-differs"
|
||||
|
||||
// MatchConsumer is the tool consumer-match (ADR 0561 §6, §9): it asks the part's process to bring one account consumer
|
||||
// the records compose to its composed configuration, keeping where it delivers from. Refused while the controller holds
|
||||
// the lease (the consumers are the controller's then), and for a consumer the records do not compose.
|
||||
// the records compose to its composed configuration, keeping where it delivers from. Refused for a
|
||||
// consumer the records do not compose.
|
||||
func (p *Part) MatchConsumer(stream, name string) (any, error) {
|
||||
if stream == "" || name == "" {
|
||||
return nil, errors.New("consumer-match names a stream and a consumer")
|
||||
}
|
||||
if on, why := leaseHandedAway(p.Read); !on {
|
||||
return nil, fmt.Errorf("not asked: %s; the controller makes and matches the consumers while it holds the lease", why)
|
||||
}
|
||||
in := ReadInputs(p.Read, &p.cache)
|
||||
recs, _ := Records(in)
|
||||
users, err := buscompose.Users(recs)
|
||||
|
||||
@@ -314,15 +314,15 @@ func orSaid(s, none string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
// sayMissing raises mesh-grants.grant-missing while a placed module has no grant, once the controller's lease is handed
|
||||
// away (the part's grants are then the ones the assigner reads), and clears it when every one has one; in shadow the
|
||||
// pass and the tool say them. Said once per set, again when it changes, and always at the first pass after a start.
|
||||
// sayMissing raises mesh-grants.grant-missing while a placed module has no grant (the part's grants are the ones the
|
||||
// assigner reads since the controller's end, issue 1126), and clears it when every one has one. Said once per set, again
|
||||
// when it changes, and always at the first pass after a start.
|
||||
func (p *Part) sayMissing(missing []string) {
|
||||
if p.Say == nil {
|
||||
return
|
||||
}
|
||||
why := ""
|
||||
if on, _ := leaseHandedAway(p.Read); on && len(missing) > 0 {
|
||||
if len(missing) > 0 {
|
||||
why = strings.Join(missing, "; ")
|
||||
}
|
||||
p.holdMu.Lock()
|
||||
|
||||
@@ -81,8 +81,6 @@ type Gap struct {
|
||||
|
||||
// Standing gaps: what the controller composes from its own store, which no bus value carries in this milestone.
|
||||
var standingGaps = []Gap{
|
||||
{Input: "enrolment tokens", Why: "while the controller holds the lease, its live enrolment tokens are in its store alone: " +
|
||||
"an enrolment user is composed only from the seat mesh-enrolment's keys"},
|
||||
{Input: "people", Why: "the people and the tools they may invoke are in the controller's store alone: no person user is composed"},
|
||||
{Input: "seat holdings", Why: "who holds each seat held once for the mesh is in the controller's store alone: a mesh seat claimed on two machines is composed as held on both"},
|
||||
{Input: "announced states", Why: "the controller does not announce its states-announced readable, so every read is composed from `reads`, as when announcements cannot be read"},
|
||||
@@ -131,9 +129,6 @@ type Inputs struct {
|
||||
// NodeLogins is each node's key of the vault's node-logins (ReadNodeLogins, ADR 0583 §1); a node absent logs in as
|
||||
// node.<n> alone.
|
||||
NodeLogins map[string]delivery.NodeLogin
|
||||
// LeaseAway says the controller's lease is handed away, as ReadInputs read it: an enrolment user then enrols through
|
||||
// the seat alone (ADR 0587 §6).
|
||||
LeaseAway bool
|
||||
// Refused is every manifest the indexer holds that the part cannot read or mesh-sdk's reader refuses, assigned or
|
||||
// not, each with why: its module counts as placed nowhere, so it is said loudly, never passed over (issue 1945).
|
||||
Refused []Gap
|
||||
@@ -280,7 +275,6 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
|
||||
in.Placed[node] = p
|
||||
}
|
||||
}
|
||||
in.LeaseAway, _ = leaseHandedAway(r)
|
||||
for node, p := range in.Placed {
|
||||
nodes[node] = true
|
||||
for _, m := range p.Assigned() {
|
||||
@@ -448,12 +442,8 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
|
||||
in.Gaps = append(in.Gaps, Gap{Input: "wanted manifests", Why: "no build record names the manifest at the wanted fingerprint, so the trunk's is used: " + strings.Join(fromTrunk, ", ")})
|
||||
}
|
||||
in.Hashes = readHashes(r, &in.Gaps)
|
||||
// The controller's tokens stand as a gap while it holds the lease; from the write the seat's keys are every token.
|
||||
var enrolmentsRead bool
|
||||
in.Enrolling, enrolmentsRead = readEnrolling(r, &in.Gaps)
|
||||
if enrolmentsRead && in.LeaseAway {
|
||||
in.Gaps = slices.DeleteFunc(in.Gaps, func(g Gap) bool { return g.Input == "enrolment tokens" })
|
||||
}
|
||||
// The seat mesh-enrolment's keys are every token since the controller's end (issue 1126).
|
||||
in.Enrolling, _ = readEnrolling(r, &in.Gaps)
|
||||
in.NodeLogins = readNodeLogins(r, &in.Gaps)
|
||||
for _, raw := range each(r, ReadResolved, &in.Gaps) {
|
||||
var v resolved
|
||||
|
||||
@@ -337,10 +337,8 @@ func (p *Part) Compose() Result {
|
||||
members = append(members, m.Membership)
|
||||
}
|
||||
seated := holdings.ByMemberships(members...)
|
||||
// From the lease write the memberships are written to the state alone and no longer said on their subject, which
|
||||
// nothing reads any more (novox/hq ADR 0571 clause 10, issue 774).
|
||||
notice, _ := leaseHandedAway(p.Read)
|
||||
notice = !notice
|
||||
// The memberships are written to the state alone and never said on their subject, which nothing reads any more
|
||||
// (novox/hq ADR 0571 clause 10, issue 774; unconditional since the controller's end, issue 1126).
|
||||
for _, key := range sortedKeys(ms) {
|
||||
// The last membership is the state's own (ADR 0571 clause 7), never the assignments stream's, which the bus
|
||||
// module's tools remove at the lease write (research 072 row 33): a last read there would freeze every membership
|
||||
@@ -360,11 +358,6 @@ func (p *Part) Compose() Result {
|
||||
m.Published, now = "not published (tried again at the next event): its own "+StateMemberships+" cannot be written", false
|
||||
} else {
|
||||
res.MembershipsWritten++
|
||||
if notice {
|
||||
if err := p.Publish.PublishMembership(m.Node, m.Module, body); err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("the membership of %s is written, its notice on its subject not said: %v", key, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// One unchanged is written bare once where the state still holds the wrapper written before 2026-10-30.
|
||||
|
||||
@@ -39,7 +39,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
|
||||
// The enrolment users, from the seat mesh-enrolment's keys, and from the lease write through its redeem alone (ADR 0587
|
||||
// §4, §6).
|
||||
out := buscompose.Records{Assigned: map[string][]buscompose.Declared{}, People: map[string][]string{},
|
||||
Interchangeable: map[string]bool{}, Enrolling: slices.Clone(in.Enrolling), LeaseHandedAway: in.LeaseAway,
|
||||
Interchangeable: map[string]bool{}, Enrolling: slices.Clone(in.Enrolling), LeaseHandedAway: true,
|
||||
// And each node's logins (ADR 0583 §1): both while it rotates.
|
||||
NodeLogins: in.NodeLogins}
|
||||
values := func(provider, setting string) []any {
|
||||
|
||||
@@ -95,8 +95,9 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut, dropped
|
||||
cur, _, had := p.Sealed()
|
||||
next := cur
|
||||
next.Follow = followOf(p.SwitchState())
|
||||
// The bus node follows this list alone once the lease is handed (ADR 0571 clause 8, ADR 0582 clause 3).
|
||||
next.LeaseHanded = p.leaseHanded()
|
||||
// The bus node follows this list alone (ADR 0571 clause 8): always since the controller ended by its unassignment,
|
||||
// with no lease written (issue 1126).
|
||||
next.LeaseHanded = true
|
||||
key := ""
|
||||
if in.Loaded != nil {
|
||||
key = in.Loaded.Key
|
||||
|
||||
@@ -132,14 +132,11 @@ func (p *Part) SwitchState() string {
|
||||
// the refusals of Decide. Asking again while an ask is open is refused.
|
||||
func (p *Part) TakeOver(caller, why string) (any, error) { return p.request(ActTakeOver, caller, why) }
|
||||
|
||||
// HandBack is the tool: it asks the operator to switch the bus node back to the controller's list. Once the lease is
|
||||
// handed to the grants part it is answered as gone: the grants part's list is the only one (ADR 0547 §11).
|
||||
// HandBack is the tool: it is answered as gone. The controller ended by its unassignment (issue 1126), so nothing
|
||||
// writes its list and the grants part's list is the only one (ADR 0547 §11).
|
||||
func (p *Part) HandBack(caller, why string) (any, error) {
|
||||
if p.leaseHanded() {
|
||||
return nil, errors.New("gone: the controller's lease is handed to mesh-grants, so the grants part's list is the only " +
|
||||
"one and the user list is not handed back (ADR 0547 §11); lease-hand-back gives the lease back first")
|
||||
}
|
||||
return p.request(ActHandBack, caller, why)
|
||||
return nil, errors.New("gone: the controller ended by its unassignment (issue 1126), so nothing writes its user list; " +
|
||||
"the grants part's list is the only one and the user list is not handed back (ADR 0547 §11)")
|
||||
}
|
||||
|
||||
// nonce is four hex digits that keep two asks of one second apart.
|
||||
@@ -546,13 +543,7 @@ func (p *Part) recordWarrant(ask string) {
|
||||
if p.Call == nil {
|
||||
return
|
||||
}
|
||||
rec := handacts.Recorder{Ask: p.Call.Call, LeaseAway: func() bool {
|
||||
if p.Read == nil {
|
||||
return false
|
||||
}
|
||||
away, _ := leaseHandedAway(p.Read)
|
||||
return away
|
||||
}}
|
||||
rec := handacts.Recorder{Ask: p.Call.Call}
|
||||
taken, err := rec.RecordWarrant(Module, ask)
|
||||
if p.Logf == nil {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user