End every goroutine serve starts before serve returns, so no loop outlives its controller (issue 1945)

The try of the on-change mirror failed with every test passed: goroutines a
served test started outlived its serve and met the next test's globals and its
closed stores. Under -race the served tests show it as a race on conditionsFrom
(watchTheMesh writing it while settlingPending, already running, read it), and
a take-in cut by the stop raised fingerprint-not-composed, a kind with no plain
words, which fails the package after its PASS line.

- serve counts every goroutine it and its parts start (serving_group.go) and
  ends and waits for them before anything else is undone: the lease's watch,
  the pending settler, the status summary, the facts, the mirrors and the new
  declared-packages keeper with its two bucket watches, the declaration sweep,
  the read-call counter, the watches' closers, the have watches, the vault move
  on a report, and the watchdogs, doctor, healers and asker under their own
  group.
- The condition store opens before anything that reads conditionsFrom starts;
  askerFrom is an atomic pointer, read from the keeper's goroutines.
- A done key's take-in cut by the stop raises nothing, and fingerprint-not-composed
  has plain words.
This commit is contained in:
2026-10-25 16:03:46 +01:00
parent b6379b9e86
commit e092f01e54
15 changed files with 159 additions and 48 deletions
+4 -3
View File
@@ -12,6 +12,7 @@ import (
"sort" "sort"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
@@ -27,7 +28,7 @@ import (
) )
// askerFrom is the serving controller's asker; nil in any other process. // askerFrom is the serving controller's asker; nil in any other process.
var askerFrom *asker var askerFrom atomic.Pointer[asker]
// askWithin is how long a verb a warrant chose is given to answer. // askWithin is how long a verb a warrant chose is given to answer.
const askWithin = time.Minute const askWithin = time.Minute
@@ -573,8 +574,8 @@ func startAsking(ctx context.Context, open *stores, server *link.Server, conn *n
fmt.Printf("the operator's answers cannot be heard, so nothing is asked: %v\n", err) fmt.Printf("the operator's answers cannot be heard, so nothing is asked: %v\n", err)
return return
} }
askerFrom = a askerFrom.Store(a)
go a.keep(ctx) goServing(ctx, func() { a.keep(ctx) })
} }
// servingAsker is the serving controller's asker, wired to the mesh: what it reads, publishes, performs and // servingAsker is the serving controller's asker, wired to the mesh: what it reads, publishes, performs and
+1 -1
View File
@@ -49,7 +49,7 @@ func attachTheChain(ctx context.Context, inv *inventory.Inventory, server *link.
// keys laid out before skips them, so this takes seconds; one that fails is tried again beside the serving, loudly. // keys laid out before skips them, so this takes seconds; one that fails is tried again beside the serving, loudly.
if err := inv.LayBuildsOut(ctx, func(format string, args ...any) { fmt.Printf("chain: "+format+"\n", args...) }); err != nil { if err := inv.LayBuildsOut(ctx, func(format string, args ...any) { fmt.Printf("chain: "+format+"\n", args...) }); err != nil {
fmt.Printf("chain: the builds are not laid out under their modules yet, and none is read until they are: %v\n", err) fmt.Printf("chain: the builds are not laid out under their modules yet, and none is read until they are: %v\n", err)
go layBuildsOutSoon(ctx, inv) goServing(ctx, func() { layBuildsOutSoon(ctx, inv) })
} }
// The requests the move of delivery 1 found never handed over are not asked again: the controller asks no build // The requests the move of delivery 1 found never handed over are not asked again: the controller asks no build
// (novox/hq ADR 0501; issue 1851, A10), and the builder builds every fingerprint the indexer writes. // (novox/hq ADR 0501; issue 1851, A10), and the builder builds every fingerprint the indexer writes.
+5 -2
View File
@@ -194,6 +194,9 @@ func followDoneKey(ctx context.Context, open *stores, module string) {
} }
continue continue
} }
if ctx.Err() != nil {
return // stopped: a take-in cut by the stop is no fault of the build, and the next controller takes it in
}
if keeper := conditionsFrom; keeper != nil && !failed { if keeper := conditionsFrom; keeper != nil && !failed {
o.Summary += err.Error() o.Summary += err.Error()
if _, oerr := keeper.Observe(ctx, o); oerr == nil { if _, oerr := keeper.Observe(ctx, o); oerr == nil {
@@ -308,10 +311,10 @@ func watchDoneKeys(ctx context.Context, open *stores) {
fmt.Printf("done keys: the builder's keys are not watched: %v\n", err) fmt.Printf("done keys: the builder's keys are not watched: %v\n", err)
return return
} }
go func() { goServing(ctx, func() {
<-ctx.Done() <-ctx.Done()
closeKeys() closeKeys()
}() })
// And every module once now, after the watch is open so no write falls between the two. // And every module once now, after the watch is open so no write falls between the two.
inBackground(func() { inBackground(func() {
doneKeysAtStart(ctx, catalogedModules(open), func(module string) { takeDoneKeyAtStart(ctx, open, module) }) doneKeysAtStart(ctx, catalogedModules(open), func(module string) { takeDoneKeyAtStart(ctx, open, module) })
+2 -2
View File
@@ -134,7 +134,7 @@ func watchHavesForJudgement(ctx context.Context, open *stores) {
fmt.Printf("compositions: the haves are not watched: %v\n", err) fmt.Printf("compositions: the haves are not watched: %v\n", err)
return return
} }
go func() { goServing(ctx, func() {
defer bus.Close() defer bus.Close()
defer w.Stop() defer w.Stop()
for { for {
@@ -165,5 +165,5 @@ func watchHavesForJudgement(ctx context.Context, open *stores) {
}) })
} }
} }
}() })
} }
@@ -608,8 +608,11 @@ func keepWhatServeSets(t *testing.T) {
t.Setenv(servedVar, "") t.Setenv(servedVar, "")
unheld, status, given, check, follow, keeper, hand := logUnheldChanges, statusFrom, givenEvents, checkEvents, unheld, status, given, check, follow, keeper, hand := logUnheldChanges, statusFrom, givenEvents, checkEvents,
link.Calls.Follow, conditionsFrom, handActConn link.Calls.Follow, conditionsFrom, handActConn
doctor, asker := doctorFrom, askerFrom.Load()
t.Cleanup(func() { t.Cleanup(func() {
logUnheldChanges, statusFrom, givenEvents, checkEvents, link.Calls.Follow, conditionsFrom, handActConn = logUnheldChanges, statusFrom, givenEvents, checkEvents, link.Calls.Follow, conditionsFrom, handActConn =
unheld, status, given, check, follow, keeper, hand unheld, status, given, check, follow, keeper, hand
doctorFrom = doctor
askerFrom.Store(asker)
}) })
} }
+1 -1
View File
@@ -49,7 +49,7 @@ func keeperOn(ctx context.Context, conn *nats.Conn, looks conditions.LookStore)
Say: func(format string, args ...any) { fmt.Fprintf(os.Stderr, format+"\n", args...) }, Say: func(format string, args ...any) { fmt.Fprintf(os.Stderr, format+"\n", args...) },
// What status leads with changed: composed again soon (a nudge outside the serving controller // What status leads with changed: composed again soon (a nudge outside the serving controller
// does nothing). // does nothing).
Changed: func() { statusFrom.nudge(); askerFrom.nudge() }, Changed: func() { statusFrom.nudge(); askerFrom.Load().nudge() },
// Written under the lease, carrying its epoch (novox/hq to-be 45 §6). // Written under the lease, carrying its epoch (novox/hq to-be 45 §6).
Epoch: func() (uint64, error) { return theLease.epoch(context.WithoutCancel(ctx)) }}), nil Epoch: func() (uint64, error) { return theLease.epoch(context.WithoutCancel(ctx)) }}), nil
} }
+2 -2
View File
@@ -169,7 +169,7 @@ func watchHavesForGiven(ctx context.Context, open *stores) {
log.Printf("given values: the haves are not watched: %v", err) log.Printf("given values: the haves are not watched: %v", err)
return return
} }
go func() { goServing(ctx, func() {
defer bus.Close() defer bus.Close()
defer w.Stop() defer w.Stop()
for { for {
@@ -198,5 +198,5 @@ func watchHavesForGiven(ctx context.Context, open *stores) {
inBackground(func() { replaceGiven(ctx, open, node, map[string]time.Time{module: at}) }) inBackground(func() { replaceGiven(ctx, open, node, map[string]time.Time{module: at}) })
} }
} }
}() })
} }
+2 -2
View File
@@ -223,10 +223,10 @@ func watchNewModules(ctx context.Context, open *stores) {
fmt.Printf("new modules: the builder's keys are not watched: %v\n", err) fmt.Printf("new modules: the builder's keys are not watched: %v\n", err)
return return
} }
go func() { goServing(ctx, func() {
<-ctx.Done() <-ctx.Done()
closeKeys() closeKeys()
}() })
} }
// wakeNewModule takes module in when the catalogue does not hold it: never on the watch's own goroutine, which must keep // wakeNewModule takes module in when the catalogue does not hold it: never on the watch's own goroutine, which must keep
+11
View File
@@ -59,6 +59,17 @@ func dataWords(o conditions.Observation) (what, module string) {
} }
var plainWordings = map[string]func(conditions.Observation) words{ var plainWordings = map[string]func(conditions.Observation) words{
// A module's new build whose compositions or grant the controller could not write (composed_keys.go).
kindNotComposed: worded(func(o conditions.Observation) words {
m := "a module"
if p := idPart(o, 0); o.Scope == conditions.ScopeBuild && p != "" {
m = p // the module: raised in the build's scope, its id the module's name
}
return words{Headline: "A new build of " + m + " cannot be delivered",
Explanation: fmt.Sprintf("A new build of %s was made, and the mesh could not prepare it for the machines, so "+
"none can be given it yet. It is tried again every half minute; the details say what fails.", m),
Resolved: "the new build of " + m + " can be delivered"}
}),
// Machines and their node-engine. // Machines and their node-engine.
"silent": worded(func(o conditions.Observation) words { "silent": worded(func(o conditions.Observation) words {
m := machineOr(o, "a machine") m := machineOr(o, "a machine")
+31 -14
View File
@@ -122,7 +122,15 @@ func serve(ctx context.Context) (err error) {
defer theLease.release() defer theLease.release()
ctx, stopActing := context.WithCancel(ctx) ctx, stopActing := context.WithCancel(ctx)
defer stopActing() defer stopActing()
go func() { // Every goroutine started from here is counted, and ended and waited for before serve returns (serving_group.go):
// on the way out by error here, and at the end before anything else is undone.
ctx, running := withServingGroup(ctx)
stopRunning := func() {
stopActing()
running.stop()
}
defer stopRunning()
goServing(ctx, func() {
select { select {
case <-lost: case <-lost:
stopActing() stopActing()
@@ -132,7 +140,7 @@ func serve(ctx context.Context) (err error) {
// asks the controller after this one rather than have a command started and killed with this // asks the controller after this one rather than have a command started and killed with this
// process (novox/hq issue 289). // process (novox/hq issue 289).
handingOver.Store(true) handingOver.Store(true)
}() })
defer func() { defer func() {
select { select {
case <-lost: case <-lost:
@@ -159,6 +167,16 @@ func serve(ctx context.Context) (err error) {
if err := attachTheChain(ctx, inv, server); err != nil { if err := attachTheChain(ctx, inv, server); err != nil {
return err return err
} }
bus, isNATS := server.Bus().(link.OverNATS)
if !isNATS {
return errors.New("the mesh's verbs are served over the bus, and this control plane is not on it")
}
// The condition store, opened before anything that raises or reads a condition starts beside the serving, so none
// of them meets conditionsFrom being written (issue 1945); closed after they all ended.
keeper := openServingConditions(ctx, open, bus)
if keeper != nil {
defer closeConditions(keeper)
}
// The bus's own objects, asserted on every start. **Not created once at genesis**: a stream // The bus's own objects, asserted on every start. **Not created once at genesis**: a stream
// somebody deleted, a mesh raised from a restored backup, or a bus whose data directory was // somebody deleted, a mesh raised from a restored backup, or a bus whose data directory was
@@ -173,7 +191,7 @@ func serve(ctx context.Context) (err error) {
// And the pending assignments settled on a tick of their own (novox/hq ADR 0261): made once their module // And the pending assignments settled on a tick of their own (novox/hq ADR 0261): made once their module
// is registered, ended with why when its build will not register it, raised and cleared as conditions. // is registered, ended with why when its build will not register it, raised and cleared as conditions.
// Never by a read. // Never by a read.
go settlingPending(ctx, open) goServing(ctx, func() { settlingPending(ctx, open) })
// The durations the core's bounds are set from are kept a month (novox/hq to-be 45 Phase 0). // The durations the core's bounds are set from are kept a month (novox/hq to-be 45 Phase 0).
// And what the catalogue decided a build meant. The builder's own result is already handled // And what the catalogue decided a build meant. The builder's own result is already handled
// above; this is the other half — the control plane is the only one of the three that knows // above; this is the other half — the control plane is the only one of the three that knows
@@ -221,10 +239,6 @@ func serve(ctx context.Context) (err error) {
"Those answer the reason when called; everything else is served as usual\n", "Those answer the reason when called; everything else is served as usual\n",
catalogue.ControllerSeatName, strings.Join(behind, ", ")) catalogue.ControllerSeatName, strings.Join(behind, ", "))
} }
bus, isNATS := server.Bus().(link.OverNATS)
if !isNATS {
return errors.New("the mesh's verbs are served over the bus, and this control plane is not on it")
}
// The hand-act log is counted for `status` on this connection rather than a new one a minute; no longer serving, the // The hand-act log is counted for `status` on this connection rather than a new one a minute; no longer serving, the
// connection is closed, and a verb run after in this process dials its own (a served test's leftover failed the // connection is closed, and a verb run after in this process dials its own (a served test's leftover failed the
// next test's `conditions` with "connection closed", issue 1945). // next test's `conditions` with "connection closed", issue 1945).
@@ -244,18 +258,18 @@ func serve(ctx context.Context) (err error) {
return err return err
} }
// Composed now and kept current, before the verb that answers from it is served. // Composed now and kept current, before the verb that answers from it is served.
go statusFrom.keep(ctx) goServing(ctx, func() { statusFrom.keep(ctx) })
// The facts snapshot a merge check is fed (novox/hq to-be 45 §9): kept current while this // The facts snapshot a merge check is fed (novox/hq to-be 45 §9): kept current while this
// controller holds the lease, read by the build seat from the artifact store. // controller holds the lease, read by the build seat from the artifact store.
go exportingFacts(ctx, open, bus.Conn.ConnectedServerVersion) goServing(ctx, func() { exportingFacts(ctx, open, bus.Conn.ConnectedServerVersion) })
// The record mirrors (novox/hq ADR 0289, design 57 F3): the assignments and each registered module's // The record mirrors (novox/hq ADR 0289, design 57 F3): the assignments and each registered module's
// declared config on the bus, written from the store only when a record changed, by the lease holder. // declared config on the bus, written from the store only when a record changed, by the lease holder.
// The provisioner's plan of each handed module, as its machine was last sent it (design 57 P6), in the module's // The provisioner's plan of each handed module, as its machine was last sent it (design 57 P6), in the module's
// assignment. // assignment.
inv.SetProvisioningPlans(inv.SentProvisioningPlans) inv.SetProvisioningPlans(inv.SentProvisioningPlans)
go mirroringRecords(ctx, open, inv, bus.Conn, bus) goServing(ctx, func() { mirroringRecords(ctx, open, inv, bus.Conn, bus) })
// And the bucket of the files that travel by digest, judged against what each machine's latest declaration names (ADR 0486 §3). // And the bucket of the files that travel by digest, judged against what each machine's latest declaration names (ADR 0486 §3).
go sweepingDeclarationFiles(ctx, inv, bus.Conn) goServing(ctx, func() { sweepingDeclarationFiles(ctx, inv, bus.Conn) })
// And the modules the catalogue does not hold yet, registered from the builder's first done key of each, woken by // And the modules the catalogue does not hold yet, registered from the builder's first done key of each, woken by
// the builder's and the indexer's writes (novox/hq ADR 0501 §6, issue 1851): nothing asks a first build. // the builder's and the indexer's writes (novox/hq ADR 0501 §6, issue 1851): nothing asks a first build.
watchNewModules(ctx, open) watchNewModules(ctx, open)
@@ -287,7 +301,7 @@ func serve(ctx context.Context) (err error) {
// signals table, what the bus says about itself, and the self-check. A store that cannot be opened // signals table, what the bus says about itself, and the self-check. A store that cannot be opened
// is said and the controller serves on: status then says the conditions cannot be read, and is // is said and the controller serves on: status then says the conditions cannot be read, and is
// not well — louder than not serving at all, and the push that repairs the bus still runs. // not well — louder than not serving at all, and the push that repairs the bus still runs.
if stopWatching := watchTheMesh(ctx, open, server, bus); stopWatching != nil { if stopWatching := watchTheMesh(ctx, open, server, bus, keeper); stopWatching != nil {
defer stopWatching() defer stopWatching()
} }
stopServing, err := bus.ServeSeatTools(catalogue.ControllerSeatName, handlers, log.New(os.Stdout, "", log.LstdFlags)) stopServing, err := bus.ServeSeatTools(catalogue.ControllerSeatName, handlers, log.New(os.Stdout, "", log.LstdFlags))
@@ -302,7 +316,7 @@ func serve(ctx context.Context) (err error) {
} }
defer stopCLI() defer stopCLI()
// Its read calls write no line each; how many each caller made is said once an hour (novox/hq issue 762). // Its read calls write no line each; how many each caller made is said once an hour (novox/hq issue 762).
go sayReadCalls(ctx) goServing(ctx, func() { sayReadCalls(ctx) })
// And says so on the bus (novox/hq ADR 0197): what it serves, as the NATS services protocol asks. // And says so on the bus (novox/hq ADR 0197): what it serves, as the NATS services protocol asks.
stopAnnouncing, err := bus.Announce(seatAnnouncement(handlers), log.New(os.Stdout, "", log.LstdFlags)) stopAnnouncing, err := bus.Announce(seatAnnouncement(handlers), log.New(os.Stdout, "", log.LstdFlags))
if err != nil { if err != nil {
@@ -310,7 +324,10 @@ func serve(ctx context.Context) (err error) {
} }
defer stopAnnouncing() defer stopAnnouncing()
return server.Serve(ctx) err = server.Serve(ctx)
// Ended and waited for before the verbs stop being served and the globals are put back, not after.
stopRunning()
return err
} }
// OverlayCIDRVar is the range the mesh allocates node addresses from. // OverlayCIDRVar is the range the mesh allocates node addresses from.
+9 -7
View File
@@ -399,14 +399,16 @@ func keepingDeclaredPackages(ctx context.Context, open *stores, api jetstream.Je
} }
k := newPackagesKeeper(read, declaredPackagesFrom, sync, holds) k := newPackagesKeeper(read, declaredPackagesFrom, sync, holds)
for _, bucket := range []string{sdkdelivery.ControllerComposedBucket, broker.AssignmentsBucket} { for _, bucket := range []string{sdkdelivery.ControllerComposedBucket, broker.AssignmentsBucket} {
go wakeOnBucket(ctx, api, bucket, k.poke) goServing(ctx, func() { wakeOnBucket(ctx, api, bucket, k.poke) })
} }
go k.run(ctx, packagesTiming{safety: packagesSafetyEvery, gap: packagesGap}, func(why string) { goServing(ctx, func() {
if why != "" { k.run(ctx, packagesTiming{safety: packagesSafetyEvery, gap: packagesGap}, func(why string) {
fmt.Printf("the %s mirror cannot be kept: %s\n", m.Name, why) if why != "" {
} else { fmt.Printf("the %s mirror cannot be kept: %s\n", m.Name, why)
fmt.Printf("the %s mirror is kept again\n", m.Name) } else {
} fmt.Printf("the %s mirror is kept again\n", m.Name)
}
})
}) })
return k return k
} }
+59
View File
@@ -0,0 +1,59 @@
package main
import (
"context"
"sync"
)
// **Every goroutine the serving controller starts ends before serve returns** (issue 1945). A loop or a watch started
// beside the serving runs until serve's context ends, and serve's stop waits for it before it closes the stores, the
// bus and the condition keeper those loops read: a loop that outlived its serve met the next serve's globals, its
// closed stores and its bus, and in the test process that is the next test (a race on conditionsFrom, a package that
// passed every test and still exited failing). Where it is checked: the tests that run serve whole, under -race.
// servingGroup counts the goroutines one serve started. Once its stop began it starts nothing more: what would start is
// work for a controller that is stopping.
type servingGroup struct {
mu sync.Mutex
stopped bool
wg sync.WaitGroup
}
type servingGroupKey struct{}
// withServingGroup is ctx carrying a new group, which goServing under it counts.
func withServingGroup(ctx context.Context) (context.Context, *servingGroup) {
g := &servingGroup{}
return context.WithValue(ctx, servingGroupKey{}, g), g
}
// goServing runs work beside the caller, counted in the group ctx carries, so serve's stop waits for it. Under a group
// whose stop began it runs nothing; under a context with no group (a part run alone, by a test or a verb) it is a plain
// goroutine, as before.
func goServing(ctx context.Context, work func()) {
g, _ := ctx.Value(servingGroupKey{}).(*servingGroup)
if g == nil {
go work()
return
}
g.mu.Lock()
if g.stopped {
g.mu.Unlock()
return
}
g.wg.Add(1)
g.mu.Unlock()
go func() {
defer g.wg.Done()
work()
}()
}
// stop starts nothing more and waits for what was started; the caller ends their context first. A second stop waits
// for nothing.
func (g *servingGroup) stop() {
g.mu.Lock()
g.stopped = true
g.mu.Unlock()
g.wg.Wait()
}
+2 -2
View File
@@ -73,10 +73,10 @@ func keepAnnouncements(ctx context.Context, js *broker.JetStream, open *stores)
if err != nil { if err != nil {
return fmt.Errorf("following the states announced: %w", err) return fmt.Errorf("following the states announced: %w", err)
} }
go func() { goServing(ctx, func() {
<-ctx.Done() <-ctx.Done()
consuming.Stop() consuming.Stop()
}() })
return nil return nil
} }
+1 -1
View File
@@ -210,7 +210,7 @@ func (l nudgingListener) Heard(ctx context.Context, report link.Report) (bool, e
// And, once the move of the secrets into the vault has started, what is left of this machine's (design 57's V3, // And, once the move of the secrets into the vault has started, what is left of this machine's (design 57's V3,
// issue 676): a machine asleep when it started is moved when it is next heard from. Off the report's path. // issue 676): a machine asleep when it started is moved when it is next heard from. Off the report's path.
if err == nil && l.open != nil && report.Superseded == "" && report.Rekey == nil && report.Node != "" { if err == nil && l.open != nil && report.Superseded == "" && report.Rekey == nil && report.Node != "" {
go moveWhenHeard(context.WithoutCancel(ctx), l.open, moveHeardOn(), report.Node) goServing(ctx, func() { moveWhenHeard(context.WithoutCancel(ctx), l.open, moveHeardOn(), report.Node) })
} }
return news, err return news, err
} }
+26 -11
View File
@@ -543,10 +543,9 @@ func later(a, b time.Time) time.Time {
return b return b
} }
// watchTheMesh opens the condition store and starts the watchdogs, the bus's advisories and the // openServingConditions opens the condition store for the serving controller and makes it conditionsFrom. Nil when the store
// self-check, for the serving controller; the returned function stops them. Nil when the store could // could not be opened, which is said: status then says the conditions cannot be read.
// not be opened, which is said. func openServingConditions(ctx context.Context, open *stores, bus link.OverNATS) *conditions.Keeper {
func watchTheMesh(ctx context.Context, open *stores, server *link.Server, bus link.OverNATS) func() {
keeper, err := keeperOn(ctx, bus.Conn, storeLooks{open.inventory}) keeper, err := keeperOn(ctx, bus.Conn, storeLooks{open.inventory})
if err != nil { if err != nil {
fmt.Printf("the condition store could NOT be opened, so nothing that goes wrong is kept or said, and "+ fmt.Printf("the condition store could NOT be opened, so nothing that goes wrong is kept or said, and "+
@@ -554,6 +553,22 @@ func watchTheMesh(ctx context.Context, open *stores, server *link.Server, bus li
return nil return nil
} }
conditionsFrom = keeper conditionsFrom = keeper
return keeper
}
// closeConditions flushes what the keeper holds and closes it, once nothing that raises a condition runs.
func closeConditions(keeper *conditions.Keeper) {
flushing, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
keeper.Close(flushing)
}
// watchTheMesh starts the watchdogs, the bus's advisories and the self-check over the condition store's keeper, for the
// serving controller; the returned function stops them and waits for them to end. Nil when there is no keeper.
func watchTheMesh(ctx context.Context, open *stores, server *link.Server, bus link.OverNATS, keeper *conditions.Keeper) func() {
if keeper == nil {
return nil
}
logf := func(format string, args ...any) { fmt.Printf(format+"\n", args...) } logf := func(format string, args ...any) { fmt.Printf(format+"\n", args...) }
stopHearing, err := server.HearAdvisories(logf) stopHearing, err := server.HearAdvisories(logf)
if err != nil { if err != nil {
@@ -566,26 +581,26 @@ func watchTheMesh(ctx context.Context, open *stores, server *link.Server, bus li
d := &doctor{open: open, js: server.JetStream(), keeper: keeper, teller: bus, watchdogs: w, host: host} d := &doctor{open: open, js: server.JetStream(), keeper: keeper, teller: bus, watchdogs: w, host: host}
w.doctor = d w.doctor = d
doctorFrom = d doctorFrom = d
// Its own group as well as serve's: its stop waits for what it started.
watching, stop := context.WithCancel(ctx) watching, stop := context.WithCancel(ctx)
go w.keep(watching) watching, running := withServingGroup(watching)
go d.keep(watching) goServing(watching, func() { w.keep(watching) })
goServing(watching, func() { d.keep(watching) })
// And the healers (novox/hq to-be 45 §7): what is open that a registered healer answers, repaired // And the healers (novox/hq to-be 45 §7): what is open that a registered healer answers, repaired
// under the lease and the brake, every act said. // under the lease and the brake, every act said.
healers := newHealing(open, keeper, bus, server.JetStream()) healers := newHealing(open, keeper, bus, server.JetStream())
go healers.keep(watching) goServing(watching, func() { healers.keep(watching) })
// And the asker (novox/hq ADR 0259): what needs the operator and names its answers is asked of them, // And the asker (novox/hq ADR 0259): what needs the operator and names its answers is asked of them,
// and the answer chosen is performed on its warrant. // and the answer chosen is performed on its warrant.
startAsking(watching, open, server, bus.Conn, keeper) startAsking(watching, open, server, bus.Conn, keeper)
go forgettingOldHeals(watching, open.inventory) goServing(watching, func() { forgettingOldHeals(watching, open.inventory) })
fmt.Printf("watching the mesh: %d signal(s) every %s, %d probe(s) every %s; what is wrong is kept in %s "+ fmt.Printf("watching the mesh: %d signal(s) every %s, %d probe(s) every %s; what is wrong is kept in %s "+
"and said as %s events\n", len(watchedRows()), watchEvery, len(runnableProbes()), doctorEvery, "and said as %s events\n", len(watchedRows()), watchEvery, len(runnableProbes()), doctorEvery,
broker.ConditionsBucket, conditions.Seat) broker.ConditionsBucket, conditions.Seat)
return func() { return func() {
stop() stop()
running.stop()
stopHearing() stopHearing()
flushing, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
keeper.Close(flushing)
} }
} }