The 2026-10-26 replacements showed the lease was given back, but last: the shutdown took 10 s, the gate had closed at half the key's age, and the release recorded the epoch released only through the gate, so the next holder found it expired and S12 said it, urgent, for an hour. Now the stop signal gives the key back first and records the epoch released whenever the delete succeeded; a candidate reads the key's delete marker on the bus to tell a handover from an expiry; and a real takeover (lost or expired) is raised and cleared once by the controller that took it, instead of an hour of S12 observation. Research 072 piece moved: the lease's takeover read from its own key on the bus and said as an event (03-domains, mesh-controller row; 04-findings 2, the watchdogs' age reading). Quick fix weighed: drop the gate check from the release record only; it leaves the release last in the shutdown and the hour-long alarm for a real takeover.
586 lines
27 KiB
Go
586 lines
27 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// sending it, and holding the link that carries it.
|
|
//
|
|
// Split out of main.go, which had reached 2,769 lines because appending was always the
|
|
// cheapest next step. That is how novox/hq ADR 0001 records `hal/sdk` reaching 34,636:
|
|
// nothing in it was wrong, and no one edit was the one that should have been a new file.
|
|
|
|
// serve is the control plane running: one connection to the broker, one queue, one consumer.
|
|
// connectLink opens the controller's link over whichever bus this process is on (design 25: one
|
|
// variable moves it). The streams and this controller's consumers are raised first on the new bus,
|
|
// so nothing served here finds them missing.
|
|
func connectLink(ctx context.Context, inv *inventory.Inventory, enroller link.Enroller, listener link.Listener) (*link.Server, error) {
|
|
busAddress, err := broker.BusAddress()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if inv != nil {
|
|
if err := raiseTheBus(ctx, inv, busAddress); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
js, err := broker.Dial(busAddress)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the mesh is on the bus at %s and this control plane cannot reach it: %w",
|
|
broker.BareAddress(busAddress), err)
|
|
}
|
|
return link.ConnectNats(js, enroller, listener), nil
|
|
}
|
|
|
|
func serve(ctx context.Context) (err error) {
|
|
// Nothing this process does, or starts, is the operator at the terminal (novox/hq ADR 0266).
|
|
markServed()
|
|
// The one process whose log is read over time, so the one that says each change to a node's
|
|
// unmet seat dependencies once (novox/hq ADR 0207).
|
|
logUnheldChanges = true
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer open.Close()
|
|
inv := open.inventory
|
|
// Lent to every reader in this process that holds only the inventory, so none opens pools of its own per call
|
|
// (issues 1945 and 1277: about 120 take-ins at once filled novox's store at 16:08 on 2026-10-25).
|
|
defer shareStores(open)()
|
|
// **What this build reads of the store's schema, on record** (novox/hq issue 352): the highest migration
|
|
// it carries, by its version, so a gate that would put this build back later knows it reads the store
|
|
// as it is then. A build that does not know its version records nothing, and is never put back.
|
|
if build := runningBuild(); build != "" {
|
|
if reach, err := schemaReach(); err != nil {
|
|
fmt.Printf("what this build reads of the store's schema is not recorded: %v\n", err)
|
|
} else if err := inv.RecordSchemaReach(ctx, build, reach); err != nil {
|
|
fmt.Printf("what this build (%s) reads of the store's schema is not recorded: %v\n", build, err)
|
|
} else {
|
|
fmt.Printf("this build (%s) reads the store's schema up to migration %04d; recorded\n", build, reach)
|
|
}
|
|
} else {
|
|
fmt.Printf("this process was not told which build it is (%s), so what it reads of the store's schema is not "+
|
|
"recorded, and a gate will never put it back\n", RunningBuildVar)
|
|
}
|
|
|
|
// The controller's own identity store, the one its readers share (closed with open), not a second pool.
|
|
ident, err := open.Identity(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Established at start rather than on first use. A control plane that cannot sign is one
|
|
// whose declarations every node correctly refuses, and that should be a startup failure
|
|
// rather than something discovered at the first declaration.
|
|
key, err := ident.Establish(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("signing as %s\n", key.Fingerprint()[:16])
|
|
|
|
// Where the broker is and what to expect there, so a node can be told how to come back
|
|
// without a person and a new token.
|
|
known, err := broker.FromEnvironment()
|
|
if err != nil && !errors.Is(err, broker.ErrNotConfigured) {
|
|
return err
|
|
}
|
|
if errors.Is(err, broker.ErrNotConfigured) {
|
|
fmt.Printf("no broker address configured, so enrolled nodes will not be told how to "+
|
|
"reconnect. Set %s and %s.\n", broker.AddressVar, broker.CertificateVar)
|
|
}
|
|
|
|
// **Which bus this mesh is on, read once** (novox/hq ADR 0116 step 5). Both clients ship; both
|
|
// being live is refused, because a mesh half on each is one where a declaration goes out on one
|
|
// and the report comes back on the other, and every component logs success while it happens.
|
|
|
|
// **The lease, before anything that acts** (novox/hq to-be 45 §6): asserting the bus's objects is the
|
|
// controller's to do, and so is everything after. A controller starting while another holds it waits
|
|
// here, said; one that loses it stops: every act's gate closes at once, and ctx ends so the process
|
|
// exits and is started again as a candidate.
|
|
busAddress, err := broker.BusAddress()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lost, err := theLease.serveUnderTheLease(ctx, inv, busAddress)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Given back before the store closes, so the epoch is recorded as given back rather than found
|
|
// expired by the next holder.
|
|
defer theLease.release()
|
|
ctx, stopActing := context.WithCancel(ctx)
|
|
defer stopActing()
|
|
// 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 {
|
|
case <-lost:
|
|
stopActing()
|
|
case <-ctx.Done():
|
|
}
|
|
// Stopping, whichever way: a verb arriving from here is refused as a handover, so its caller
|
|
// asks the controller after this one rather than have a command started and killed with this
|
|
// process (novox/hq issue 289).
|
|
handingOver.Store(true)
|
|
})
|
|
defer func() {
|
|
select {
|
|
case <-lost:
|
|
if err == nil {
|
|
err = errors.New("the controller lease was lost; this controller stopped acting and exits, to " +
|
|
"be started again as a candidate")
|
|
}
|
|
default:
|
|
}
|
|
}()
|
|
|
|
work := link.Enrolment{Inventory: inv, Identity: ident, Broker: known,
|
|
OnNATS: true}
|
|
// `status` from a summary kept current here (novox/hq to-be 45 Phase 0): a machine saying
|
|
// something new is one thing that moves it, so the listener nudges it.
|
|
statusFrom = newStatusSummary(composeStatus(open))
|
|
server, err := connectLink(ctx, inv, work, nudgingListener{Enrolment: work, summary: statusFrom, open: open})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer server.Close()
|
|
// The chain on the bus, and the move of what the tables still hold into it (novox/hq ADR 0408 §8): before anything
|
|
// reads builds, rollouts or assignments.
|
|
if err := attachTheChain(ctx, inv, server); err != nil {
|
|
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)
|
|
defer stopRunning() // on an error return, the goroutines end before the keeper closes
|
|
// The takeover of the lease said as it happened, raised and cleared (novox/hq issue 906).
|
|
theLease.watchTakeovers(ctx, inv, keeper)
|
|
defer theLease.watchTakeovers(ctx, nil, nil) // the keeper closes after this: nothing said into it then
|
|
}
|
|
|
|
// 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
|
|
// replaced all have records and no objects — and a node whose consumer is missing hears nothing
|
|
// while everything else about it looks correct.
|
|
// Open plans move on events (novox/hq ADR 0406 §5, issue 974): a tier is asked on the `built` and the send of the
|
|
// tier before it, a walk ends on its machines' reports, a silent machine is a key that expired, and a plan left
|
|
// by a replaced controller resumes from the first look, which this asks for. No timer looks every so often.
|
|
// **Its own grants on the bus before it writes** (novox/hq issue 1800): a bucket this controller newly writes is
|
|
// granted before the first write, not after its gate.
|
|
startWithOwnGrants(ctx, ownGrantsOf(open), func() {})
|
|
// 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.
|
|
// Never by a read.
|
|
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).
|
|
// 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
|
|
// which machines run the thing, so it is the one that acts (novox/hq ADR 0072).
|
|
if err := server.Follows(following{open: open}); err != nil {
|
|
return err
|
|
}
|
|
// And the merges mesh-delivery made, kept a week with the ask each was made on: what registers a module that
|
|
// moved between repositories on the operator's Approve (novox/hq ADR 0420 §7, issue 1025).
|
|
if err := server.KeepsDeliveryMerges(keepingDeliveryMerges{js: server.JetStream, catalog: open.inventory.Catalogued}); err != nil {
|
|
return err
|
|
}
|
|
// No timer reads the merges back (novox/hq ADR 0407 decision 3, issue 973): the forge's event and the durable
|
|
// consumer on the bus keep every merge, and one the controller cannot record yet is held and handed over again.
|
|
// And a catalogue that has just started, asking for what it missed. The same type answers
|
|
// both: what a build meant and what the builds were are two questions about one record.
|
|
if err := server.Answers(following{open: open}); err != nil {
|
|
return err
|
|
}
|
|
// And what providers say about consumers they keep failing, kept for `status` (novox/hq ADR
|
|
// 0224): a provider's journal must not be the only place that says so.
|
|
if err := server.Watches(standings{keeper: func() *conditions.Keeper { return conditionsFrom }}); err != nil {
|
|
return err
|
|
}
|
|
// And what each machine says of what it runs between its reports, kept and raised from (novox/hq ADR
|
|
// 0240): the gate, `node show` and the conditions read it.
|
|
server.Hears(moduleHealth{inv: inv, keeper: func() *conditions.Keeper { return conditionsFrom }})
|
|
// And what they say about consumers the mesh stopped asking for: one waiting for a person is an
|
|
// urgent condition, and an act asked of a provider some other way is recorded by hand (ADR 0230).
|
|
if err := server.KeepsRetirements(retirements{keeper: func() *conditions.Keeper { return conditionsFrom },
|
|
record: recordHandActOnTheBus}); err != nil {
|
|
return err
|
|
}
|
|
|
|
// And the mesh's own verbs, as the seat this control plane holds (novox/hq ADR 0154). Served
|
|
// from the store's row, so what the seat declares is what is answered.
|
|
handlers, behind, err := seatToolHandlers()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(behind) > 0 {
|
|
// Said once, loudly, and then served anyway (novox/hq ADR 0185): the mesh keeps answering
|
|
// while whatever put an older control plane here is undone.
|
|
fmt.Printf("this control plane is behind the %s row: it cannot run %s. "+
|
|
"Those answer the reason when called; everything else is served as usual\n",
|
|
catalogue.ControllerSeatName, strings.Join(behind, ", "))
|
|
}
|
|
// 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
|
|
// next test's `conditions` with "connection closed", issue 1945).
|
|
handActConn = bus.Conn
|
|
defer func() { handActConn = nil }()
|
|
defer stopRunning() // on an error return, the goroutines end before handActConn is put back
|
|
// And everything else this controller does on the bus for a moment (novox/hq issue 327).
|
|
servingBus.Store(server.JetStream())
|
|
// No longer serving: nothing is lent, and dead-letters says it is not read here (novox/hq issue 330).
|
|
defer servingBus.Store(nil)
|
|
defer stopRunning() // on an error return, the goroutines end before the bus is taken back
|
|
// And says when it replaced a value given by hand (novox/hq ADR 0228).
|
|
givenEvents = bus
|
|
// And a pull request's merge check, asked when the forge announces its head and said when judged
|
|
// (novox/hq to-be 45 §9).
|
|
checkEvents = bus
|
|
// And every walk this controller keeps in a new state, for the delivery it walks (novox/hq ADR 0239).
|
|
if err := server.Checks(following{open: open}); err != nil {
|
|
return err
|
|
}
|
|
// Composed now and kept current, before the verb that answers from it is served.
|
|
goServing(ctx, func() { statusFrom.keep(ctx) })
|
|
// 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.
|
|
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
|
|
// 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
|
|
// assignment.
|
|
inv.SetProvisioningPlans(inv.SentProvisioningPlans)
|
|
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).
|
|
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
|
|
// the builder's and the indexer's writes (novox/hq ADR 0501 §6, issue 1851): nothing asks a first build.
|
|
watchNewModules(ctx, open)
|
|
// And a value given by hand replaced when its module's have first runs healthy (novox/hq ADR 0228, ADR 0539 §9).
|
|
watchHavesForGiven(ctx, open)
|
|
// And says what it keeps, and keeps what every module says it keeps (novox/hq ADR 0495, issue 1790): a changed word
|
|
// sends the machine holding the bus its new user list.
|
|
if err := announceOwnStates(ctx, server.JetStream()); err != nil {
|
|
fmt.Printf("states: %v\n", err)
|
|
}
|
|
if err := keepAnnouncements(ctx, server.JetStream(), open); err != nil {
|
|
fmt.Printf("states: the states the modules announce are not kept: %v\n", err)
|
|
}
|
|
// Every call carries the lease's epoch, and its record is written only under the lease (novox/hq
|
|
// to-be 45 §6).
|
|
link.Calls.UnderLease(func() (uint64, error) { return theLease.epoch(ctx) })
|
|
// A call that outlasts its caller's patience is followed by `calls` (novox/hq issue 265).
|
|
link.Calls.Follow = catalogue.ControllerSeatName + ".calls"
|
|
// And every call is kept on the bus, so a restart of this process keeps what came of each
|
|
// (novox/hq to-be 45 §6). A bus without the bucket is said and served from memory, as before:
|
|
// answering no calls at all would be worse than answering them without the record.
|
|
said := log.New(os.Stdout, "", log.LstdFlags)
|
|
if keeper, err := link.CallsOnTheBus(ctx, bus.Conn); err != nil {
|
|
fmt.Printf("calls are kept in memory only, and lost when this controller stops: %v\n", err)
|
|
} else if err := link.Calls.Durably(ctx, keeper, instance, said); err != nil {
|
|
fmt.Printf("calls are kept on the bus from now on; the ones kept before could not be read: %v\n", err)
|
|
}
|
|
// What is wrong, kept and said (novox/hq to-be 45 §2): the condition store, the watchdogs of the
|
|
// 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
|
|
// not well — louder than not serving at all, and the push that repairs the bus still runs.
|
|
if stopWatching := watchTheMesh(ctx, open, server, bus, keeper); stopWatching != nil {
|
|
defer stopWatching()
|
|
}
|
|
stopServing, err := bus.ServeSeatTools(catalogue.ControllerSeatName, handlers, log.New(os.Stdout, "", log.LstdFlags))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer stopServing()
|
|
// And the operator's command on every node, asked through each node's engine (novox/hq ADR 0272).
|
|
stopCLI, err := bus.ServeCLI(answerMeshCLI(open.inventory), log.New(os.Stdout, "", log.LstdFlags))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer stopCLI()
|
|
// Its read calls write no line each; how many each caller made is said once an hour (novox/hq issue 762).
|
|
goServing(ctx, func() { sayReadCalls(ctx) })
|
|
// 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))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer stopAnnouncing()
|
|
|
|
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.
|
|
const OverlayCIDRVar = "MESH_OVERLAY_CIDR"
|
|
|
|
// errFilesNotPut marks a send that stopped because the files it names by digest could not be put in the bucket.
|
|
var errFilesNotPut = errors.New("the files it names by digest are not in the bucket")
|
|
|
|
// brokerBehind is the machine holding the bus — the one whose declaration carries the user list —
|
|
// and, when it is not among the machines named, whether the user list it would be sent now differs
|
|
// from the one it was last sent (novox/hq issue 249).
|
|
//
|
|
// **The user list alone, not the whole declaration.** Read from the whole declaration, any change
|
|
// pending on that machine — an upgrade its policy records rather than rolls out — went with every
|
|
// send anywhere, and a module running there always put it in its first wave. A digest of the list
|
|
// last sent is kept for this (ADR 0043: the list is composed on each push, never kept itself).
|
|
func brokerBehind(ctx context.Context, open *stores, names []string) (string, bool, error) {
|
|
s, err := brokerListState(ctx, open, names)
|
|
return s.Machine, s.Behind, err
|
|
}
|
|
|
|
// brokerListState is brokerBehind with why the list could not be composed, when it could not: the plan's own
|
|
// reason, which a remedy names rather than a push that would fail on it (the review of hq issue 353).
|
|
func brokerListState(ctx context.Context, open *stores, names []string) (busListState, error) {
|
|
inv := open.inventory
|
|
holders, err := seatHolders(ctx, inv)
|
|
if err != nil {
|
|
return busListState{}, err
|
|
}
|
|
h, held := holders[theBrokerSeat]
|
|
if !held || h.Node == "" {
|
|
return busListState{}, nil
|
|
}
|
|
shelf, err := inv.Catalogue(ctx)
|
|
if err != nil {
|
|
return busListState{}, err
|
|
}
|
|
if m, known := shelf[h.Module]; !known || m.BusUsers == "" {
|
|
// A holder that is sent no user list carries no grant: nothing to send first.
|
|
return busListState{}, nil
|
|
}
|
|
for _, n := range names {
|
|
if n == h.Node {
|
|
return busListState{Machine: h.Node}, nil
|
|
}
|
|
}
|
|
// **What cannot be worked out is behind** (the review of hq issue 353): the composition carrying the user list is not
|
|
// written, so whether the bus's machine runs it is not known; taken as behind, never "not behind".
|
|
// The user list is part of the bus module's composition on the machine holding it (ADR 0539 §15), so the machine is
|
|
// behind while its have does not name the digest of the composition written now.
|
|
fps, err := currentFingerprints(ctx, inv)
|
|
if err != nil {
|
|
return busListState{Machine: h.Node, Behind: true, Uncomposed: err}, nil
|
|
}
|
|
bus, err := aBus()
|
|
if err != nil {
|
|
return busListState{}, err
|
|
}
|
|
defer bus.Close()
|
|
js, err := jetstream.New(bus.Conn())
|
|
if err != nil {
|
|
return busListState{}, err
|
|
}
|
|
digest, digestErr := composedDigestOf(ctx, js, h.Node, h.Module, fps[h.Module])
|
|
have, had, err := haveOn(ctx, js, h.Node, h.Module)
|
|
if err != nil {
|
|
return busListState{}, err
|
|
}
|
|
return busListState{Machine: h.Node, Behind: listBehind(digest, digestErr, have, had), Uncomposed: digestErr}, nil
|
|
}
|
|
|
|
// composedDigestOf is the digest of the composition the controller wrote for a module on a node at a fingerprint.
|
|
func composedDigestOf(ctx context.Context, js jetstream.JetStream, node, module, fp string) (string, error) {
|
|
digests, err := js.KeyValue(ctx, sdkdelivery.ControllerComposedBucket)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
key, err := sdkdelivery.ComposedKey(node, module, fp)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
e, err := digests.Get(ctx, key)
|
|
if err != nil {
|
|
return "", fmt.Errorf("the composition %s is not written: %w", key, err)
|
|
}
|
|
v, err := sdkdelivery.DecodeComposedDigest(e.Value())
|
|
return v.Digest, err
|
|
}
|
|
|
|
// listBehind is whether the bus's machine is behind: its have does not name the composition written now, or the
|
|
// composition could not be read at all.
|
|
func listBehind(digest string, digestErr error, have sdkdelivery.Have, had bool) bool {
|
|
return digestErr != nil || !had || have.Composed != digest
|
|
}
|
|
|
|
// digestOf is what the mesh compares to answer "has this machine been sent what it should be".
|
|
//
|
|
// Over the same bytes that are sent, so the comparison is of the thing itself rather than of
|
|
// something derived beside it that could drift from it.
|
|
func digestOf(body []byte) string {
|
|
sum := sha256.Sum256(body)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// raiseTheBus asserts the streams and consumers the mesh's own traffic needs.
|
|
//
|
|
// **Every start, and it says what it did.** The objects are the mesh's, created by nothing else —
|
|
// the controller is their only writer (design 25 §3) — so a mesh that came up without them is one
|
|
// where nodes connect, authenticate, and hear nothing. Said rather than silent for the reason the
|
|
// first line of `serve` is said: a log that is quiet on success and loud on failure reads as broken
|
|
// when it is working.
|
|
func raiseTheBus(ctx context.Context, inv *inventory.Inventory, address string) error {
|
|
js, err := broker.Dial(address)
|
|
if err != nil {
|
|
return fmt.Errorf("the mesh is on the bus at %s and this control plane cannot reach it: %w",
|
|
broker.BareAddress(address), err)
|
|
}
|
|
defer js.Close()
|
|
// What the raise decided not to fail over. Said, for the reason everything else here is said:
|
|
// a consumer kept as it was is a difference between what the mesh asked for and what the bus
|
|
// holds, and one nobody would find by reading either (novox/hq 04-ISSUES/156).
|
|
js.Note = func(format string, args ...any) {
|
|
fmt.Printf(" "+format+"\n", args...)
|
|
}
|
|
|
|
// **Its own user, before anything else.** The controller's account is created by the installer at
|
|
// a bootstrap password, before there is a controller to mint one — so nothing recorded a hash for
|
|
// it, and the first composition would leave the writer out of the file it was writing. Recorded
|
|
// only if absent: a credential the mesh minted since is the one that counts.
|
|
// **Its own user, before anything else it does here.** The controller's account is created by the
|
|
// installer at a bootstrap password, before there is a controller to mint one — so nothing
|
|
// recorded a hash for it, and the first composition would leave the writer out of the file it was
|
|
// writing: a bus nothing can connect to, produced by the thing connected to it. Recorded only if
|
|
// absent, so a restart cannot put the bootstrap credential back over a rotated one.
|
|
if user, password, _ := broker.CredentialIn(address); user != "" && password != "" {
|
|
if err := inv.SeedBusUser(ctx, inventory.BusUser{
|
|
Username: user, Kind: inventory.BusController,
|
|
}, password); err != nil {
|
|
return fmt.Errorf("cannot record the credential this control plane is using: %w", err)
|
|
}
|
|
}
|
|
|
|
// The mesh's own streams and consumers, the seats' work queues and their workers, and how every
|
|
// machine hears its declaration — one derivation, which the self-check reads as well (D6, D7).
|
|
names, err := assertBusObjects(ctx, inv, js)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// And the controller's own buckets (novox/hq to-be 45 §1): the calls it serves and the acts done
|
|
// by hand, kept where a restart of this process does not take them.
|
|
if err := js.EnsureControllerBuckets(); err != nil {
|
|
return err
|
|
}
|
|
// Every module's state (novox/hq ADR 0201), from the catalogue: a bucket exists from
|
|
// registration, so a module reading one may watch it before its owner runs anywhere. One that
|
|
// nothing declares any more is said and kept — what it holds is data.
|
|
buckets, err := inv.DeclaredBuckets(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
undeclared, err := broker.RaiseBuckets(js, buckets)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(undeclared) > 0 {
|
|
fmt.Printf("the bus holds state nothing declares any more, kept because it is data: %s — "+
|
|
"removing it is a person's act\n", strings.Join(undeclared, ", "))
|
|
}
|
|
// Every module's log (novox/hq ADR 0297), from the catalogue, as its buckets: one that nothing
|
|
// declares any more is said and kept — a log is a module's record, and no path of the mesh removes it.
|
|
logs, err := inv.DeclaredLogs(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
unlogged, err := broker.RaiseLogs(js, logs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(unlogged) > 0 {
|
|
fmt.Printf("the bus holds logs nothing declares any more, kept because they are data: %s — "+
|
|
"removing one is a person's act\n", strings.Join(unlogged, ", "))
|
|
}
|
|
// And how every module hears what it consumes: asserted with the rest above, counted here.
|
|
hearing, err := moduleConsumerCount(ctx, inv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("the bus at %s has its streams, %d machine(s) can hear a declaration, %d module(s) "+
|
|
"can hear what they consume, %d bucket(s) of state and %d log(s)\n", broker.BareAddress(address), len(names), hearing,
|
|
len(buckets), len(logs))
|
|
return nil
|
|
}
|
|
|
|
// seatHolders is who holds each of the mesh's seats, by seat name: the record where a handover
|
|
// wrote one, and the assigned module claiming the seat otherwise — the same derivation the
|
|
// resolver makes, read from the catalogue rather than re-resolved.
|
|
func seatHolders(ctx context.Context, inv *inventory.Inventory) (map[string]broker.Holder, error) {
|
|
out := map[string]broker.Holder{}
|
|
entries, err := inv.Catalogued(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, e := range entries {
|
|
if len(e.On) == 0 {
|
|
continue
|
|
}
|
|
for _, c := range e.Manifest.Claims {
|
|
seat, known := catalogue.SeatNamed(c.Name)
|
|
if !known {
|
|
continue
|
|
}
|
|
if _, taken := out[seat.Name]; !taken {
|
|
out[seat.Name] = broker.Holder{Node: e.On[0], Module: e.Manifest.Module}
|
|
}
|
|
}
|
|
}
|
|
recorded, err := inv.Holdings(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, h := range recorded {
|
|
if seat, known := catalogue.SeatNamed(h.Claim); known {
|
|
out[seat.Name] = broker.Holder{Node: h.Node, Module: h.Module}
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// epochForActs is the lease's gate as a composition asks it; a variable so a test can act under an epoch
|
|
// without a bus.
|
|
var epochForActs = func(ctx context.Context) (uint64, error) { return theLease.epoch(ctx) }
|
|
|
|
// schemaReach is the highest migration this build carries for the inventory's store (novox/hq issue 352).
|
|
func schemaReach() (int, error) {
|
|
migrations, err := inventory.Migrations()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
reach := 0
|
|
for _, m := range migrations {
|
|
if m.Number > reach {
|
|
reach = m.Number
|
|
}
|
|
}
|
|
return reach, nil
|
|
}
|