ADR 0121 carried through to working code. `Builders` is the asking side and `BuildMachine` the taking side, each with an implementation per bus, and the builder binary and the `build` command now go through them. On the bus being built, one publish does what two did. The old bus answered the asker through a reply queue and announced to an events exchange, because two audiences meant two topologies. Here the outcome is the role's own event: the asker matches it by the id its request carried, the controller records it, the catalogue places it in the graph. So a build machine publishes once, needs a reply queue for nothing, and needs a grant over nobody's inbox — which is what ruled out the alternatives. The outcome carries the module name now. Only the manifest says what was built, and on the old bus the separate announcement carried it; with one message for three readers it belongs in the result. A failed build names none, because it produced no module version and the catalogue would otherwise place something that was never made. Checked against a real server: the whole round trip; a third party on the role's event hearing the same outcome the asker did, which is the claim the decision rests on; work leaving the queue once settled, so no second machine repeats it; work submitted with no machine holding the role waiting instead of failing, and being done when one arrives; and work a machine handed back coming round again. One thing I got wrong twice now and have written down where it bit: binding to a consumer must name that consumer's own filter subject, not the narrower subject the caller cares about. The client compares the two and refuses anything that is not equal, with "subject does not match consumer".
101 lines
4.3 KiB
Go
101 lines
4.3 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" }
|
|
|
|
// 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 {
|
|
// 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
|
|
}
|