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 {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// 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.
|
// as two fields and this machine joins them once, here, to dial.
|
||||||
User string `json:"user,omitempty"`
|
User string `json:"user,omitempty"`
|
||||||
Password string `json:"password,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
|
// 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.
|
// build-agent to the machines that build; unassign builder and forget it and its seat's stream.
|
||||||
const TheBuildMachine = "node-build-agent"
|
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
|
// 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.
|
// the seat, so both sides name the role and neither names the other. The no-argument forms name the
|
||||||
func BuildWork() string { return "mesh.seat." + TheBuildMachine + ".accept.build" }
|
// current role; the `Of` forms take the seat, for the handover during which two roles exist.
|
||||||
func BuildOutcome() string { return "mesh.seat." + TheBuildMachine + ".event.built" }
|
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
|
// 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).
|
// 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
|
// 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,
|
// 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.
|
// 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 BuildStarted() string { return BuildStartedOf(TheBuildMachine) }
|
||||||
func BuildLog(id string) string { return "mesh.seat." + TheBuildMachine + ".event.log." + id }
|
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.
|
// BuildStart is what a build machine says the moment it takes a build.
|
||||||
type BuildStart struct {
|
type BuildStart struct {
|
||||||
|
|||||||
@@ -115,9 +115,16 @@ type natsMachine struct {
|
|||||||
sub *nats.Subscription
|
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 {
|
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() {
|
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.
|
// the ask to a second machine nor counts the wait against its deliveries.
|
||||||
working := make(chan struct{})
|
working := make(chan struct{})
|
||||||
go stillWorking(msg, working)
|
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)
|
close(working)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,6 +212,8 @@ type natsBuild struct {
|
|||||||
msg *nats.Msg
|
msg *nats.Msg
|
||||||
on string
|
on string
|
||||||
js *broker.JetStream
|
js *broker.JetStream
|
||||||
|
// seat is the role this build was taken from; what the machine says about it is that role's.
|
||||||
|
seat string
|
||||||
seq int
|
seq int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +241,7 @@ func (b *natsBuild) Announce(ctx context.Context, result BuildResult) error {
|
|||||||
}
|
}
|
||||||
publish, cancel := context.WithTimeout(ctx, 30*time.Second)
|
publish, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||||
defer cancel()
|
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 fmt.Errorf("cannot announce a build's outcome: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -252,7 +261,7 @@ func (b *natsBuild) Began(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
publish, cancel := context.WithTimeout(ctx, 30*time.Second)
|
publish, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||||
defer cancel()
|
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 fmt.Errorf("cannot say a build started: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -270,7 +279,7 @@ func (b *natsBuild) Say(step, message string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
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) }
|
func (b *natsBuild) Hold(after time.Duration) error { return b.msg.NakWithDelay(after) }
|
||||||
|
|||||||
Reference in New Issue
Block a user