A seat's work is shared by its holders: node-build-agent, pulled one ask at a time (hq ADR 0190) #228
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
+33
-5
@@ -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 {
|
||||
|
||||
@@ -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) }
|
||||
|
||||
Reference in New Issue
Block a user