Merge pull request 'An unread seat list holds what is new, says so, and is read again by the operator's sessions (issue 1966; group fix/1966-seat-list-unread)' (#221) from fix/1966-seat-list-unread into main
This commit was merged in pull request #221.
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -72,7 +74,10 @@ type itBed struct {
|
||||
seatSilent atomic.Bool
|
||||
// seatAsked counts the node's asks of the stand-in seat, answered or refused: the node reached the bus and asked.
|
||||
seatAsked atomic.Int32
|
||||
findings nats.KeyValue
|
||||
// seatAway makes the stand-in answer as the bus answers a subject nothing serves (no responders), as while the seat's
|
||||
// holder re-serves (issue 1966).
|
||||
seatAway atomic.Bool
|
||||
findings nats.KeyValue
|
||||
}
|
||||
|
||||
// aServer is an embedded bus with JetStream and two users: the test's, unrestricted, and the node's, granted exactly what
|
||||
@@ -918,6 +923,10 @@ func (b *itBed) standInSeat() {
|
||||
}
|
||||
_ = json.Unmarshal(m.Data, &q)
|
||||
b.seatAsked.Add(1)
|
||||
if b.seatAway.Load() {
|
||||
_ = m.RespondMsg(&nats.Msg{Header: nats.Header{"Status": []string{"503"}}}) // the bus's no responders
|
||||
return
|
||||
}
|
||||
var result any
|
||||
switch {
|
||||
case b.seatSilent.Load():
|
||||
@@ -1002,7 +1011,14 @@ func TestNatsANodeThatCannotReadTheSeatListActsOnNothingNew(t *testing.T) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
said := strings.Join(b.said, "\n")
|
||||
return strings.Contains(said, selfcompose.WaitsForTheSeatListOwn) && strings.Contains(said, selfcompose.WaitsForTheSeatList)
|
||||
// web's reason names the bootstrapped seats the node holds (issue 1966).
|
||||
var boot []string
|
||||
for _, d := range seatlist.Bootstrapped() {
|
||||
boot = append(boot, d.Seat)
|
||||
}
|
||||
sort.Strings(boot)
|
||||
return strings.Contains(said, selfcompose.WaitsForTheSeatListOwn) &&
|
||||
strings.Contains(said, "web: "+fmt.Sprintf(selfcompose.WaitsForTheSeatListHeld, len(boot), strings.Join(boot, ", ")))
|
||||
})
|
||||
if b.file("web") != "" {
|
||||
t.Fatalf("the node applied web before it read the seat list: %q", b.file("web"))
|
||||
@@ -1047,6 +1063,10 @@ func TestNatsANodeThatCannotReadTheSeatListActsOnNothingNew(t *testing.T) {
|
||||
|
||||
// The first read of the seat list ends the wait whether or not it changed what the reader held: Start returns at once, and
|
||||
// the node waits on the reader's Ready before it says the read is over (ADR 0636 §1).
|
||||
//
|
||||
// Issue 1966, the live case of ace: the seat's holder re-serves while the node starts, so the node's reader is answered as
|
||||
// nothing serves the seat (no responders); once the holder serves again the reader reads the list within the bus's retry
|
||||
// bound, with no event of the seat and no hand act.
|
||||
func TestNatsTheFirstReadOfTheSeatListEndsTheNodesWait(t *testing.T) {
|
||||
clock := &fiveMinutes{}
|
||||
was := seatWaitClock
|
||||
@@ -1054,8 +1074,17 @@ func TestNatsTheFirstReadOfTheSeatListEndsTheNodesWait(t *testing.T) {
|
||||
t.Cleanup(func() { seatWaitClock = was })
|
||||
|
||||
b := newItBedAs(t, nodegrant.For(itNode, "it2.mailer.domain"))
|
||||
b.seatAway.Store(true)
|
||||
stop, _ := h8(t, b, false)
|
||||
defer stop()
|
||||
var st *seatListState
|
||||
within(t, 5*time.Second, "the node started no reader of the seat list", func() bool { st = seatListNow.Load(); return st != nil })
|
||||
asked := b.seatAsked.Load()
|
||||
within(t, 10*time.Second, "the node's reader did not ask the seat while nothing served it", func() bool {
|
||||
now := st.reader.Status()
|
||||
return b.seatAsked.Load() > asked || (!now.Reading && !now.Heard)
|
||||
})
|
||||
b.seatAway.Store(false)
|
||||
within(t, 30*time.Second, "the node did not end its wait at the first read of the seat list", func() bool {
|
||||
st := seatListNow.Load()
|
||||
if st == nil {
|
||||
@@ -1065,4 +1094,7 @@ func TestNatsTheFirstReadOfTheSeatListEndsTheNodesWait(t *testing.T) {
|
||||
waiting, _ := st.waiter.Waiting()
|
||||
return st.reader.Status().Heard && !waiting
|
||||
})
|
||||
if seatListNow.Load() != st {
|
||||
t.Error("the list was read by the reader of a run begun since, not by the one that met no responders")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +182,10 @@ type Scope struct {
|
||||
// decoder (compose.Input.Holds), with the build it would compose it at, so the node says why it holds it (issue 1921,
|
||||
// the composition verb). Nil: said nowhere.
|
||||
Holds func(map[string]compose.Hold)
|
||||
// Held is the modules the node holds before anything of their builds is read, each with why (a node that has not read
|
||||
// the seat list, novox/hq ADR 0636 §1): each is held as it runs, as a module refused alone is, so no take-in or
|
||||
// artifact reference of it can refuse the node (issue 1966). Nil holds none.
|
||||
Held map[string]string
|
||||
}
|
||||
|
||||
// InOwn is, of the modules p places on its node, those whose resources the node's own composition it runs
|
||||
@@ -585,7 +589,10 @@ func (b base) finish(ctx context.Context, r Reader, build func(ctx context.Conte
|
||||
// node never applies a stand-in. Only a refusal that is no single module's (the node's own resources, a value served
|
||||
// elsewhere, a stand-in for the node's own) refuses the node, err.
|
||||
func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (out map[string]engcomp.Composed, skipped []string, held map[string]string, err error) {
|
||||
held = map[string]string{}
|
||||
held = maps.Clone(s.Held)
|
||||
if held == nil {
|
||||
held = map[string]string{}
|
||||
}
|
||||
// A node refused whole says what it held on the way, since the refusal can be of a held module's stub (issue 774,
|
||||
// 2026-10-30 19:30: node-tools held, then "its build produced 0 bundle(s)" with its own refusal unsaid).
|
||||
defer func() {
|
||||
@@ -933,6 +940,10 @@ func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, m
|
||||
}
|
||||
}
|
||||
for _, pm := range p.Modules {
|
||||
if why, held := s.Held[pm.Module]; held && refuse != nil {
|
||||
stub(pm.Module, errors.New(why)) // nothing of its build is read: it keeps what runs (Scope.Held)
|
||||
continue
|
||||
}
|
||||
m, ok := modules[pm.Module]
|
||||
if (!ok || m.Fp2 == "") && s.Unmade != nil && wanted[pm.Module] != "" {
|
||||
s.Unmade(pm.Module, "the indexer's modules hold no build of the new form for it (fingerprint "+m.Fp2+"), so it is composed from the manifest")
|
||||
|
||||
@@ -2,10 +2,12 @@ package link
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/busretry"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
@@ -37,4 +39,11 @@ func TestNatsAToolCheckIsAskedOnTheLink(t *testing.T) {
|
||||
if err == nil || !strings.Contains(err.Error(), "nothing on this machine answers") {
|
||||
t.Fatalf("a tool nothing serves read as %v", err)
|
||||
}
|
||||
// The bus's "no responders" is kept beside ErrNotAsked, so a reader that asks again while its request reached no one
|
||||
// (busretry.Undelivered) sees it (issue 1966: the seat list's first read fell into its holder's re-serve and was never
|
||||
// asked again).
|
||||
_, err = (&natsLink{conn: conn, js: js, node: node}).Ask(ctx, ToolSubject("sensors", "nothing_serves_this", node), nil)
|
||||
if !errors.Is(err, ErrNotAsked) || !errors.Is(err, nats.ErrNoResponders) || !busretry.Undelivered(err) {
|
||||
t.Fatalf("an ask nothing serves read as %v: want ErrNotAsked and the bus's no responders, undelivered", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,17 +52,19 @@ const (
|
||||
// ActManifestReads reads the reader capabilities this engine states (novox/hq ADR 0618 §3).
|
||||
ActManifestReads = nodeengine.VerbManifestReads
|
||||
// ActSeatList reads the seat list this engine holds and what it waits for; ActSeatListAgain makes it read the list now
|
||||
// (novox/hq ADR 0636 §7). The node-engine seat's definition carries both verbs, which answer only the operator and the
|
||||
// healers.
|
||||
ActSeatList = "seat-list"
|
||||
ActSeatListAgain = "seat-list-again"
|
||||
// (novox/hq ADR 0636 §7). The node-engine seat's definition carries both verbs.
|
||||
ActSeatList = nodeengine.VerbSeatList
|
||||
ActSeatListAgain = nodeengine.VerbSeatListAgain
|
||||
)
|
||||
|
||||
// SeatListCallers are who may call the engine's two seat-list verbs: every operator's session and the healers (novox/hq
|
||||
// ADR 0636 §7, the node-engine seat's definition: the operator and the healers).
|
||||
// SeatListCallers are who may call the engine's two seat-list verbs (novox/hq ADR 0636 §7; the node-engine seat's
|
||||
// definition: the operator and the healers): every operator's session and the healers, both verbs alike. A call through
|
||||
// the mesh MCP server, and nox, is proved only as its machine's tool runner (<node>.node-tools, issue 627), so node-tools
|
||||
// stands for an operator's session until a session is proved; without it the verbs answered nobody but the healers, and
|
||||
// no healer calls them (issue 1966).
|
||||
var SeatListCallers = stdio.VerbCallers{
|
||||
ActSeatList: {stdio.AnyPerson, "mesh-healers"},
|
||||
ActSeatListAgain: {stdio.AnyPerson, "mesh-healers"},
|
||||
ActSeatList: {stdio.AnyPerson, "node-tools", "mesh-healers"},
|
||||
ActSeatListAgain: {stdio.AnyPerson, "node-tools", "mesh-healers"},
|
||||
}
|
||||
|
||||
// The states of an act: asked, done and refused are also the seat's events.
|
||||
|
||||
@@ -301,8 +301,12 @@ func TestALinkThatHearsHandOversIsGivenTheQueues(t *testing.T) {
|
||||
{"packages", EngineToolSubject("packages", "n1") + " " + `{"write":true}`, `"key":null,"wrote":true`},
|
||||
{"packages", EngineToolSubject("packages", "n1") + " " + `{"write":"true"}`, `"note":"the engine has written no key for this node yet`},
|
||||
{"manifest-reads", EngineToolSubject("manifest-reads", "n1"), `"manifest-reads":["form:a","seat:b"],"node":"n1","stated-at":"2026-11-04T10:00:00Z","v":1`},
|
||||
// ADR 0636 §7: the seat-list verbs answer every operator's session and the healers, no other module.
|
||||
// ADR 0636 §7: both seat-list verbs answer every operator's session, proved as a person or, through the mesh MCP
|
||||
// server, as its machine's tool runner (issue 627, issue 1966), and the healers, no other module.
|
||||
{"seat-list", "mesh.seat.node-engine.call.seat-list.n1.person~jochen", `"held":true`},
|
||||
{"seat-list", "mesh.seat.node-engine.call.seat-list.n1.shanks~node-tools", `"held":true`},
|
||||
{"seat-list", "mesh.seat.node-engine.call.seat-list.n1.ace~messenger", `not on the holder's caller list for seat-list`},
|
||||
{"seat-list-again", "mesh.seat.node-engine.call.seat-list-again.n1.shanks~node-tools", `"read":true`},
|
||||
{"seat-list-again", "mesh.seat.node-engine.call.seat-list-again.n1.person~jochen", `"read":true`},
|
||||
{"seat-list-again", "mesh.seat.node-engine.call.seat-list-again.n1.ace~mesh-healers", `"read":true`},
|
||||
{"seat-list-again", "mesh.seat.node-engine.call.seat-list-again.n1.ace~messenger", `not on the holder's caller list for seat-list-again`},
|
||||
|
||||
@@ -324,8 +324,10 @@ func (l *natsLink) Ask(ctx context.Context, subject string, body []byte) ([]byte
|
||||
case err == nil:
|
||||
return reply, nil
|
||||
case errors.Is(err, nats.ErrNoResponders):
|
||||
// The server says nobody subscribes the subject: delivered to no one.
|
||||
return nil, fmt.Errorf("%w: nothing on this machine answers %s", ErrNotAsked, subject)
|
||||
// The server says nobody subscribes the subject: delivered to no one. The bus's own error stays beside ErrNotAsked,
|
||||
// so a caller that asks again while a request reached no one (mesh-sdk busretry.Undelivered, as the seat list's
|
||||
// reader does while its holder re-serves; issue 1966) sees it.
|
||||
return nil, fmt.Errorf("%w: nothing on this machine answers %s (%w)", ErrNotAsked, subject, err)
|
||||
}
|
||||
if refused := l.refusal(subject, before); refused != "" {
|
||||
return nil, fmt.Errorf("%s: %v%s", subject, err, refused)
|
||||
|
||||
@@ -43,6 +43,7 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
"github.com/novox/mesh-host/internal/compose"
|
||||
"github.com/novox/mesh-host/internal/compose/frombus"
|
||||
"github.com/novox/mesh-host/internal/compose/resolution"
|
||||
@@ -997,6 +998,10 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
|
||||
c.mu.Lock()
|
||||
c.dataScope = frombus.Scope{Node: c.env.Node, KeptAt: kept, InOwn: inOwn} // the data items are worked out as composed
|
||||
c.mu.Unlock()
|
||||
// A node that has not read the seat list takes nothing of a module whose seats are not all bootstrapped from its build
|
||||
// (ADR 0636 §1): the module is held as it runs before anything of its build is read, so no take-in or artifact
|
||||
// reference of it (the artifact store's seat is not bootstrapped) can refuse the pass (issue 1966).
|
||||
scope.Held = c.heldUntilListed(ctx, r, own)
|
||||
missed := c.missed.Load()
|
||||
compositions, skipped, held, err := frombus.Compose(ctx, r, c.env.Mesh, scope, fps)
|
||||
err = c.missedIn(missed, err)
|
||||
@@ -1021,6 +1026,10 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
|
||||
}
|
||||
if err != nil {
|
||||
why := fmt.Sprintf("%v", err)
|
||||
if line, unread := c.seatListUnread(); unread {
|
||||
// A refusal while the list is unread may be of a seat the node cannot yet know: said first (issue 1966).
|
||||
why = line + "; " + why
|
||||
}
|
||||
// A bus that did not answer past every try is no answer: composed again after a bound (issue 1126, 2026-10-28),
|
||||
// and the refusal says when (issue 774, 2026-10-31). A refusal is final until an input moves.
|
||||
if BusFailed(err) {
|
||||
@@ -1869,8 +1878,54 @@ func mapValues(m map[string]compositions.Composed) []compositions.Composed {
|
||||
return out
|
||||
}
|
||||
|
||||
// WaitsForTheSeatList is why a module is held while the node has not read the seat list.
|
||||
const WaitsForTheSeatList = "the node has not read the seat list, so it composes nothing new of this module and keeps what runs of it (novox/hq ADR 0636 §1)"
|
||||
// WaitsForTheSeatList is why a module is held while the node has not read the seat list, said by a gate that names no
|
||||
// seat it holds.
|
||||
const WaitsForTheSeatList = "the node has not read the seat list" + composesNothingNewOfIt
|
||||
|
||||
// WaitsForTheSeatListHeld is why a module is held while the node holds only its bootstrapped seat list (novox/hq ADR 0636
|
||||
// §2): the number and the names of the seats it holds, then what it does of the module (issue 1966).
|
||||
const WaitsForTheSeatListHeld = "the node holds only its bootstrapped seat list (%d seats: %s) and has not read the seat " +
|
||||
seatlist.Seat
|
||||
|
||||
const composesNothingNewOfIt = ", so it composes nothing new of this module and keeps what runs of it (novox/hq ADR 0636 §1)"
|
||||
|
||||
// seatListUnread is, while the node has not read the seat list, the line that says so, naming the bootstrapped seats it
|
||||
// holds when its gate says them (the reader's Status); false once the list was read, or with no gate.
|
||||
func (c *Composer) seatListUnread() (string, bool) {
|
||||
if c.env.SeatList == nil || c.env.SeatList.MayAct() {
|
||||
return "", false
|
||||
}
|
||||
says, ok := c.env.SeatList.(interface{ Status() seatlist.Status })
|
||||
if !ok {
|
||||
return strings.TrimSuffix(WaitsForTheSeatList, composesNothingNewOfIt), true
|
||||
}
|
||||
var seats []string
|
||||
for _, h := range says.Status().Held {
|
||||
seats = append(seats, h.Seat)
|
||||
}
|
||||
return fmt.Sprintf(WaitsForTheSeatListHeld, len(seats), strings.Join(seats, ", ")), true
|
||||
}
|
||||
|
||||
// heldUntilListed is, while the node has not read the seat list, every module placed on it whose seats are not all
|
||||
// bootstrapped, each with why: the pass holds them as they run before reading anything of their builds (frombus.Scope.Held).
|
||||
// A holder of the seat mesh-catalog is composed whatever the list says, since the list is read from it (holdUntilListed,
|
||||
// holderPart). Nil once the list was read.
|
||||
func (c *Composer) heldUntilListed(ctx context.Context, r reader, own meshmoduleassigner.Assignments) map[string]string {
|
||||
line, unread := c.seatListUnread()
|
||||
if !unread {
|
||||
return nil
|
||||
}
|
||||
holders := c.claimants(ctx, r, meshcatalog.Seat)
|
||||
held := map[string]string{}
|
||||
for _, pm := range own.Modules {
|
||||
if pm.Module == "" || pm.Module == agent.NodeModule || slices.Contains(holders, pm.Module) ||
|
||||
c.env.SeatList.MayAct(c.seatsOf(ctx, r, pm.Module)...) {
|
||||
continue
|
||||
}
|
||||
held[pm.Module] = line + composesNothingNewOfIt
|
||||
}
|
||||
return held
|
||||
}
|
||||
|
||||
// WaitsForTheSeatListOwn is why the node's own composition is held: it folds modules and accounts whose seats the node
|
||||
// cannot judge before the list was read, so only the last applied one runs (ADR 0636 §1).
|
||||
@@ -1906,7 +1961,8 @@ func (c *Composer) holdUntilListed(ctx context.Context, r reader, compositions m
|
||||
if dig, err := engcomp.DigestOf(comp); err == nil && ran[comp.Module+"/"+dig] {
|
||||
continue
|
||||
}
|
||||
module, why := comp.Module, WaitsForTheSeatList
|
||||
line, _ := c.seatListUnread()
|
||||
module, why := comp.Module, line+composesNothingNewOfIt
|
||||
if module == "" { // the node's own: no seat to judge, so while the list is unread it waits whole
|
||||
if c.env.SeatList.MayAct() {
|
||||
continue
|
||||
|
||||
@@ -32,6 +32,8 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/novox/mesh-host/internal/compose"
|
||||
"github.com/novox/mesh-host/internal/compose/frombus"
|
||||
@@ -1842,6 +1844,20 @@ type gate struct{ open atomic.Bool }
|
||||
|
||||
func (g *gate) MayAct(...string) bool { return g.open.Load() }
|
||||
|
||||
// namedGate is a gate that says the seats it holds, as the reader of the seat list does (seatlist.Reader.Status).
|
||||
type namedGate struct {
|
||||
*gate
|
||||
seats []string
|
||||
}
|
||||
|
||||
func (g *namedGate) Status() seatlist.Status {
|
||||
st := seatlist.Status{Heard: g.open.Load()}
|
||||
for _, s := range g.seats {
|
||||
st.Held = append(st.Held, seatlist.Held{Seat: s, Source: seatlist.SourceBootstrap})
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
// ADR 0636 §1: a node that has not read the seat list composes nothing new of a module whose seats are not bootstrapped,
|
||||
// says it holds the module (so its have says why) and hands it at the read, which the reader says as an event; what
|
||||
// already runs with the same composition is not held.
|
||||
@@ -1852,22 +1868,38 @@ func TestAnUnreadSeatListHoldsWhatIsNewAndHandsItAtTheRead(t *testing.T) {
|
||||
man, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "lemurs", Manifest: lemurs})
|
||||
key, _ := meshcatalogclient.ManifestsKey(digest)
|
||||
b.kv[buckets.Manifests+"/"+key] = man
|
||||
// Issue 1966: avahi's archive is in the artifact store, whose seat the bootstrap does not hold, so before the read the
|
||||
// node reaches no store; avahi is held for the list before its build is read, and the pass is not refused for it.
|
||||
avahi := `{"module":"avahi","version":"1","capabilities":["systemd"],"resources":[{"id":"tools","type":"archive",` +
|
||||
`"path":"/opt/avahi","source":"` + catalogue.ArtifactStoreScheme + `avahi/tools/blobs/sha256:` + strings.Repeat("d", 64) + `"}]}`
|
||||
avahiDigest := meshcatalogclient.ManifestDigest([]byte(avahi))
|
||||
avahiMan, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "avahi", Manifest: avahi})
|
||||
avahiKey, _ := meshcatalogclient.ManifestsKey(avahiDigest)
|
||||
b.kv[buckets.Manifests+"/"+avahiKey] = avahiMan
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
env := b.env()
|
||||
g := &gate{}
|
||||
g := &namedGate{gate: &gate{}, seats: []string{"mesh-catalog", "node-engine"}}
|
||||
changed := make(chan struct{}, 1)
|
||||
env.SeatList, env.SeatListChanged = g, changed
|
||||
var mu sync.Mutex
|
||||
heldBy := map[string]string{}
|
||||
env.Refused = func(_ string, held map[string]string) { mu.Lock(); heldBy = held; mu.Unlock() }
|
||||
var refusedWhole []string
|
||||
env.Refused = func(why string, held map[string]string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
heldBy = held
|
||||
if why != "" {
|
||||
refusedWhole = append(refusedWhole, why)
|
||||
}
|
||||
}
|
||||
c := New(env)
|
||||
out, err := c.Watch(ctx, "n1.>")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
place := assignedValues(t, meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}}})
|
||||
Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}, {Module: "avahi"}}})
|
||||
have, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}})
|
||||
mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
|
||||
want, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp, Self: true})
|
||||
@@ -1875,6 +1907,8 @@ func TestAnUnreadSeatListHoldsWhatIsNewAndHandsItAtTheRead(t *testing.T) {
|
||||
b.putAssigned(t, place)
|
||||
b.put(t, buckets.Have, "n1.node", have)
|
||||
b.put(t, buckets.Modules, "lemurs", mod)
|
||||
avahiMod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: avahiDigest, Repository: "novox/x", Commit: "c"})
|
||||
b.put(t, buckets.Modules, "avahi", avahiMod)
|
||||
b.put(t, buckets.Wants, "want.n1.lemurs", want)
|
||||
b.put(t, buckets.Wants, "want.n1.node", nodeWant)
|
||||
for _, bucket := range []string{buckets.Wants, buckets.Assignments, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} {
|
||||
@@ -1888,11 +1922,32 @@ func TestAnUnreadSeatListHoldsWhatIsNewAndHandsItAtTheRead(t *testing.T) {
|
||||
}
|
||||
handed[u.Key] = true
|
||||
}
|
||||
// The reason names the bootstrapped list the node holds (issue 1966: ace's modules said "reaches no artifact store").
|
||||
waits := fmt.Sprintf(WaitsForTheSeatListHeld, 2, "mesh-catalog, node-engine") + composesNothingNewOfIt
|
||||
mu.Lock()
|
||||
why := heldBy["lemurs"]
|
||||
why, avahiWhy, whole := heldBy["lemurs"], heldBy["avahi"], slices.Clone(refusedWhole)
|
||||
mu.Unlock()
|
||||
if why != WaitsForTheSeatList {
|
||||
t.Errorf("lemurs is held with %q, want %q", why, WaitsForTheSeatList)
|
||||
if why != waits || avahiWhy != waits || len(whole) != 0 {
|
||||
t.Errorf("lemurs is held with %q, avahi with %q, the pass refused %q; want both %q and no refusal", why, avahiWhy, whole, waits)
|
||||
}
|
||||
// A pass refused whole while the list is unread says the list first (issue 1966).
|
||||
refusing := b.env()
|
||||
refusing.SeatList = g
|
||||
refusing.Get = func(context.Context, string, string) ([]byte, error) {
|
||||
return nil, errors.New("nats says ${dir:mesh-state}, and nats declares no directory")
|
||||
}
|
||||
var refusal string
|
||||
refusing.Refused = func(w string, _ map[string]string) { refusal = w }
|
||||
c2 := New(refusing)
|
||||
c.mu.Lock()
|
||||
maps.Copy(c2.wants, c.wants)
|
||||
maps.Copy(c2.assignments, c.assignments)
|
||||
maps.Copy(c2.records, c.records)
|
||||
maps.Copy(c2.modules, c.modules)
|
||||
c.mu.Unlock()
|
||||
c2.compose(ctx)
|
||||
if line := fmt.Sprintf(WaitsForTheSeatListHeld, 2, "mesh-catalog, node-engine") + "; "; !strings.HasPrefix(refusal, line) {
|
||||
t.Errorf("a pass refused while the list is unread says %q, want it to begin %q", refusal, line)
|
||||
}
|
||||
for k := range handed {
|
||||
if strings.Contains(k, "lemurs") {
|
||||
|
||||
Reference in New Issue
Block a user