The build-machine seat emits `started` and `log.<build id>` beside `built`. Every line the builder speaks — each step, each command with its duration, and on failure the command's own output — goes to stderr as before and onto the bus under the build's id, one subject per build, kept a week in EVENTS with every other event. `builds --log <id>` reads it back from the stream with a consumer that is gone when the reading is done, on the command line and as the controller's seat verb; `builds` lists each build's id and `build` says the id it asked with. Lines are core publishes with a sequence number, so a build is not slowed by an ack per line and a gap is visible; `started` and `built` are awaited into the stream. The seat protocol widens additively at the controller's next start; the holder's grant follows on the broker node's next composition.
150 lines
6.6 KiB
Go
150 lines
6.6 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Asking a role to build something, and being told what came of it.
|
|
//
|
|
// A build is work submitted to a role, not a message to a machine (novox/hq ADR 0121). The
|
|
// build-machine seat accepts a build and emits an outcome, so the same publish that answers whoever
|
|
// asked also reaches the controller that records it and the catalogue that places it in the module
|
|
// graph — and no build machine needs permission to publish into anybody's inbox.
|
|
//
|
|
// **This is the one flow whose shape differs from every other**, which is why it has its own seam
|
|
// rather than living in `Bus`. Everything else the controller sends is either an event nobody must
|
|
// act on or a declaration a node reconciles toward; a build is a request that takes minutes and has
|
|
// exactly one answer. Too long for request/reply, too particular to be an event.
|
|
|
|
// TheBuildMachine is the role a build is submitted to.
|
|
const TheBuildMachine = "mesh-build-machine"
|
|
|
|
// BuildWork is where a build request lands, and BuildOutcome is where its result does. Derived from
|
|
// the seat, so both sides name the role and neither names the other.
|
|
func BuildWork() string { return "mesh.seat." + TheBuildMachine + ".accept.build" }
|
|
func BuildOutcome() string { return "mesh.seat." + TheBuildMachine + ".event.built" }
|
|
|
|
// BuildStarted is where a build machine says it has taken a build, and BuildLog is where it says
|
|
// what it is doing, one line per message, under the build's own id (novox/hq ADR 0157).
|
|
//
|
|
// **The whole build is on the bus as it happens.** The outcome alone told a person that a build
|
|
// failed and its first line why; everything between — which command, how long, where it hung —
|
|
// lived in one container's stderr on one machine. Every line is now an event of the role, retained
|
|
// with the rest of the mesh's events, so a reader follows a build live by subscribing its subject,
|
|
// or reads it back afterwards from the stream, and a viewer is a subscriber and nothing more.
|
|
func BuildStarted() string { return "mesh.seat." + TheBuildMachine + ".event.started" }
|
|
func BuildLog(id string) string { return "mesh.seat." + TheBuildMachine + ".event.log." + id }
|
|
|
|
// BuildStart is what a build machine says the moment it takes a build.
|
|
type BuildStart struct {
|
|
ID string `json:"id"`
|
|
Repository string `json:"repository"`
|
|
Path string `json:"path,omitempty"`
|
|
Ref string `json:"ref,omitempty"`
|
|
On string `json:"on"`
|
|
At string `json:"at"`
|
|
}
|
|
|
|
// BuildLine is one thing a build said while building.
|
|
type BuildLine struct {
|
|
ID string `json:"id"`
|
|
// Seq counts the lines of one build from 1, so a reader that joined late or read two copies
|
|
// can order them and see a gap.
|
|
Seq int `json:"seq"`
|
|
At string `json:"at"`
|
|
// Step is which part of the build spoke — clone, context, image, run, failed — and Message is
|
|
// what it said, as the builder's own log prints it.
|
|
Step string `json:"step"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// KeyRoleBuilt is the build outcome under the role's name, on the bus the mesh runs on today.
|
|
//
|
|
// The same event as KeyModuleBuilt and published beside it, because a catalogue installed before this
|
|
// change listens for the module's name and one installed after listens for the role's. Both, until
|
|
// this bus retires: a rename needs publisher and subscriber to change together, and a deployment
|
|
// cannot promise which arrives first.
|
|
const KeyRoleBuilt = "built"
|
|
|
|
// Builders is how work reaches a build machine and how the outcome comes back.
|
|
type Builders interface {
|
|
// Submit asks for one build and waits for its outcome.
|
|
//
|
|
// The wait is long by nature. A build clones, pulls a base image and runs a container build, so
|
|
// a timeout here says "nothing is doing builds" rather than "this build is slow" — and the two
|
|
// need different remedies, which is why the message distinguishes them.
|
|
Submit(ctx context.Context, request BuildRequest, wait time.Duration) (BuildResult, error)
|
|
|
|
// Close lets go of whatever was dialled.
|
|
Close()
|
|
}
|
|
|
|
// BuildMachine is a machine taking work from the role it holds.
|
|
type BuildMachine interface {
|
|
// Take hands each request to do until the context ends, and says why it stopped.
|
|
Take(ctx context.Context, do func(context.Context, Build)) error
|
|
Close()
|
|
}
|
|
|
|
// Build is one request a machine has been handed.
|
|
type Build interface {
|
|
// Began says the build has been taken and is under way, before anything runs.
|
|
Began(ctx context.Context) error
|
|
|
|
// Say publishes one line of what the build is doing. Never fails the build: a line the bus
|
|
// did not take is a line lost, and the outcome still comes.
|
|
Say(step, message string)
|
|
|
|
// Request is what to build.
|
|
Request() BuildRequest
|
|
|
|
// Announce publishes the outcome as the role's own event.
|
|
//
|
|
// One publish, three audiences: whoever asked matches it by the id their request carried, the
|
|
// controller records it, and the catalogue places it. On the bus the mesh runs on today that
|
|
// fan-out came from a shared exchange; here the mesh derived the subject.
|
|
Announce(ctx context.Context, result BuildResult) error
|
|
|
|
// Done settles the request. Called only after the outcome is away, so a machine that dies
|
|
// before announcing leaves the work for another rather than losing it.
|
|
Done() error
|
|
|
|
// Hold hands the work back for another attempt after the delay.
|
|
Hold(after time.Duration) error
|
|
}
|
|
|
|
// waitingFor is the message a caller gets when nothing answered. Its own function because both
|
|
// transports say it, and saying it differently in two places is how one of them ends up vague.
|
|
func waitingFor(wait time.Duration) error {
|
|
return fmt.Errorf(
|
|
"no build machine answered within %s. Either nothing holds %s — in which case the work is "+
|
|
"queued and will be done when something does — or a build is taking longer than this",
|
|
wait, TheBuildMachine)
|
|
}
|
|
|
|
// theOutcomeOf reads a result and says whether it is the answer to this request.
|
|
func theOutcomeOf(body []byte, id string) (BuildResult, bool, error) {
|
|
var result BuildResult
|
|
if err := json.Unmarshal(body, &result); err != nil {
|
|
return BuildResult{}, false, fmt.Errorf("a build machine answered with something unreadable: %w", err)
|
|
}
|
|
// Somebody else's build. Skipped rather than returned, because returning it would attribute one
|
|
// build's outcome to another's.
|
|
return result, result.ID == id, nil
|
|
}
|
|
|
|
// ModuleOf reads the module's name out of a manifest a build produced, which is the only place it is
|
|
// authoritative — a request named a repository and a path, not a module.
|
|
func ModuleOf(manifest json.RawMessage) string {
|
|
var named struct {
|
|
Module string `json:"module"`
|
|
}
|
|
if err := json.Unmarshal(manifest, &named); err != nil {
|
|
return ""
|
|
}
|
|
return named.Module
|
|
}
|