Pass a seat held once to its only claimant when its recorded holder is assigned nowhere (issue 1317, ADR 0449)
This commit is contained in:
@@ -285,12 +285,22 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory,
|
||||
assigned []string
|
||||
}
|
||||
var others []candidate
|
||||
// **Every machine's assignments, before the holdings are read by any resolution** (novox/hq ADR 0449 §4): a
|
||||
// seat held once whose holder on record is assigned on no machine needs the whole mesh to be known, and it
|
||||
// passes to its only claimant here, in the composition that finds it.
|
||||
everyAssigned := map[string][]string{}
|
||||
for _, n := range nodes {
|
||||
if theirs, err := inv.Assigned(ctx, n.Name); err == nil && len(theirs) > 0 {
|
||||
everyAssigned[n.Name] = theirs
|
||||
}
|
||||
}
|
||||
holdings = theMeshPassesSeatsOn(ctx, inv, shelf, everyAssigned, holdings)
|
||||
for _, n := range nodes {
|
||||
if n.Name == exclude {
|
||||
continue
|
||||
}
|
||||
theirs, err := inv.Assigned(ctx, n.Name)
|
||||
if err != nil || len(theirs) == 0 {
|
||||
theirs := everyAssigned[n.Name]
|
||||
if len(theirs) == 0 {
|
||||
continue
|
||||
}
|
||||
caps, _ := inv.ProfileOf(ctx, n.Name)
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
// seatPassing is what passing seats on needs of the outside: the record's compare-and-set, and the hand act.
|
||||
type seatPassing struct {
|
||||
pass func(ctx context.Context, p catalogue.SeatPass) (bool, error)
|
||||
act func(ctx context.Context, p catalogue.SeatPass) error
|
||||
say io.Writer
|
||||
}
|
||||
|
||||
// passSeatsOn records, in the composition that finds it, the new holder of each seat held once whose holder on
|
||||
// record is assigned nowhere and which has exactly one claimant able to hold it, and writes the controller's own
|
||||
// hand act for it (novox/hq ADR 0449 §4, issue 1317). It answers the holdings as they are after it, so the
|
||||
// resolution that follows reads the record it just moved. A failed write is said and leaves the record as it was:
|
||||
// a composition is never refused for it.
|
||||
func passSeatsOn(ctx context.Context, sp seatPassing, holdings []catalogue.Held,
|
||||
assignments []catalogue.Assignment) []catalogue.Held {
|
||||
passes, said := catalogue.SeatsToPassOn(holdings, assignments)
|
||||
for _, line := range said {
|
||||
fmt.Fprintln(sp.say, line)
|
||||
}
|
||||
for _, p := range passes {
|
||||
moved, err := sp.pass(ctx, p)
|
||||
if err != nil {
|
||||
fmt.Fprintf(sp.say, "%s could not be passed from %s on %s to %s on %s: %v\n", p.Seat.Name,
|
||||
p.From.Module, p.From.Node, p.To.Module, p.To.Node, err)
|
||||
continue
|
||||
}
|
||||
if !moved {
|
||||
continue // another composition passed it first, and wrote the act
|
||||
}
|
||||
if err := sp.act(ctx, p); err != nil {
|
||||
fmt.Fprintf(sp.say, "%s passed from %s on %s to %s on %s, and the hand act could NOT be recorded: %v\n",
|
||||
p.Seat.Name, p.From.Module, p.From.Node, p.To.Module, p.To.Node, err)
|
||||
}
|
||||
var next []catalogue.Held
|
||||
for _, h := range holdings {
|
||||
if hs, ok := catalogue.SeatNamed(h.Claim); ok && hs.Name == p.Seat.Name {
|
||||
continue
|
||||
}
|
||||
next = append(next, h)
|
||||
}
|
||||
p.To.Site = p.From.Site
|
||||
holdings = append(next, p.To)
|
||||
}
|
||||
return holdings
|
||||
}
|
||||
|
||||
// seatPassHandAct is the controller's own entry for a pass: it is the act that puts the holder on record,
|
||||
// where a person's `seat --to` would have, so the record of who held what stays complete (ADR 0449 §4).
|
||||
func seatPassHandAct(p catalogue.SeatPass) link.HandAct {
|
||||
return link.HandAct{
|
||||
By: "the controller",
|
||||
Verb: "seat",
|
||||
Args: []string{p.Seat.Name, "--to", p.To.Node + "/" + p.To.Module, "--was", p.From.Node + "/" + p.From.Module},
|
||||
Why: fmt.Sprintf("%s on %s, the holder on record of %s, is assigned on no machine, and %s on %s is the only "+
|
||||
"assignment that claims it and can hold it (ADR 0449 §4)", p.From.Module, p.From.Node, p.Seat.Name,
|
||||
p.To.Module, p.To.Node),
|
||||
Cause: "seat-passed",
|
||||
Outcome: "done",
|
||||
}
|
||||
}
|
||||
|
||||
// theMeshPassesSeatsOn is passSeatsOn as the composition runs it: against the store's record, with the hand act
|
||||
// on the bus. Said to where the composition says its other lines (sayLeftOut).
|
||||
func theMeshPassesSeatsOn(ctx context.Context, inv *inventory.Inventory, shelf map[string]catalogue.Manifest,
|
||||
assigned map[string][]string, holdings []catalogue.Held) []catalogue.Held {
|
||||
var assignments []catalogue.Assignment
|
||||
for node, modules := range assigned {
|
||||
for _, module := range modules {
|
||||
if m, ok := shelf[module]; ok {
|
||||
assignments = append(assignments, catalogue.Assignment{Node: node, Manifest: m})
|
||||
}
|
||||
}
|
||||
}
|
||||
var w io.Writer = os.Stderr
|
||||
if lw, ok := ctx.Value(leftOutKey{}).(io.Writer); ok {
|
||||
w = lw
|
||||
}
|
||||
return passSeatsOn(ctx, seatPassing{
|
||||
say: w,
|
||||
pass: func(ctx context.Context, p catalogue.SeatPass) (bool, error) {
|
||||
return inv.PassSeat(ctx, p.Seat.Name, p.Seat.Scope, p.From, p.To)
|
||||
},
|
||||
act: func(ctx context.Context, p catalogue.SeatPass) error {
|
||||
return onTheBus(func(conn *nats.Conn) error {
|
||||
_, err := link.RecordHandAct(ctx, conn, seatPassHandAct(p))
|
||||
return err
|
||||
})
|
||||
},
|
||||
}, holdings, assignments)
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
func aBrokerClaiming(module string) catalogue.Manifest {
|
||||
return catalogue.Manifest{Module: module,
|
||||
Provides: []catalogue.Offer{{Name: "mesh-bus", Scope: catalogue.ScopeMesh}},
|
||||
Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh}}}
|
||||
}
|
||||
|
||||
func withBusSeat(t *testing.T) {
|
||||
t.Helper()
|
||||
was := catalogue.Seats()
|
||||
t.Cleanup(func() { catalogue.UseSeats(was) })
|
||||
catalogue.UseSeats([]catalogue.Seat{{Name: "mesh-broker", Scope: catalogue.ScopeMesh, Delivers: "mesh-bus", Decision: "test"}})
|
||||
}
|
||||
|
||||
// 1317/AC1: the composition records the claimant as the holder and writes one hand act by the controller naming
|
||||
// the seat, the former holder and the new one; the holdings it answers name the new holder.
|
||||
func TestACompositionPassesTheSeatAndWritesOneHandActByTheController(t *testing.T) {
|
||||
withBusSeat(t)
|
||||
var passed []catalogue.SeatPass
|
||||
var acts []link.HandAct
|
||||
var said bytes.Buffer
|
||||
sp := seatPassing{say: &said,
|
||||
pass: func(_ context.Context, p catalogue.SeatPass) (bool, error) {
|
||||
passed = append(passed, p)
|
||||
return true, nil
|
||||
},
|
||||
act: func(_ context.Context, p catalogue.SeatPass) error {
|
||||
acts = append(acts, seatPassHandAct(p))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
holdings := []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}}
|
||||
got := passSeatsOn(t.Context(), sp, holdings, []catalogue.Assignment{{Node: "novox", Manifest: aBrokerClaiming("gate")}})
|
||||
if len(got) != 1 || got[0].Module != "gate" || got[0].Node != "novox" {
|
||||
t.Fatalf("the holdings after the composition do not name the gate: %+v", got)
|
||||
}
|
||||
if len(passed) != 1 || len(acts) != 1 {
|
||||
t.Fatalf("passes=%d acts=%d, want one each", len(passed), len(acts))
|
||||
}
|
||||
a := acts[0]
|
||||
text := strings.Join(append([]string{a.By, a.Verb, a.Why}, a.Args...), " ")
|
||||
for _, want := range []string{"the controller", "mesh-broker", "novox/old-broker", "novox/gate"} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("the hand act does not name %q: %+v", want, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1317/AC2: two claimants, nothing recorded, the refusal said.
|
||||
func TestACompositionWithTwoClaimantsRecordsNoneAndSaysSo(t *testing.T) {
|
||||
withBusSeat(t)
|
||||
sp := seatPassing{say: &bytes.Buffer{},
|
||||
pass: func(context.Context, catalogue.SeatPass) (bool, error) {
|
||||
t.Fatal("a seat with two claimants was passed")
|
||||
return false, nil
|
||||
},
|
||||
act: func(context.Context, catalogue.SeatPass) error { t.Fatal("an act was written"); return nil },
|
||||
}
|
||||
var said bytes.Buffer
|
||||
sp.say = &said
|
||||
holdings := []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}}
|
||||
got := passSeatsOn(t.Context(), sp, holdings, []catalogue.Assignment{
|
||||
{Node: "novox", Manifest: aBrokerClaiming("gate")}, {Node: "ace", Manifest: aBrokerClaiming("other")}})
|
||||
if len(got) != 1 || got[0].Module != "old-broker" {
|
||||
t.Fatalf("the record changed: %+v", got)
|
||||
}
|
||||
if !strings.Contains(said.String(), "gate") || !strings.Contains(said.String(), "other") ||
|
||||
!strings.Contains(said.String(), "seat mesh-broker --to") {
|
||||
t.Fatalf("the refusal is not said with both and the remedy: %q", said.String())
|
||||
}
|
||||
}
|
||||
|
||||
// 1317/AC3 and AC4: nothing changes while the holder is assigned, nor with no claimant.
|
||||
func TestACompositionChangesNothingWhileTheHolderIsAssignedOrNobodyClaims(t *testing.T) {
|
||||
withBusSeat(t)
|
||||
sp := seatPassing{say: &bytes.Buffer{},
|
||||
pass: func(context.Context, catalogue.SeatPass) (bool, error) { t.Fatal("passed"); return false, nil },
|
||||
act: func(context.Context, catalogue.SeatPass) error { t.Fatal("act"); return nil },
|
||||
}
|
||||
holdings := []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}}
|
||||
got := passSeatsOn(t.Context(), sp, holdings, []catalogue.Assignment{
|
||||
{Node: "novox", Manifest: aBrokerClaiming("old-broker")}, {Node: "novox", Manifest: aBrokerClaiming("gate")}})
|
||||
if len(got) != 1 || got[0].Module != "old-broker" {
|
||||
t.Fatalf("holder assigned: record changed: %+v", got)
|
||||
}
|
||||
got = passSeatsOn(t.Context(), sp, holdings, nil)
|
||||
if len(got) != 1 || got[0].Module != "old-broker" {
|
||||
t.Fatalf("no claimant: record changed: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A composition that lost the compare-and-set writes no act, and a failed write leaves the record.
|
||||
func TestALostPassWritesNoActAndAFailedOneLeavesTheRecord(t *testing.T) {
|
||||
withBusSeat(t)
|
||||
holdings := []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}}
|
||||
as := []catalogue.Assignment{{Node: "novox", Manifest: aBrokerClaiming("gate")}}
|
||||
lost := seatPassing{say: &bytes.Buffer{},
|
||||
pass: func(context.Context, catalogue.SeatPass) (bool, error) { return false, nil },
|
||||
act: func(context.Context, catalogue.SeatPass) error { t.Fatal("act without a pass"); return nil },
|
||||
}
|
||||
passSeatsOn(t.Context(), lost, holdings, as)
|
||||
var said bytes.Buffer
|
||||
sp := seatPassing{say: &said,
|
||||
pass: func(context.Context, catalogue.SeatPass) (bool, error) { return false, context.DeadlineExceeded },
|
||||
act: func(context.Context, catalogue.SeatPass) error { t.Fatal("act"); return nil },
|
||||
}
|
||||
got := passSeatsOn(t.Context(), sp, holdings, as)
|
||||
if got[0].Module != "old-broker" || !strings.Contains(said.String(), "could not be passed") {
|
||||
t.Fatalf("got %+v said %q", got, said.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user