diff --git a/cmd/mesh-builder/main.go b/cmd/mesh-builder/main.go index f7d6946..0eb8215 100644 --- a/cmd/mesh-builder/main.go +++ b/cmd/mesh-builder/main.go @@ -137,7 +137,12 @@ func takeWorkFrom(credential Credential, on string) (link.BuildMachine, error) { if err != nil { return nil, err } - return link.MachineOverNATS(js, on), nil + // **The seat this machine serves is the one its credential claims** (novox/hq ADR 0190, the + // handover): the mesh issues a build machine's credential naming the seat its module claims, + // and one binary serves the old role as `builder` and the new as `build-agent` from that alone. + seat := link.BuildSeatClaimed(credential.seatsClaimed()) + fmt.Fprintf(os.Stderr, "taking build work as a holder of %s\n", seat) + return link.MachineOverNATSOn(js, on, seat), nil } // answer does one build and says what happened, whichever way it went. @@ -453,6 +458,21 @@ type Credential struct { // as two fields and this machine joins them once, here, to dial. User string `json:"user,omitempty"` Password string `json:"password,omitempty"` + // Claims are the seats the module this credential was issued for claims, as the mesh writes + // them beside the credential (novox/hq ADR 0159). The first is the build role this machine + // serves; a credential naming none is from before claims travelled in it. + Claims []struct { + Seat string `json:"seat"` + } `json:"claims,omitempty"` +} + +// seatsClaimed is the seats the credential names, in order. +func (c Credential) seatsClaimed() []string { + out := make([]string, 0, len(c.Claims)) + for _, claim := range c.Claims { + out = append(out, claim.Seat) + } + return out } // onTheNewBus is whether a credential is for the bus being built: its address says so, and the diff --git a/cmd/mesh-builder/seat_test.go b/cmd/mesh-builder/seat_test.go new file mode 100644 index 0000000..8518646 --- /dev/null +++ b/cmd/mesh-builder/seat_test.go @@ -0,0 +1,27 @@ +package main + +import ( + "encoding/json" + "testing" + + "github.com/novox/mesh-controller/internal/link" +) + +// The seat a build machine serves comes from its credential (novox/hq ADR 0190 handover). +func TestTheCredentialSaysWhichBuildRoleThisMachineServes(t *testing.T) { + var held Credential + if err := json.Unmarshal([]byte(`{"url":"nats://bus:4222","user":"anchor.builder","password":"x", + "claims":[{"seat":"mesh-build-machine","scope":"mesh","serves":[]}]}`), &held); err != nil { + t.Fatal(err) + } + if got := link.BuildSeatClaimed(held.seatsClaimed()); got != "mesh-build-machine" { + t.Errorf("the old builder's credential serves %q", got) + } + var bare Credential + if err := json.Unmarshal([]byte(`{"url":"nats://bus:4222","user":"anchor.build-agent","password":"x"}`), &bare); err != nil { + t.Fatal(err) + } + if got := link.BuildSeatClaimed(bare.seatsClaimed()); got != link.TheBuildMachine { + t.Errorf("a credential without claims serves %q, want %s", got, link.TheBuildMachine) + } +} diff --git a/internal/link/build_seat_test.go b/internal/link/build_seat_test.go new file mode 100644 index 0000000..4969798 --- /dev/null +++ b/internal/link/build_seat_test.go @@ -0,0 +1,32 @@ +package link + +import "testing" + +// A build machine serves the seat its credential claims (novox/hq ADR 0190 handover): the old +// `builder` keeps the old role, a `build-agent` takes the new, from one binary and no flag. +func TestABuildMachineServesTheSeatItsCredentialClaims(t *testing.T) { + if got := BuildSeatClaimed([]string{"mesh-build-machine"}); got != "mesh-build-machine" { + t.Errorf("a credential claiming the old role serves %q", got) + } + if got := BuildSeatClaimed([]string{"node-build-agent"}); got != TheBuildMachine { + t.Errorf("a credential claiming the new role serves %q", got) + } + if got := BuildSeatClaimed(nil); got != TheBuildMachine { + t.Errorf("a credential claiming nothing serves %q, want the current role", got) + } + if got := BuildSeatClaimed([]string{"", "node-build-agent"}); got != TheBuildMachine { + t.Errorf("an empty claim is skipped; got %q", got) + } +} + +// What a machine says about a build is the event of the seat it took the build from, so an outcome +// is heard where the asker of that seat listens. +func TestABuildsEventsAreItsSeats(t *testing.T) { + if BuildOutcomeOf(TheBuildMachineBefore) != "mesh.seat.mesh-build-machine.event.built" { + t.Error(BuildOutcomeOf(TheBuildMachineBefore)) + } + if BuildWorkOf(TheBuildMachine) != BuildWork() || BuildOutcomeOf(TheBuildMachine) != BuildOutcome() || + BuildStartedOf(TheBuildMachine) != BuildStarted() || BuildLogOf(TheBuildMachine, "b1") != BuildLog("b1") { + t.Error("the no-argument forms must name the current role") + } +} diff --git a/internal/link/builds.go b/internal/link/builds.go index 2f0b599..f43cf5b 100644 --- a/internal/link/builds.go +++ b/internal/link/builds.go @@ -31,10 +31,36 @@ import ( // build-agent to the machines that build; unassign builder and forget it and its seat's stream. const TheBuildMachine = "node-build-agent" +// TheBuildMachineBefore is the role a build was submitted to until ADR 0190: the mesh's one build +// machine, mesh-scoped. Kept named while the handover runs — a machine whose credential claims it +// still serves it, and the controller still hears its outcomes — and dropped with the retired seat +// row once nothing claims it. +const TheBuildMachineBefore = "mesh-build-machine" + +// BuildSeatClaimed is the build role a machine serves: the first seat its credential claims, or the +// current role when the credential names none (a credential from before claims travelled in it, or +// one written by hand). **The credential decides, not the binary** (ADR 0190 handover): one build +// machine binary runs as the old `builder` on the old seat and as a `build-agent` on the new one, +// each taking the work the mesh issued it a credential for, so neither drains the other's queue +// and the switch needs no flag day. +func BuildSeatClaimed(claimed []string) string { + for _, seat := range claimed { + if seat != "" { + return seat + } + } + return TheBuildMachine +} + // 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" } +// the seat, so both sides name the role and neither names the other. The no-argument forms name the +// current role; the `Of` forms take the seat, for the handover during which two roles exist. +func BuildWork() string { return BuildWorkOf(TheBuildMachine) } +func BuildOutcome() string { return BuildOutcomeOf(TheBuildMachine) } +func BuildWorkOf(seat string) string { return "mesh.seat." + seat + ".accept.build" } +func BuildOutcomeOf(seat string) string { + return "mesh.seat." + seat + ".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). @@ -44,8 +70,10 @@ func BuildOutcome() string { return "mesh.seat." + TheBuildMachine + ".event.bui // 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 } +func BuildStarted() string { return BuildStartedOf(TheBuildMachine) } +func BuildLog(id string) string { return BuildLogOf(TheBuildMachine, id) } +func BuildStartedOf(seat string) string { return "mesh.seat." + seat + ".event.started" } +func BuildLogOf(seat, id string) string { return "mesh.seat." + seat + ".event.log." + id } // BuildStart is what a build machine says the moment it takes a build. type BuildStart struct { diff --git a/internal/link/builds_nats.go b/internal/link/builds_nats.go index 0355857..9e158b6 100644 --- a/internal/link/builds_nats.go +++ b/internal/link/builds_nats.go @@ -115,9 +115,16 @@ type natsMachine struct { sub *nats.Subscription } -// MachineOverNATS takes build work from the role this machine holds. +// MachineOverNATS takes build work from the current build role. func MachineOverNATS(js *broker.JetStream, on string) BuildMachine { - return &natsMachine{js: js, on: on, seat: TheBuildMachine} + return MachineOverNATSOn(js, on, TheBuildMachine) +} + +// MachineOverNATSOn takes build work from the role named — the one this machine's credential claims +// (ADR 0190 handover): its asks come from that seat's worker, and what it says about a build goes +// out as that seat's events, so an outcome is heard where the asker listens. +func MachineOverNATSOn(js *broker.JetStream, on, seat string) BuildMachine { + return &natsMachine{js: js, on: on, seat: seat} } func (m *natsMachine) Close() { @@ -194,7 +201,7 @@ func (m *natsMachine) Take(ctx context.Context, do func(context.Context, Build)) // the ask to a second machine nor counts the wait against its deliveries. working := make(chan struct{}) go stillWorking(msg, working) - do(ctx, &natsBuild{request: request, msg: msg, on: m.on, js: m.js}) + do(ctx, &natsBuild{request: request, msg: msg, on: m.on, js: m.js, seat: m.seat}) close(working) } } @@ -205,7 +212,9 @@ type natsBuild struct { msg *nats.Msg on string js *broker.JetStream - seq int + // seat is the role this build was taken from; what the machine says about it is that role's. + seat string + seq int } func (b *natsBuild) Request() BuildRequest { return b.request } @@ -232,7 +241,7 @@ func (b *natsBuild) Announce(ctx context.Context, result BuildResult) error { } publish, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - if _, err := b.js.Context().Publish(BuildOutcome(), body, nats.Context(publish)); err != nil { + if _, err := b.js.Context().Publish(BuildOutcomeOf(b.seat), body, nats.Context(publish)); err != nil { return fmt.Errorf("cannot announce a build's outcome: %w", err) } return nil @@ -252,7 +261,7 @@ func (b *natsBuild) Began(ctx context.Context) error { } publish, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - if _, err := b.js.Context().Publish(BuildStarted(), body, nats.Context(publish)); err != nil { + if _, err := b.js.Context().Publish(BuildStartedOf(b.seat), body, nats.Context(publish)); err != nil { return fmt.Errorf("cannot say a build started: %w", err) } return nil @@ -270,7 +279,7 @@ func (b *natsBuild) Say(step, message string) { if err != nil { return } - _ = b.js.Conn().Publish(BuildLog(b.request.ID), body) + _ = b.js.Conn().Publish(BuildLogOf(b.seat, b.request.ID), body) } func (b *natsBuild) Hold(after time.Duration) error { return b.msg.NakWithDelay(after) }