The versions, bounds, reserved, publish-queue, acted, toolchains-heard and instances buckets were constants naming mesh-delivery-builder; ownBucketsOf takes them from the module the credential names. Builds: design 47 G A-296, §428 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
548 lines
32 KiB
Go
548 lines
32 KiB
Go
// Command mesh-delivery-builder is delivery's builder (novox/hq ADR 0453 clause 4, ADR 0478, issue 1607): it builds every
|
|
// new-form fingerprint the indexer writes, dark, into its own build keys (builds.<module>.<fingerprint>), through the build
|
|
// modules' fetch and build tools. Nothing reads its keys to deliver until release R4e. It serves no request subject and
|
|
// holds no timer: every reaction is a watch or a bus event (ADR 0437 §8).
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
|
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
|
mcseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
|
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
|
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
|
|
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient/enginereads"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds/forms"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
|
|
sweep "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshartifactsweep"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshbuilds"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep"
|
|
"git.novox.be/novox/mesh-sdk/go/operatorask"
|
|
"git.novox.be/novox/mesh-sdk/go/process"
|
|
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
|
"git.novox.be/novox/mesh-sdk/go/views"
|
|
|
|
"mesh-delivery-builder/internal/build"
|
|
"mesh-delivery-builder/internal/versions"
|
|
)
|
|
|
|
const (
|
|
// defaultStepBound is how long one ask's step may run once a build node took it (a fetch downloads the Go modules, a
|
|
// build compiles): from its start, never from its put, so an ask waiting in its stream for a free slot is not failed
|
|
// (buildrun.Asker.Bound; ADR 0525 §3, §6).
|
|
defaultStepBound = 30 * time.Minute
|
|
// defaultToolchainWait is the bound of a toolchain wait, its setting toolchain-wait-minutes (design 47 G §672).
|
|
defaultToolchainWait = 60 * time.Minute
|
|
)
|
|
|
|
func main() {
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
if err := run(ctx, os.Getenv, os.Stderr); err != nil {
|
|
fmt.Fprintln(os.Stderr, "mesh-delivery-builder:", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// run is the program: it returns when ctx ends or the bus cannot be used.
|
|
func run(ctx context.Context, getenv func(string) string, logOut io.Writer) error {
|
|
logger := log.New(logOut, "mesh-delivery-builder: ", log.LstdFlags)
|
|
cred, err := process.ReadBrokerAccount(getenv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
conn, js, err := cred.Dial()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
buildsKV, err := process.EssentialBucket(ctx, js, "builder", builds.BuildsBucket(cred.Module), "claim or write a build key")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// The assigner's assignments are its state, read by the first view of its read tool and then its views family, never its
|
|
// bucket (ADR 0615 §1, DLV-M1); the catalog half is read through the seat mesh-catalog. An owner that is down never holds the
|
|
// start: each is read on a goroutine once its first view is answered, and until then the unanswered first view is the
|
|
// builder's finding.
|
|
calls, err := process.Connect(cred, nats.Name(cred.Node+"."+cred.Module+".calls"), nats.RetryOnFailedConnect(true))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer calls.Close()
|
|
// The module has one durable consumer of EVENTS: one hub pulls it and hands each message to the part that hears it (the
|
|
// views reader and the builder's own events), started once every part is registered.
|
|
hub := process.NewEventHub(logger.Printf)
|
|
reader := &views.ProcessReader{Module: cred.Module, Call: calls.Call, Logf: logger.Printf, Hub: hub}
|
|
findingsKV := process.AwaitBucket(ctx, js, conn, builds.StateBucket(cred.Module, mcseat.StateFindings), "the builder's findings, a first view unanswered past its bound among them, are said in its log only", logger.Printf)
|
|
// The builder's findings are raised as they happen, so its one finding writer holds them (design 47 G A-29, §689): it
|
|
// is started once the state is bound, and what is written before is stated at its start.
|
|
fw, err := views.NewHeldWriter(views.WriterConfig[mcseat.Finding, mcseat.FindingCleared, mcseat.FindingsStated, mcseat.FindingsAskedEvent]{
|
|
Module: cred.Module, State: calls.State("findings"), Node: cred.Node, Asked: mcseat.SubjectFindingsAsked, JS: js, Conn: conn, Logf: logger.Printf})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
findingsKV.Ready(func(jetstream.KeyValue) {
|
|
if err := fw.Start(ctx); err != nil {
|
|
logger.Printf("the finding writer cannot start, its findings are kept in the state alone: %v", err)
|
|
}
|
|
})
|
|
reader.Findings = func() views.WireWriter { return fw }
|
|
// The catalog half per watched branch (design 47 G §348, §479, §614; A-5, A-15): the shared branch follower of
|
|
// meshcatalogclient on the builder's reader, so the modules, their manifests, their packages and each package's name in its
|
|
// ecosystem are read from the entries, never the indexer's states.
|
|
half := &catalogHalf{manifests: &entryManifests{}, logf: logger.Printf, ctx: ctx}
|
|
branches, err := meshcatalogclient.NewBranches(sweepWiring{meshcatalogclient.ProcessWiring(reader), half}, calls.Call)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
branches.Logf = logger.Printf
|
|
half.branches = branches
|
|
// The build modules are no names (design 47 G A-82): the holders of the build seats, the seat list's definitions and the
|
|
// catalog half's claims, found at each use.
|
|
// The part's own buckets are `<module>_<state>` of the credential's module (design 47 G A-296).
|
|
own := ownBucketsOf(cred.Module)
|
|
seatGate := newSeatGate()
|
|
// The part's one holder function (A-298): the seat list's callers and the views found by their seat (§717) read it.
|
|
holders := branches.SeatHolders(ctx)
|
|
seatGate.Holders(holders)
|
|
seats := buildrun.NewBuildSeats(seatGate.definitions, versions.HolderOf(branches))
|
|
bound := defaultStepBound
|
|
if v := strings.TrimSpace(getenv("MESH_BUILDER_STEP_BOUND")); v != "" {
|
|
if bound, err = time.ParseDuration(v); err != nil {
|
|
return fmt.Errorf("MESH_BUILDER_STEP_BOUND: %w", err)
|
|
}
|
|
}
|
|
tools := &buildrun.ToolchainSet{Seats: seats}
|
|
// Each build module's part of a build is an ask on its build seat's stream, pulled by a build node with a free slot
|
|
// (novox/hq ADR 0525): the builder chooses no node, so it reads neither presence nor have and holds no capacity.
|
|
// The read of an ask's steps is a kept call (design 47 G §568): a fault ends no key, and past five minutes unanswered it is a
|
|
// finding; its bound is a key of the state bounds, bound once the bus holds it (set below, with the findings).
|
|
stepReads := &stepsRead{bounds: func() jetstream.KeyValue { return nil }, bound: meshconditionsclient.CallUnansweredAfter, now: time.Now, logf: logger.Printf}
|
|
asks := buildrun.JetStreamAsks{JS: js, Unanswered: stepReads.unanswered, Answered: stepReads.answered}
|
|
// The steps of each build module are watched once the seat list and the catalog half name it (seatsChanged).
|
|
// The asks the build seats gave up on (issue 734): heard from DEAD_LETTERS, each ends the wait for its step at once,
|
|
// and the lane asks its part again (Builder.waitOnce) instead of waiting for the check bound.
|
|
// Heard in the background, and again at a reconnect or an ask while it is not; whether it is heard is kept in the
|
|
// state hearing, which builder_stuck says (dead_letters).
|
|
dead := &buildrun.DeadLetters{}
|
|
// The state may not be on the bus yet when a delivery started the builder (the grants part creates it, issue 1790): it
|
|
// is bound when the bus says it was created, and the latest hearing is written then.
|
|
hearingKV := process.AwaitBucket(ctx, js, conn, builds.StateBucket(cred.Module, buildrun.HearingState), "builder_stuck cannot say whether DEAD_LETTERS is heard", logger.Printf)
|
|
hearing := buildrun.NewDeadLetterHearing(ctx, buildrun.JetStreamDeadLetters{JS: js}, buildstep.AskLanguages, dead, logger.Printf,
|
|
buildrun.SayHearing(ctx, hearingKV, logger.Printf))
|
|
defer hearing.Stop()
|
|
// Each ask's step is heard by its own watch and, at every step end of its build module and every reconnect, read
|
|
// (buildrun.HearEnds): a watch that goes deaf loses no end (the lost check of 2026-10-28).
|
|
heard := buildrun.HearEnds(ctx, asks, asks, seats.Modules(), logger.Printf)
|
|
conn.SetReconnectHandler(func(*nats.Conn) { hearing.Again(); heard.Nudge("") })
|
|
// The builder asks the controller, as its own account, whether the artifact store still holds a done key's blobs
|
|
// (the seat verb held, novox/hq issue 1937) before it leaves the key done, on its calls connection.
|
|
// The builder numbers each trunk build at its claim (novox/hq design 51 *The versions*): its state versions, bound once
|
|
// the bus holds it; the floor's three sources asked as its own account.
|
|
versionsKV := process.AwaitBucket(ctx, js, conn, own.versions, "no trunk build is numbered: each is built unnumbered and published by its digest", logger.Printf)
|
|
found := &findings{module: cred.Module, w: fw, logf: logger.Printf, now: time.Now}
|
|
floorWaits := &versions.FloorWaits{Holder: versions.HolderOf(branches), Logf: logger.Printf, Said: found.floorSaid(context.WithoutCancel(ctx))}
|
|
// The names waiting on their floor are kept in the state floor-waits (DLV-B4): a restarted builder takes them in, so
|
|
// their silent sources' events still wake them; until the state is bound they are kept here only.
|
|
process.AwaitBucket(ctx, js, conn, builds.StateBucket(cred.Module, versions.FloorWaitsState), "the names waiting on their floor are kept in memory only, and builder_floor_waits shows none", logger.Printf).
|
|
Ready(func(kv jetstream.KeyValue) {
|
|
floorWaits.Keep(kvKeeper{ctx: ctx, kv: kv}, heldFloorWaits(ctx, kv, logger.Printf))
|
|
})
|
|
// A package is numbered under its name in its ecosystem, read from the catalog half's entries (design 32, design 47 G §348).
|
|
numberer := &versions.Numberer{Versions: versions.Versions{Store: awaitedVersions{b: versionsKV}}, Floor: floorVia(calls.Call, versions.RequirementsFrom(versions.EntriesOf(branches))), Waits: floorWaits,
|
|
Names: versions.NamesFrom(versions.EntriesOf(branches))}
|
|
// A publish that ends badly is a finding (design 51 *Health and conditions*): a holder's refused, a version no holder
|
|
// answered within its bound (a key of the state bounds with its own time to live), a seat's outcome that does not read.
|
|
pubBoundsKV := process.AwaitBucket(ctx, js, conn, own.bounds, "a publish no holder answers, and a node holding the old form seven days, are not raised", logger.Printf)
|
|
stepReads.bounds, stepReads.findings = func() jetstream.KeyValue { return pubBoundsKV.KV() }, found
|
|
pubs := &publishes{bounds: pubBoundsKV.KV, versions: numberer.Versions, findings: found, bound: versions.PublishBound, logf: logger.Printf}
|
|
numberer.Publishing = pubs.arm
|
|
stepExpiry := &buildrun.Expiry{}
|
|
var b *build.Builder
|
|
seatsChanged := func(ctx context.Context) {
|
|
heard.Watch(seats.Modules())
|
|
if b != nil {
|
|
b.Reconsider(ctx)
|
|
}
|
|
}
|
|
b = &build.Builder{
|
|
OwnModule: cred.Module,
|
|
Claimer: build.Claimer{Store: kvStore{buildsKV}, By: cred.Node + "." + cred.Module},
|
|
Seats: seats,
|
|
Asks: buildrun.Asker{Name: meshbuilds.Seat, Seats: seats, Publisher: asks, Waiter: buildrun.LostAskWaiter{Inner: heard, Dead: dead, Hearing: hearing}, Bound: bound, Dropper: buildrun.SeatDrops{Caller: calls},
|
|
Bounds: &stepBounds{bounds: pubBoundsKV.KV, js: js, now: time.Now, logf: logger.Printf}, Expiry: stepExpiry},
|
|
Held: heldOnTheBus{js: js, seats: seats},
|
|
// An ask that can no longer be answered is asked again (issue 1945), read at the step bound: a context's bound.
|
|
Gone: heldOnTheBus{js: js, seats: seats},
|
|
CheckAfter: bound,
|
|
Toolchains: tools,
|
|
Manifests: half.manifests,
|
|
Logf: logger.Printf,
|
|
Stored: storedVia(calls.Call),
|
|
Node: cred.Node,
|
|
}
|
|
// A key a look left building because its end could not be written is call-unanswered past five minutes (issue 1966,
|
|
// ADR 0668 §3): its bound is a key of the state bounds.
|
|
unput := &unputs{bounds: pubBoundsKV.KV, store: b.Claimer.Store, looking: b.Looking, findings: found,
|
|
bound: meshconditionsclient.CallUnansweredAfter, now: time.Now, logf: logger.Printf}
|
|
b.Left, b.Ended = unput.left, unput.ended
|
|
b.Numbers = build.VersionNumbers{N: numberer}
|
|
// The builder's toolchain waits (design 47 G §672): its state waits, and wait-held and wait-ended on its stream waits; its
|
|
// bound is its setting toolchain-wait-minutes.
|
|
waitBound, err := toolchainWaitOf(getenv("MESH_BUILDER_TOOLCHAIN_WAIT_FILE"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
waitsBucket := process.AwaitBucket(ctx, js, conn, builds.StateBucket(cred.Module, build.WaitsState), "the toolchain waits are kept in memory only, and builder_waits shows none", logger.Printf)
|
|
b.Waits = newWaitsKept(waitsBucket, func(ctx context.Context, event, id string, body any) error {
|
|
return emitEvent(context.WithoutCancel(ctx), js, cred, event, id, body)
|
|
}, waitBound, logger.Printf)
|
|
waitsBucket.Ready(func(kv jetstream.KeyValue) { b.Waits.(*waitsKept).start(ctx, kv) })
|
|
// The versions a build reserves at its start (design 47 G §369): its state reserved, and the catalog's versions-filled
|
|
// and version-released heard; nothing is reserved before the fill.
|
|
reservedKV := process.AwaitBucket(ctx, js, conn, own.reserved, "no version is reserved: builds are made unreserved", logger.Printf)
|
|
reserved := newReservations(awaitedReserved{awaitedVersions{b: reservedKV, bucket: own.reserved}}, calls.Call, pubBoundsKV.KV, found, b.Reconsider, logger.Printf)
|
|
// Each artifact the holder says published at a reserved version is said as the seat mesh-builds' package-published (design 47 G §52).
|
|
reserved.say = func(ctx context.Context, p meshbuilds.PackagePublished) error {
|
|
return emitSeatEvent(context.WithoutCancel(ctx), js, cred, meshbuilds.SubjectPackagePublished, "package-published-"+p.Module+"-"+p.Version+"-"+p.Artifact, p)
|
|
}
|
|
b.Reserved = reserved
|
|
// A state bound later numbers the builds looked at without it.
|
|
versionsKV.Ready(func(jetstream.KeyValue) { b.Reconsider(ctx) })
|
|
// The versions' hand acts (builder_publish_again, builder_floor_again) come through reasks, as builder_build_again.
|
|
acts := versions.Acts{Numberer: numberer, DropReserved: reserved.dropped, Reconsider: b.Reconsider}
|
|
// A reserved version left unpublished is published again, by the builder on an event or by a person's tool (design 47 G
|
|
// §588): its states publish-queue and acted, bound once the bus holds them.
|
|
republishMax, err := republishMaxOf(getenv("MESH_BUILDER_REPUBLISH_MAX_FILE"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
queueKV := process.AwaitBucket(ctx, js, conn, own.queue, "no version is published again on an event or by builder_publish_again", logger.Printf)
|
|
actedKV := process.AwaitBucket(ctx, js, conn, own.acted, "no version is published again on an event or by builder_publish_again", logger.Printf)
|
|
republish := &versions.Republisher{Reserved: reserved.r, Queue: awaitedQueue{awaitedReserved{awaitedVersions{b: queueKV, bucket: own.queue}}},
|
|
Acted: awaitedReserved{awaitedVersions{b: actedKV, bucket: own.acted}}, Entry: entryVia(calls.Call), Send: sendVia(calls.Call),
|
|
Unreleased: reserved.unreleasedWords, Max: republishMax, Logf: logger.Printf}
|
|
acts.Republish = republish.Republish
|
|
again := &republishing{rp: republish}
|
|
// The operator is asked once a key's event builds are spent (design 47 G §568): the builder's asker on the seat operator-channel.
|
|
decided := &decisions{subject: oc.SubjectDecided(cred.Module)}
|
|
retakeActed := awaitedReserved{awaitedVersions{b: actedKV, bucket: own.acted}}
|
|
asker := &operatorask.Asker[oc.Warrant]{Module: cred.Module, Call: calls.Call, Record: calls.Ask, Publish: publishVia(ctx, js), Subscribe: decided.hear(logger.Printf),
|
|
Contract: operatorask.Contract{Seat: oc.Seat, StateAsks: oc.StateAsks},
|
|
AskSubject: oc.AcceptSubjectAsk, DecidedSubject: oc.SubjectDecided}
|
|
operator := &build.OperatorAsk{Builder: b, Acted: retakeActed, Ctx: ctx, Logf: logger.Printf, Asker: asker,
|
|
Client: &occ.Client{Module: cred.Module, State: calls.State("asked"),
|
|
Publish: func(_ string, body any, id string) error { return asker.Ask(id, body) }}}
|
|
if err := operator.Listen(); err != nil {
|
|
return fmt.Errorf("hearing the operator's answers: %w", err)
|
|
}
|
|
retake := &retaking{rt: &build.Retaker{Builder: b, Acted: retakeActed, Entry: entryVia(calls.Call), Ask: operator.Ask, Logf: logger.Printf}}
|
|
queueKV.Ready(again.bound(ctx, js, logger.Printf))
|
|
actedKV.Ready(again.bound(ctx, js, logger.Printf))
|
|
errs := make(chan error, 10)
|
|
b.Members = versions.ModuleFrom(versions.EntriesOf(branches)) // the part a resolved package names (design 47 G §466)
|
|
// The builder's log (ADR 0571 §1): each build's start and end in its module log `build-log`, read by the view, the
|
|
// indexer and the assigner through their reads, and by the operator's command nox builds through the tool builder_builds_since.
|
|
b.Log = func(e builds.BuildLogEntry) {
|
|
if e.Node == "" { // a narration of no one build (a tag ask, the old form's window) is this instance's
|
|
e.Node = cred.Node
|
|
}
|
|
if err := appendLog(context.WithoutCancel(ctx), js, cred, e); err != nil {
|
|
logger.Printf("%v", err)
|
|
}
|
|
if err := emitBuildEnded(context.WithoutCancel(ctx), js, cred, e); err != nil {
|
|
logger.Printf("%v", err)
|
|
}
|
|
// A failed trunk build is the builder's finding build-failed, cleared by a later done one (DLV-B7).
|
|
found.buildEnded(context.WithoutCancel(ctx), e)
|
|
}
|
|
b.Said = func(e build.AskedAgain) {
|
|
if err := emitAskedAgain(context.WithoutCancel(ctx), js, cred, e); err != nil {
|
|
logger.Printf("%v", err)
|
|
}
|
|
}
|
|
// **Said once every build module answered a reference, taken away when stopped** (issue 1886, ADR 0522): the
|
|
// controller delivers from this builder's keys only while an instance says it writes references. A bucket not there
|
|
// yet (the manifest's state not made) is said, and the controller's plan waits for the keys, named (no build-agent
|
|
// is asked since R4g, ADR 0524 §7).
|
|
switch kv := process.OptionalBucket(ctx, js, own.instances, "the controller is not told that this builder writes references until it starts again", logger.Printf); {
|
|
case kv == nil:
|
|
default:
|
|
says := newSayer(ctx, kv, own.instances, cred.Node, seats.Modules, logger.Printf)
|
|
b.Answered = says.Answered
|
|
defer func() {
|
|
stopping, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
|
|
defer cancel()
|
|
if err := says.unsay(stopping); err != nil {
|
|
logger.Printf("%v", err)
|
|
}
|
|
}()
|
|
}
|
|
// The toolchains the build seats say (design 47 G §492, §604): kept in the state toolchains-heard, read back at the start, so
|
|
// a build that starts from the first modules value finds the nodes; the assignments only counts the nodes (ADR 0624 §1).
|
|
gate := buildrun.NewPlacedNodes(logger.Printf)
|
|
heardTools := newToolchainsHeard(tools, b.NodesChanged, func(ctx context.Context, e meshbuilds.ToolchainAgreed) error {
|
|
return emitSeatEvent(context.WithoutCancel(ctx), js, cred, meshbuilds.SubjectToolchainAgreed, "toolchain-agreed-"+e.Language+"-"+e.Version, e)
|
|
}, logger)
|
|
process.AwaitBucket(ctx, js, conn, own.toolchainsHeard, "the toolchains heard are kept in memory only, and builder_toolchains shows none", logger.Printf).
|
|
Ready(func(kv jetstream.KeyValue) {
|
|
heardTools.bind(ctx, kvHeard{kvKeeper{ctx: ctx, kv: kv}})
|
|
heardTools.readRunning(ctx, js)
|
|
dropped := func(e jetstream.KeyValueEntry) {
|
|
if !opOf(e) {
|
|
heardTools.Dropped(ctx, e.Key())
|
|
}
|
|
}
|
|
if w, err := kv.WatchAll(ctx, jetstream.UpdatesOnly()); err != nil {
|
|
process.NotWatched(logger.Printf, own.toolchainsHeard, err, "builder_toolchains_drop is not heard until the builder starts again")
|
|
} else {
|
|
go func() { errs <- follow(ctx, w, dropped) }()
|
|
}
|
|
})
|
|
// What each counted node's engine reads (ADR 0618 §2, ADR 0624 §1): asked through the node-engine seat's verb as the
|
|
// builder's own account, for every node the assignments names, and kept in the state engine-reads once it is bound.
|
|
reads := newEngineReads(calls.CallKeptOn, gate, logger.Printf)
|
|
reads.module = cred.Module
|
|
reads.keeper.Once = calls.CallOn // its fault keeps the last statement at once while the kept call asks on (ADR 0668 §3)
|
|
// The old reference form's window over them (DLV-B9), kept in the state old-form once it is bound; builder_old_form
|
|
// reads it and drops a retired node by hand.
|
|
reads.ctx = context.WithoutCancel(ctx)
|
|
reads.window = &oldForm{module: cred.Module, forms: forms.Current, findings: found, logf: logger.Printf, now: time.Now, log: func(e builds.BuildLogEntry) { b.Log(e) },
|
|
bounds: pubBoundsKV.KV,
|
|
said: func(body versions.OldFormClosedBody) {
|
|
if err := emitEvent(context.WithoutCancel(ctx), js, cred, versions.OldFormClosedEvent, "old-form-closed-"+body.At.Format(time.RFC3339Nano), body); err != nil {
|
|
logger.Printf("saying %s: %v", versions.OldFormClosedEvent, err)
|
|
}
|
|
}}
|
|
acts.DropOldForm = func(ctx context.Context, node, by, why string, at time.Time) (string, error) {
|
|
answer, err := reads.window.drop(ctx, node, by, why, at)
|
|
if err == nil {
|
|
reads.stepWindow()
|
|
}
|
|
return answer, err
|
|
}
|
|
process.AwaitBucket(ctx, js, conn, builds.StateBucket(cred.Module, versions.OldFormState), "the old form's window is kept in memory only, and builder_old_form shows none", logger.Printf).
|
|
Ready(func(kv jetstream.KeyValue) {
|
|
reads.window.bind(context.WithoutCancel(ctx), kv)
|
|
reads.stepWindow()
|
|
})
|
|
gate.OnPlaced = reads.placed
|
|
// A kept manifest-reads call unanswered past five minutes, its node's machine heard (the watchdogs seat's presence,
|
|
// asked when the bound ends), is call-unanswered in the builder's findings (issue 1966, ADR 0668 §3, ADR 0596 §2).
|
|
reads.callsOf(found, pubBoundsKV.KV, heardBy(calls.CallKept), logger.Printf)
|
|
b.Forms = reads.built
|
|
// The assignments names the nodes the builder counts: the seat mesh-module-assigner's view, followed at the holder the
|
|
// seat-holders view names and as it moves (design 47 G §717), its first view the holder's tool named for the state; no
|
|
// assigner module is named and nothing asks for the holder on a timer.
|
|
assignmentsErrs := make(chan error, 1)
|
|
assignments, err := reader.FollowSeat(mma.Seat, mma.StateAssignments, holders)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
go func() {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-assignments.Follower().Ready():
|
|
}
|
|
pe, err := buildrun.FollowAssignmentsIn(ctx, reader, assignments.Bucket(), gate, b.NodesChanged, logger.Printf)
|
|
if err != nil {
|
|
assignmentsErrs <- err
|
|
return
|
|
}
|
|
assignmentsErrs <- <-pe
|
|
}()
|
|
// Each consumer that is not there yet is the finding family-not-heard naming its stream until it binds at a later
|
|
// start (heard.Writer; ADR 0615 §5); one there that cannot be heard stops the builder, as before.
|
|
notHeard := found.notHeard(ctx)
|
|
// The seat mesh-artifact-sweep says artifacts-gone when it found a key's blob gone (issue 1937, ADR 0571 §2): the
|
|
// builder's durable consumer on EVENTS, made by the grants part from the manifest's consumes. Where it is not there yet the builder says
|
|
// so; it still asks held at its start and at every module value.
|
|
// The same consumer carries what wakes a floor (design 51 *The floor*): the indexer's manifest-accepted and the
|
|
// condition keeper's condition-cleared.
|
|
floor := floorWake{b: b, waits: floorWaits, logf: logger.Printf}
|
|
// And the four registry seats' published, refused and removed (DLV-B4), which move a numbered version.
|
|
seatHeard := seatHearer{n: numberer, b: b, logf: logger.Printf, pub: pubs, res: reserved}
|
|
events := func(m jetstream.Msg) {
|
|
if again.heard(ctx, m, logger.Printf) { // the publish again's own subjects, left for the bus to give again when its state cannot take them
|
|
return
|
|
}
|
|
if decided.heard(m) { // the operator's answer to an ask about a key (§568)
|
|
return
|
|
}
|
|
if retake.heard(ctx, m, logger.Printf) { // a key failed on a transport fault, built again when its cause passed (§568)
|
|
return
|
|
}
|
|
if reads.heard(m, logger.Printf) || heardTools.Reported(ctx, m) || heardTools.Running(ctx, m) || floor.heard(ctx, m) || reserved.heard(ctx, m) || seatHeard.heard(ctx, m) { // reads: the node-engine seat's manifest-reads-changed (ADR 0618 §4)
|
|
return
|
|
}
|
|
heardGone(ctx, b, m, logger.Printf)
|
|
}
|
|
// The seats' events (artifacts-gone, the registry seats' words, the build seats' toolchain-reported, manifest-accepted and
|
|
// manifest-reads-changed and the seat mesh-conditions' condition-cleared: seatEventPatterns); the owners' views events and the
|
|
// catalog half's catalog-revision and registration-changed and the gate's source-moved are the views reader's (a message has
|
|
// one taker, so no pattern here covers them).
|
|
// The seat list over the connection and heard on the hub: a change of it, as of the catalog half's holders, names the build
|
|
// modules again (design 47 G A-82, §204).
|
|
// The runner's build step ask, the builder's one entry point for a candidate (design 47 G A-97).
|
|
b.StepEntry = newEntryWaits(branches, half.manifests).entry
|
|
// The seat mesh-builds' verb reserved, answered here (A-115, A-127): the reservations, the catalog half's carriers and the seat
|
|
// mesh-catalog's versions.
|
|
stopReserved, err := calls.AnswerSeat(meshbuilds.Seat, seatlist.Definition(seatGate.Reader, meshbuilds.Seat), map[string]process.SeatHandler{
|
|
meshbuilds.VerbReserved: reservedVerb(ctx, reservedReader(ctx, reserved.r.Store), versions.CarriersFrom(versions.EntriesOf(branches)), catalogReleased(calls.Call))})
|
|
if err != nil {
|
|
return fmt.Errorf("answering the verb reserved: %w", err)
|
|
}
|
|
defer stopReserved()
|
|
if err := serveSteps(ctx, conn, js, cred, b, logger.Printf); err != nil {
|
|
return err
|
|
}
|
|
seatGate.Follow(seatlist.ProcessBus(calls.Call, hub), func() { seatsChanged(ctx) }, logger.Printf)
|
|
half.holders = func() { seatsChanged(ctx) }
|
|
for _, pattern := range seatEventPatterns {
|
|
if err := hub.Handle(pattern, events); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
hub.OnBound(reads.atBind) // what a node's engine said before the consumer was bound (ADR 0668 §4)
|
|
// The catalog half's entries as they come: a module's entry of a branch goes to the builder, and a key left building for a
|
|
// fingerprint no branch carries is closed (issue 1660) once the first view of every followed branch is in (never at the start:
|
|
// a holder of the seat that is down holds no other part of it).
|
|
half.b = b
|
|
half.ready = func() {
|
|
if err := b.Sweep(ctx, half.pictured()...); err != nil {
|
|
errs <- err
|
|
}
|
|
}
|
|
branches.Changed = half.Changed(ctx)
|
|
// A catalog revision the builder hears wakes a floor whose tags or requirements were silent (design 47 G A-119).
|
|
half.floor = func(ctx context.Context) {
|
|
(&floorWake{b: b, waits: floorWaits, logf: logger.Printf}).wake(ctx, versions.WakeRevision, "", "")
|
|
}
|
|
// All of the catalog half's wiring is set before the reader starts: its views are heard on the reader's goroutines from then.
|
|
reader.Start(ctx, js, cred, process.AwaitBucket(ctx, js, conn, builds.StateBucket(cred.Module, views.StatusState), "builder_first_views reads nothing and builder_first_view_again is not heard", logger.Printf))
|
|
// Every part has registered: the hub pulls the module's durable. Where the consumer is not there yet the builder says so
|
|
// and binds it at a later change (heard.Writer; ADR 0615 §5).
|
|
_ = notHeard.Bind(eventsStream, sweep.EventArtifactsGone+", what wakes a floor, the registry seats' words, the views and manifest-reads-changed are not heard until the builder starts again", func() error {
|
|
return hub.Start(ctx, js, cred)
|
|
})
|
|
go func() { <-ctx.Done(); hub.Stop() }()
|
|
// A person asks a key's build again with builder_ask_again, which writes the reasks state (issue 1945): watched, as
|
|
// every other input. The builder asks again by itself without it.
|
|
// A state not on the bus yet at this start is watched once it appears (issue 1790).
|
|
process.AwaitBucket(ctx, js, conn, builds.StateBucket(cred.Module, build.ReAsksState), "builder_ask_again is not heard; an ask that can no longer be answered is still asked again by itself", logger.Printf).Ready(func(kv jetstream.KeyValue) {
|
|
eachReAsk := func(e jetstream.KeyValueEntry) { reAsked(ctx, b, acts, kv, e, logger.Printf) }
|
|
if rw, err := settle(ctx, kv, eachReAsk); err != nil {
|
|
process.NotWatched(logger.Printf, builds.StateBucket(cred.Module, build.ReAsksState), err, "builder_ask_again is not heard until the builder starts again")
|
|
} else {
|
|
go func() { errs <- follow(ctx, rw, eachReAsk) }()
|
|
}
|
|
})
|
|
pubBoundsKV.Ready(func(kv jetstream.KeyValue) {
|
|
each := func(e jetstream.KeyValueEntry) {
|
|
if reserved.ended(ctx, e) {
|
|
return
|
|
}
|
|
if _, ok := versions.UnputBoundOf(e.Key()); ok {
|
|
unput.expired(ctx, e)
|
|
return
|
|
}
|
|
if buildrun.StepBoundOf(e.Key()) {
|
|
if e.Operation() == jetstream.KeyValuePurge { // its time to live ended: the ask's wait ends
|
|
stepExpiry.Ended(e.Key())
|
|
}
|
|
return
|
|
}
|
|
if _, _, ok := versions.StepsBoundOf(e.Key()); ok {
|
|
stepReads.expired(ctx, e)
|
|
return
|
|
}
|
|
if node, ok := enginereads.BoundOf(e.Key()); ok {
|
|
if e.Operation() == jetstream.KeyValuePurge { // its five minutes ended
|
|
reads.expiredCall(node)
|
|
}
|
|
return
|
|
}
|
|
if versions.IsOldFormBound(e.Key()) {
|
|
if e.Operation() == jetstream.KeyValuePurge {
|
|
reads.stepWindow() // a node's seven days ended: the step raises old-form-held now
|
|
}
|
|
return
|
|
}
|
|
pubs.ended(ctx, e)
|
|
}
|
|
watchBounds(ctx, kv, each, found.notHeard(ctx), errs, logger.Printf)
|
|
})
|
|
// A person reads a build seat's stream with builder_seat_queue, which writes the queues state (issue 1945 note 62): the
|
|
// builder alone may read the stream, so it answers there.
|
|
process.AwaitBucket(ctx, js, conn, builds.StateBucket(cred.Module, build.QueuesState), "builder_seat_queue is not answered", logger.Printf).Ready(func(kv jetstream.KeyValue) {
|
|
eachRead := func(e jetstream.KeyValueEntry) {
|
|
queueRead(ctx, heldOnTheBus{js: js, seats: seats}, kv, e, logger.Printf)
|
|
}
|
|
if qw, err := settle(ctx, kv, eachRead); err != nil {
|
|
process.NotWatched(logger.Printf, builds.StateBucket(cred.Module, build.QueuesState), err, "builder_seat_queue is not answered until the builder starts again")
|
|
} else {
|
|
go func() { errs <- follow(ctx, qw, eachRead) }()
|
|
}
|
|
})
|
|
// The state engine-reads, new with ADR 0618 (issue 1790's kind): until it is on the bus each statement is kept in memory
|
|
// only, and builder_engine_reads answers nothing.
|
|
process.AwaitBucket(ctx, js, conn, builds.StateBucket(cred.Module, EngineReadsState), "what each node's engine reads is kept in memory only", logger.Printf).Ready(func(kv jetstream.KeyValue) {
|
|
reads.bind(ctx, kv, errs, logger.Printf)
|
|
})
|
|
logger.Printf("started on %s", cred.Node)
|
|
select {
|
|
case <-ctx.Done():
|
|
b.Wait()
|
|
return nil
|
|
case err := <-errs:
|
|
return err
|
|
case err := <-assignmentsErrs:
|
|
return err
|
|
}
|
|
}
|
|
|
|
// toolchainWaitOf reads the toolchain wait's setting, toolchain-wait-minutes, from the file the manifest declares with the
|
|
// content ${config:toolchain-wait-minutes} (design 47 G §672, A-71), at the builder's start: no file, or an empty one, is
|
|
// the default; a content that is no number of minutes is an error naming the file.
|
|
func toolchainWaitOf(path string) (time.Duration, error) {
|
|
if path == "" {
|
|
return defaultToolchainWait, nil
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return defaultToolchainWait, nil
|
|
}
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
v := strings.TrimSpace(string(raw))
|
|
if v == "" {
|
|
return defaultToolchainWait, nil
|
|
}
|
|
n, err := strconv.Atoi(v)
|
|
if err != nil || n < 1 {
|
|
return 0, fmt.Errorf("%s holds %q, which is not a number of minutes", path, v)
|
|
}
|
|
return time.Duration(n) * time.Minute, nil
|
|
}
|