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 }