The take-in watched the builder's and the indexer's buckets, started a follower per module that retried on a 30 s timer, and at start took every catalogued module in alphabetically: on 2026-10-25 a restart took in about 2 modules a minute, about 190 followers queued behind 16 slots, and every take-in composed two or three times even with nothing new, so a new build waited 30 minutes and more for its credential. Now two durable consumers named mesh-controller-take-in read the indexer's modules bucket (every module's last trunk value when the consumer is made, then each write) and the builder's builds bucket (each write), at most 16 keys unacknowledged each. The bus keeps what is not taken in: the start's look is the consumers' own catch-up, and a restart costs only what was left unacknowledged. A batch reads the catalogue once, takes each module in once, and asks the composer twice: once for its modules' nodes, once for the bus's node with the grants. A module registered at its trunk whose compositions and grant exist, or that this process took in at the same trunk, composes nothing; a key delivered again is taken in whole. A wait for the builder's or indexer's next write is acknowledged, a wait on a read or a failure refused with a 30 s delay. The backlog is in status, and a backlog standing still for 5 minutes raises take-in-stalled. The controller's account is granted the two consumers' acks. The follower tests go with the followers: twelve tests become three. Research 072: the take-ins on a bounded pull consumer (milestone 4, done keys alone) and the start's look composing only what changed (milestone 2).
280 lines
13 KiB
Go
280 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"io"
|
|
"os"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// asking a build machine for a module, and what came back.
|
|
//
|
|
// 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.
|
|
|
|
// buildsCommand says what has been built lately.
|
|
func buildsCommand(ctx context.Context, args []string) error {
|
|
set := flag.NewFlagSet("builds", flag.ContinueOnError)
|
|
limit := set.Int("n", 20, "how many to show")
|
|
positionals, err := parseAround(set, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
module := ""
|
|
if len(positionals) == 1 {
|
|
module = positionals[0]
|
|
} else if len(positionals) > 1 {
|
|
return errors.New("builds [<module>] [-n N]")
|
|
}
|
|
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer open.Close()
|
|
inv := open.inventory
|
|
|
|
builds, err := inv.Builds(ctx, module, *limit)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(builds) == 0 {
|
|
// Said rather than printed as nothing: an empty list and a failed read must never look
|
|
// the same, and getting here means the store answered.
|
|
if module != "" {
|
|
fmt.Printf("nothing has been built for %s\n", module)
|
|
return nil
|
|
}
|
|
fmt.Println("nothing has been built yet")
|
|
return nil
|
|
}
|
|
|
|
for _, b := range builds {
|
|
printBuild(os.Stdout, b)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// printBuild says one build: what, its outcome, where and when, what it made and published, its reuse key, and its
|
|
// source fingerprint (novox/hq issue 1204: the one tool that reads what the builder recorded it was made from).
|
|
func printBuild(w io.Writer, b inventory.Build) {
|
|
what := b.Module
|
|
if what == "" {
|
|
// It failed before knowing what it was building, which is most of the interesting
|
|
// failures. The repository is what a person has to go and look at.
|
|
what = "?"
|
|
}
|
|
outcome := "built " + short(b.Commit)
|
|
if !b.Worked() {
|
|
outcome = "failed"
|
|
}
|
|
fmt.Fprintf(w, "%-18s %-14s %-10s %s %s\n",
|
|
what, outcome, b.On, b.At.Local().Format("2006-01-02 15:04"), b.ID)
|
|
fmt.Fprintf(w, " %s", b.Repository)
|
|
if b.Ref != "" {
|
|
fmt.Fprintf(w, " at %s", b.Ref)
|
|
}
|
|
fmt.Fprintln(w)
|
|
for _, made := range b.Made {
|
|
fmt.Fprintf(w, " %-10s %s\n", made.Kind, made.Reference)
|
|
}
|
|
// What it published and under what reuse key (novox/hq ADR 0408 §1).
|
|
if p := b.Published; p != nil {
|
|
if p.Artifact != "" {
|
|
fmt.Fprintf(w, " published %s\n", p.Artifact)
|
|
}
|
|
if p.Package != nil {
|
|
fmt.Fprintf(w, " published %s@%s\n", p.Package.Name, p.Package.Version)
|
|
}
|
|
}
|
|
if b.ReuseKey != "" {
|
|
fmt.Fprintf(w, " reuse key %s\n", b.ReuseKey)
|
|
}
|
|
if b.SourceFingerprint != "" {
|
|
fmt.Fprintf(w, " source %s\n", b.SourceFingerprint)
|
|
}
|
|
if !b.Worked() {
|
|
// The builder's own first line. The whole failure is often a build log, and printing
|
|
// it here would bury every other row.
|
|
fmt.Fprintf(w, " %s\n", firstLine(b.Failed))
|
|
}
|
|
}
|
|
|
|
// takenBuild is a build the controller judges and registers, whatever told it of the build: build-agent's outcome (the
|
|
// builder's own module's, until R4g, ADR 0453 §5) or the builder's done key (ADR 0501 §1). **One judging for both**:
|
|
// the rules of who may register, the gate a build failed, the predefined keys and the rule that an unchanged build
|
|
// source moves nothing are the same whichever part built it (ADR 0501 §1, §3).
|
|
type takenBuild struct {
|
|
kept inventory.Build
|
|
manifest catalogue.Manifest
|
|
recorded inventory.Source
|
|
ask registrationAsk
|
|
path string
|
|
trunk string
|
|
// by and what name the build in what is said: who built it, and what was built ("ace", "novox/mesh-catalog").
|
|
by, what, commit string
|
|
// published refuses a build that may not become a module's version (off the trunk, in the scratch namespace);
|
|
// nil for a key, which is a trunk fingerprint's by construction (ADR 0501 §2).
|
|
published func() error
|
|
// carried publishes a list of predefined keys the build carried; nil when it carried none.
|
|
carried func() error
|
|
}
|
|
|
|
// registerJudged judges a recorded build and registers its module, or says why not.
|
|
func registerJudged(ctx context.Context, inv *inventory.Inventory, j takenBuild) (catalogue.Manifest, error) {
|
|
manifest, kept := j.manifest, j.kept
|
|
notRegistered := func(err error) (catalogue.Manifest, error) {
|
|
return manifest, fmt.Errorf("%s built %s (%s), recorded and not registered: %w", j.by, manifest.Module,
|
|
short(j.commit), err)
|
|
}
|
|
if err := namesNoInstallation(manifest); err != nil {
|
|
return manifest, fmt.Errorf("%s built %s (%s), and the mesh does not register it: %w",
|
|
j.by, j.what, short(j.commit), err)
|
|
}
|
|
// **Only from the repository the catalogue builds the module from** (novox/hq ADR 0266): else the trunk
|
|
// below is the trunk of whatever repository was built, which may be one an agent made — and a module named
|
|
// `sudo` from it would be what the next push sends. Another repository is the operator's, at the terminal.
|
|
repoID, err := mayRegisterFrom(ctx, inv, manifest.Module, j.recorded, j.ask, j.path, j.trunk)
|
|
if err != nil {
|
|
return notRegistered(err)
|
|
}
|
|
if j.published != nil {
|
|
if err := j.published(); err != nil {
|
|
return notRegistered(err)
|
|
}
|
|
}
|
|
// Then the module is judged against the list the mesh publishes, as every manifest read again is: one naming a
|
|
// key that list does not hold, or of another type, is refused here and never enters the catalogue.
|
|
if j.carried != nil {
|
|
if err := j.carried(); err != nil {
|
|
return manifest, err
|
|
}
|
|
}
|
|
if manifest.NamesPredefined() {
|
|
if _, err := catalogue.ParseManifest(kept.Manifest); err != nil {
|
|
return notRegistered(fmt.Errorf("judged against the list of predefined keys the mesh publishes, %w", err))
|
|
}
|
|
}
|
|
// **A build whose source is unchanged is never a move** (novox/hq issue 280): a rebuild made from
|
|
// what the build the mesh stands on was made from registers that build's artifacts at the new
|
|
// commit, so no machine is sent a new digest for a source nobody changed — an image is not
|
|
// byte-reproducible, and the bus rebuilt for another module's merge demanded a planned upgrade.
|
|
if kept.SourceFingerprint != "" {
|
|
stands, raw, err := inv.StandingBuild(ctx, manifest.Module, kept.ID)
|
|
if err != nil {
|
|
return manifest, err
|
|
}
|
|
if stands != "" && stands != kept.ID && len(raw) > 0 {
|
|
if was, err := catalogue.ParseManifest(raw); err == nil && was.Module == manifest.Module {
|
|
fmt.Printf("%s at %s was made from the source %s was: registered with its artifacts, no move\n",
|
|
manifest.Module, short(j.commit), stands)
|
|
manifest = was
|
|
}
|
|
}
|
|
}
|
|
if err := inv.RegisterModule(ctx, manifest, j.recorded); err != nil {
|
|
if errors.Is(err, inventory.ErrSuperseded) {
|
|
return notRegistered(err)
|
|
}
|
|
return manifest, err
|
|
}
|
|
// A bus process the new definition gains gets its account now, on each machine assigned the module, so the
|
|
// next push composes a real account (issue 1226).
|
|
issueGainedProcessAccounts(ctx, inv, manifest)
|
|
// Which repository it is registered from, by the forge's own id (novox/hq ADR 0266).
|
|
if err := inv.SetSourceIdentity(ctx, manifest.Module, repoID); err != nil {
|
|
return manifest, err
|
|
}
|
|
// The keep set just moved, and new bytes just landed (novox/hq ADR 0189). Asked here rather
|
|
// than on a timer of its own: this is the only moment either is true. Never fatal — the build
|
|
// worked and the module is registered.
|
|
collect(ctx, inv)
|
|
return manifest, nil
|
|
}
|
|
|
|
// answers is what the three questions came back with, read once.
|
|
type answers struct {
|
|
// notAnnounced are the owners some module reads the state of that have announced nothing yet (novox/hq ADR 0495,
|
|
// issue 1790): read from `reads` as before until they do; notAnnouncedWhy why that could not be read.
|
|
notAnnounced []string
|
|
notAnnouncedWhy string
|
|
wrong []inventory.Doing
|
|
nodes []inventory.Node
|
|
quiet []inventory.Node
|
|
behind map[string][]string
|
|
sources map[string]inventory.Source
|
|
// waiting is every machine not running what the mesh would send it.
|
|
waiting []inventory.Machine
|
|
// recordedWaits is, per machine, every move of a recorded module that waits there for a person (novox/hq ADR
|
|
// 0377 §6): the machine is behind for these, and `push <node>` sends them.
|
|
recordedWaits map[string][]string
|
|
// reported is every machine's last word beside when it was last sent a declaration — the
|
|
// pair that answers "has it caught up", which waiting alone cannot (the sent digest is
|
|
// recorded at send, not at apply).
|
|
reported []inventory.Reported
|
|
// refused is why a machine cannot be worked out at all, by name. A different thing from every
|
|
// other answer here: those are about a machine that was told something, and this is about one
|
|
// that cannot be told anything — it never reaches waiting, because nothing was computed for it
|
|
// to compare against, so without this a wholly blocked mesh reads as a well one.
|
|
refused map[string]string
|
|
// network is why the private network could not be computed, when it could not. Almost always
|
|
// a consequence of the refusals above: a node that does not resolve is not on the network, and
|
|
// a mesh whose hub is that node has no hub.
|
|
network string
|
|
// filtered is every converged machine that is not filtered by the mesh alone (novox/hq ADR
|
|
// 0168): what filters it beyond the mesh's own, the runtime's plumbing and bans, by name — a
|
|
// predecessor's chain, a found firewall in force again. Such a machine is not "all well".
|
|
filtered map[string]inventory.Filtering
|
|
// untaken is, per machine, each assigned module whose resources the machine is holding as it
|
|
// found them, and how many — a module that was assigned, sent, and is running none of what it
|
|
// declares because nothing has taken it (novox/hq ADR 0100, 04-ISSUES/125).
|
|
//
|
|
// **Its absence cost an outage.** The module was assigned, the push reported success, this
|
|
// command said the machine was doing everything it was told, and the module's three containers
|
|
// did not exist. On the strength of those reports the predecessor's proxy was stopped and every
|
|
// public name on the machine went dark. The holds were correct; they were recorded only in the
|
|
// machine's own state file, and the one visible symptom was a count that did not add up.
|
|
untaken map[string]map[string]int
|
|
// leftOut is, per machine, every module of its set its composition leaves out, and why (novox/hq issue
|
|
// 380): assigned and not applied, which every push said only in passing. Not well while there is any.
|
|
leftOut map[string][]leftOutModule
|
|
// unheld is every module on a machine whose resources are applied through a seat nothing on
|
|
// that machine holds (novox/hq ADR 0207), with the modules that could hold it. Reported, not
|
|
// refused, until the switch — and while there is any, the mesh is not all well: the order the
|
|
// machines' modules are built in is the mesh's to keep, and this is where it says it is not kept.
|
|
unheld []catalogue.Unheld
|
|
// conditions is every open condition (novox/hq to-be 45 §2), urgent first and then oldest first:
|
|
// what leads status, and what its all-well sentence needs to be none of, silenced ones included.
|
|
// A provider failing a consumer is one of them (ADR 0224). conditionsUnread says why they could
|
|
// not be read when they could not — never read as none.
|
|
conditions []conditions.Condition
|
|
conditionsUnread string
|
|
// overflowing is every module whose identity overflows the bound of a provision it requires
|
|
// (novox/hq ADR 0225): its provider leaves it out of the grants and composes everything else, so
|
|
// this is the one place it is said across the mesh. Not well while there is any.
|
|
overflowing []catalogue.Overflow
|
|
// handActs is how many acts were done by hand in the last seven days (novox/hq to-be 45 §7), nil
|
|
// where the log is not on hand; handActsUnread why it could not be read when it could not.
|
|
handActs *int
|
|
handActsUnread string
|
|
// heals is what the healers did in the last seven days (novox/hq to-be 45 §7): acts, and conditions
|
|
// handed to the operator; healsUnread why it could not be read.
|
|
heals *healsCount
|
|
healsUnread string
|
|
// pending is every assignment waiting for its module's build, and every one ended in the last day
|
|
// (novox/hq issue 325); pendingUnread why they could not be read. One waiting, or one ended other than
|
|
// applied, is not well: an assignment somebody made is not made yet, or will not be.
|
|
pending []inventory.PendingAssignment
|
|
pendingUnread string
|
|
// takeIn is the take-in's backlog of done keys on the bus (novox/hq ADR 0544 §6), nil where it cannot be read
|
|
// (a process with no bus, or a consumer not made yet).
|
|
takeIn *takeInBacklog
|
|
}
|