From 0e3580895da133cd097030bf03db5aada1e2f370 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 05:03:13 +0100 Subject: [PATCH] mesh-conditions: ListBound is asked again on the bus module's created or a node-engine's running, not on a timer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G §515, §663 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../cmd/mesh-conditions/bound.go | 26 ++++++++--- .../cmd/mesh-conditions/events.go | 6 ++- .../cmd/mesh-conditions/keeper.go | 6 +++ .../cmd/mesh-conditions/keeper_test.go | 44 +++++++++++++++++++ 4 files changed, 76 insertions(+), 6 deletions(-) diff --git a/modules/mesh-conditions/cmd/mesh-conditions/bound.go b/modules/mesh-conditions/cmd/mesh-conditions/bound.go index edff8dac4..8bec1b32d 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/bound.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/bound.go @@ -7,7 +7,6 @@ package main import ( "encoding/json" "fmt" - "time" broker "git.novox.be/novox/mesh-contracts/go/seats/meshbroker" ) @@ -48,10 +47,27 @@ func (k *Keeper) takeSource(module string) error { return nil } +// nudgeBound says, without waiting, that the bus module's entries may be read now (ListBound's retry). +func (k *Keeper) nudgeBound() { + k.mu.Lock() + k.init() + c := k.boundRetry + k.mu.Unlock() + select { + case c <- struct{}{}: + default: + } +} + // ListBound reads the entries the bus module holds at the keeper's start: every module with a `finding-events` entry is a -// source. A call that fails is asked again with a growing wait, the bus module being delivered after the keeper at times. +// source. A call that fails is asked again on the next event that says the bus module may answer, its `created` or a +// node-engine's `running` (the bus module being delivered after the keeper at times), never on a timer. func ListBound(k *Keeper, call func(key string, body any) (json.RawMessage, error)) { - for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) { + k.mu.Lock() + k.init() + retry := k.boundRetry + k.mu.Unlock() + for { raw, err := call("seat:"+broker.Seat+"."+broker.VerbList, broker.ListInput{}) var answer broker.ListAnswer if err == nil { @@ -67,7 +83,7 @@ func ListBound(k *Keeper, call func(key string, body any) (json.RawMessage, erro } return } - k.Logf("the entries of the bus module are not read yet (%v); asked again in %s", err, wait) - time.Sleep(wait) + k.Logf("the entries of the bus module are not read yet (%v); asked again when it says created or a node-engine says running", err) + <-retry } } diff --git a/modules/mesh-conditions/cmd/mesh-conditions/events.go b/modules/mesh-conditions/cmd/mesh-conditions/events.go index 4a22ca0b5..0c892aece 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/events.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/events.go @@ -72,13 +72,17 @@ func (k *Keeper) FindingEvent(e stdio.Envelope) error { if _, ok := emitterOf(e.Key, watchdogs.EventMachineBack); ok && strings.HasPrefix(e.Key, watchdogs.Seat+".") { return k.MachinePresence(e.Body, false) } - if _, _, _, ok := engineKey(e.Key); ok { + if event, _, _, ok := engineKey(e.Key); ok { + if event == engine.EventRunning { + k.nudgeBound() + } return k.EngineEvent(e) } if strings.HasPrefix(e.Key, meshhealers.Seat+".attempt-changed.") { return k.AttemptChanged(e.Key, e.Body) } if e.Key == broker.Seat+"."+broker.EventCreated { + k.nudgeBound() return k.BrokerCreated(e.Body) } return nil diff --git a/modules/mesh-conditions/cmd/mesh-conditions/keeper.go b/modules/mesh-conditions/cmd/mesh-conditions/keeper.go index 179e30b70..cc5682b2e 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/keeper.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/keeper.go @@ -164,6 +164,9 @@ type Keeper struct { stopped map[string]bool // created are the modules whose `finding-events` entry the bus module made (bound.go). created map[string]bool + // boundRetry is told, once, when the bus module says `created` or a node-engine says `running`: the events after which + // the entries of the bus module may be read (ListBound). + boundRetry chan struct{} // followed are the finding streams the catalog half declared for each module, by module (catalog.go). followed map[string]string } @@ -179,6 +182,9 @@ func (k *Keeper) init() { k.halves, k.placed, k.stopped = map[string]map[string]catseat.CatalogEntry{}, map[string]bool{}, map[string]bool{} k.views, k.keyNode, k.whys = map[string]*sourceView{}, map[string]map[string]string{}, map[string]string{} } + if k.boundRetry == nil { + k.boundRetry = make(chan struct{}, 1) + } if k.Now == nil { k.Now = time.Now } diff --git a/modules/mesh-conditions/cmd/mesh-conditions/keeper_test.go b/modules/mesh-conditions/cmd/mesh-conditions/keeper_test.go index 2c69ba789..7fdaa964f 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/keeper_test.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/keeper_test.go @@ -723,3 +723,47 @@ func TestTheKeepersOwnFindingIsTheContractsFindingByItsWireForm(t *testing.T) { t.Fatal("the condition stands after its finding was cleared") } } + +// ListBound reads the bus module's entries once and, when the call fails, again on the event that says the bus module may +// answer (its `created`, or a node-engine's `running`), never on a timer (design 47 G §515, §663). +func TestListBoundIsAskedAgainOnTheEventsNotATimer(t *testing.T) { + for _, event := range []string{"mesh-broker.created", "node-engine.running.ace.mesh-conditions"} { + t.Run(event, func(t *testing.T) { + var asked []map[string]any + k := &Keeper{Publish: func(_ string, b any) error { asked = append(asked, b.(map[string]any)); return nil }} + calls := make(chan int) + ended := make(chan struct{}) + go func() { + n := 0 + ListBound(k, func(string, any) (json.RawMessage, error) { + n++ + calls <- n + if n == 1 { + return nil, fmt.Errorf("the bus module is not delivered yet") + } + return mustJSON(t, map[string]any{"entries": []any{map[string]any{"module": "newpart", "template": FindingEventsTemplate}}}), nil + }) + close(ended) + }() + <-calls + select { + case <-ended: + t.Fatal("ListBound ended with no answer") + default: + } + _ = k.FindingEvent(stdio.Envelope{Key: event, Body: []byte(`{}`)}) + select { + case n := <-calls: + if n != 2 { + t.Fatalf("asked %d times", n) + } + case <-time.After(time.Second): + t.Fatal("not asked again on the event") + } + <-ended + if len(asked) != 1 || asked[0]["module"] != "newpart" { + t.Errorf("asked %v, want newpart once", asked) + } + }) + } +}