Merge pull request 'Ask a failed delivery's Retry on the phone, tell a refused answer, and pass a seat to its only claimant (ADR 0449, ADR 0451)' (#402) from feat/no-terminal into main
This commit was merged in pull request #402.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// An assignment as the passing of a seat reads it: a module on a machine, with its manifest.
|
||||
type Assignment struct {
|
||||
Node string
|
||||
Manifest Manifest
|
||||
}
|
||||
|
||||
// SeatPass is one seat whose holder on record is assigned nowhere, and the only assignment that can take it.
|
||||
type SeatPass struct {
|
||||
Seat Seat
|
||||
From Held
|
||||
To Held
|
||||
}
|
||||
|
||||
// SeatsToPassOn reads the mesh for seats held once whose holder on record has left (novox/hq ADR 0449 §4).
|
||||
//
|
||||
// **A holder on record that is assigned on no machine holds nothing, and a seat that answers nothing is
|
||||
// the day a delivery removed its module.** Of such a seat, exactly one assignment in the mesh able to hold
|
||||
// it (it claims the seat at its scope and CanHold accepts it) takes it over: that is a pass, and the caller
|
||||
// records it and writes the hand act. With no such assignment the record stays; with two or more nothing is
|
||||
// recorded and the answer says so, naming both and the `seat --to` that settles it. A holder still assigned
|
||||
// anywhere changes nothing: the other claimants stand beside it, eligible and silent (ADR 0131). Only mesh
|
||||
// seats held once are read: a replicated seat's holders are added by an act (ADR 0223), a kinded bench is held
|
||||
// by its claims, and a seat with several holders on record is a store that disagrees with the mesh (refused
|
||||
// by the resolver).
|
||||
func SeatsToPassOn(holdings []Held, assignments []Assignment) ([]SeatPass, []string) {
|
||||
assigned := map[string]bool{}
|
||||
for _, a := range assignments {
|
||||
assigned[a.Manifest.Module] = true
|
||||
}
|
||||
bySeat := map[string][]Held{}
|
||||
var order []string
|
||||
for _, h := range holdings {
|
||||
s, ok := SeatNamed(h.Claim)
|
||||
if !ok || s.Replicated || s.Scope != ScopeMesh || h.Scope != ScopeMesh || KindedBenches[s.Name] {
|
||||
continue
|
||||
}
|
||||
if _, seen := bySeat[s.Name]; !seen {
|
||||
order = append(order, s.Name)
|
||||
}
|
||||
bySeat[s.Name] = append(bySeat[s.Name], h)
|
||||
}
|
||||
sort.Strings(order)
|
||||
|
||||
var passes []SeatPass
|
||||
var said []string
|
||||
for _, name := range order {
|
||||
recs := bySeat[name]
|
||||
if len(recs) != 1 || assigned[recs[0].Module] {
|
||||
continue
|
||||
}
|
||||
seat, _ := SeatNamed(name)
|
||||
var claimants []Held
|
||||
for _, a := range assignments {
|
||||
if CanHold(a.Manifest, seat) != nil {
|
||||
continue
|
||||
}
|
||||
claimants = append(claimants, Held{Claim: seat.Name, Scope: seat.Scope, Node: a.Node, Module: a.Manifest.Module})
|
||||
}
|
||||
sort.Slice(claimants, func(i, j int) bool {
|
||||
if claimants[i].Node != claimants[j].Node {
|
||||
return claimants[i].Node < claimants[j].Node
|
||||
}
|
||||
return claimants[i].Module < claimants[j].Module
|
||||
})
|
||||
switch len(claimants) {
|
||||
case 0:
|
||||
case 1:
|
||||
passes = append(passes, SeatPass{Seat: seat, From: recs[0], To: claimants[0]})
|
||||
default:
|
||||
said = append(said, fmt.Sprintf(
|
||||
"%s on %s is on record as the holder of %q and is assigned nowhere, and %s on %s and %s on %s both claim it "+
|
||||
"— nothing is recorded; `seat %s --to <node>/<module>` records the one that holds it",
|
||||
recs[0].Module, recs[0].Node, seat.Name, claimants[0].Module, claimants[0].Node,
|
||||
claimants[1].Module, claimants[1].Node, seat.Name))
|
||||
}
|
||||
}
|
||||
return passes, said
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// **A seat held once whose holder on record is assigned nowhere passes to its only claimant** (novox/hq ADR
|
||||
// 0449 §4, issue 1317).
|
||||
|
||||
func onRecord(node, module string) []Held {
|
||||
return []Held{{Claim: "mesh-broker", Scope: ScopeMesh, Node: node, Module: module}}
|
||||
}
|
||||
|
||||
// 1317/AC1
|
||||
func TestASeatWhoseHolderIsAssignedNowhereIsPassedToItsOnlyClaimant(t *testing.T) {
|
||||
busSeatDelivering(t, "mesh-bus")
|
||||
passes, said := SeatsToPassOn(onRecord("novox", "old-broker"),
|
||||
[]Assignment{{Node: "novox", Manifest: newBroker()}})
|
||||
if len(said) != 0 || len(passes) != 1 {
|
||||
t.Fatalf("the only claimant was not given the seat: passes=%v said=%v", passes, said)
|
||||
}
|
||||
p := passes[0]
|
||||
if p.Seat.Name != "mesh-broker" || p.From.Module != "old-broker" || p.From.Node != "novox" ||
|
||||
p.To.Module != "new-broker" || p.To.Node != "novox" {
|
||||
t.Fatalf("the pass does not name the seat, the former holder and the new one: %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
// 1317/AC2
|
||||
func TestWithTwoClaimantsAndTheHolderGoneNothingIsPassedAndTheRefusalNamesBoth(t *testing.T) {
|
||||
busSeatDelivering(t, "mesh-bus")
|
||||
passes, said := SeatsToPassOn(onRecord("novox", "gone"), []Assignment{
|
||||
{Node: "novox", Manifest: newBroker()}, {Node: "ace", Manifest: oldBroker()}})
|
||||
if len(passes) != 0 || len(said) != 1 {
|
||||
t.Fatalf("two claimants: passes=%v said=%v", passes, said)
|
||||
}
|
||||
for _, want := range []string{"old-broker", "new-broker", "seat mesh-broker --to"} {
|
||||
if !strings.Contains(said[0], want) {
|
||||
t.Fatalf("the refusal does not say %q: %s", want, said[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1317/AC3
|
||||
func TestAHolderStillAssignedSomewhereChangesNothing(t *testing.T) {
|
||||
busSeatDelivering(t, "mesh-bus")
|
||||
// Assigned on another machine than the record names: still assigned somewhere.
|
||||
passes, said := SeatsToPassOn(onRecord("novox", "old-broker"), []Assignment{
|
||||
{Node: "ace", Manifest: oldBroker()}, {Node: "novox", Manifest: newBroker()}})
|
||||
if len(passes) != 0 || len(said) != 0 {
|
||||
t.Fatalf("a holder still assigned lost its seat: passes=%v said=%v", passes, said)
|
||||
}
|
||||
}
|
||||
|
||||
// 1317/AC4
|
||||
func TestWithNoClaimantTheRecordStays(t *testing.T) {
|
||||
busSeatDelivering(t, "mesh-bus")
|
||||
passes, said := SeatsToPassOn(onRecord("novox", "old-broker"), []Assignment{
|
||||
{Node: "novox", Manifest: Manifest{Module: "other"}}})
|
||||
if len(passes) != 0 || len(said) != 0 {
|
||||
t.Fatalf("with no claimant: passes=%v said=%v", passes, said)
|
||||
}
|
||||
// A claimant that cannot hold the seat (it does not provide what the seat delivers) is no claimant.
|
||||
bad := Manifest{Module: "no-bus", Claims: []Claim{{Name: "mesh-broker", Scope: ScopeMesh}}}
|
||||
passes, said = SeatsToPassOn(onRecord("novox", "old-broker"), []Assignment{{Node: "novox", Manifest: bad}})
|
||||
if len(passes) != 0 || len(said) != 0 {
|
||||
t.Fatalf("a module that cannot hold the seat was passed it: passes=%v said=%v", passes, said)
|
||||
}
|
||||
}
|
||||
|
||||
// A replicated seat is never passed: its holders are added by an act (ADR 0223).
|
||||
func TestAReplicatedSeatIsNeverPassed(t *testing.T) {
|
||||
record := []Held{{Claim: "mesh-dns-resolver", Scope: ScopeMesh, Node: "novox", Module: "gone"}}
|
||||
resolver := Manifest{Module: "resolver", Provides: []Offer{{Name: "wildcard-resolution", Scope: ScopeMesh}},
|
||||
Claims: []Claim{{Name: "mesh-dns-resolver", Scope: ScopeMesh}}}
|
||||
passes, _ := SeatsToPassOn(record, []Assignment{{Node: "novox", Manifest: resolver}})
|
||||
if len(passes) != 0 {
|
||||
t.Fatalf("a replicated seat was passed: %v", passes)
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,7 @@ func (i *Inventory) UseChain(c *chain.Chain) {
|
||||
// beside the real one, as the merge gate raises the facts snapshot's mesh in a throwaway store (issue 1121). Nothing of it
|
||||
// reaches the bus.
|
||||
func (i *Inventory) UseThrowawayChain() {
|
||||
i.MarkThrowaway()
|
||||
i.UseChain(chain.InMemory(nil))
|
||||
i.markLaidOut()
|
||||
}
|
||||
@@ -129,3 +130,9 @@ func (i *Inventory) theChain(ctx context.Context) (*chain.Chain, error) {
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// MarkThrowaway says this inventory is a mesh raised for a moment: a composition on it writes nothing outside it.
|
||||
func (i *Inventory) MarkThrowaway() { i.throwaway = true }
|
||||
|
||||
// Throwaway says whether this inventory is a mesh raised for a moment (UseThrowawayChain).
|
||||
func (i *Inventory) Throwaway() bool { return i != nil && i.throwaway }
|
||||
|
||||
@@ -153,3 +153,24 @@ func TestAReplicatedSeatHasSeveralHoldersOnRecord(t *testing.T) {
|
||||
t.Fatalf("a handover left other holders on record: %+v", held)
|
||||
}
|
||||
}
|
||||
|
||||
// A pass is a compare-and-set: it moves the record only while the holder it was read with is still on it.
|
||||
func TestPassingASeatMovesTheRecordOnceAndOnlyFromTheHolderRead(t *testing.T) {
|
||||
inv, ctx := twoBrokersOnTwoNodes(t)
|
||||
if err := inv.HoldSeat(ctx, "mesh-broker", catalogue.ScopeMesh, "anchor", "old-broker"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
from := catalogue.Held{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "anchor", Module: "old-broker"}
|
||||
to := catalogue.Held{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "laptop", Module: "new-broker"}
|
||||
passed, err := inv.PassSeat(ctx, "mesh-broker", catalogue.ScopeMesh, from, to)
|
||||
if err != nil || !passed {
|
||||
t.Fatalf("the seat was not passed: %v %v", passed, err)
|
||||
}
|
||||
held, _ := inv.Holdings(ctx)
|
||||
if len(held) != 1 || held[0].Node != "laptop" || held[0].Module != "new-broker" {
|
||||
t.Fatalf("the record does not name the new holder alone: %+v", held)
|
||||
}
|
||||
if passed, err = inv.PassSeat(ctx, "mesh-broker", catalogue.ScopeMesh, from, to); err != nil || passed {
|
||||
t.Fatalf("a second pass from the same read moved the record again: %v %v", passed, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ type Inventory struct {
|
||||
state *chainState
|
||||
// recorded, when set, takes the durations a save of the chain measures in place of the store: a test with no store.
|
||||
recorded func(context.Context, Duration) error
|
||||
// throwaway is set on an inventory raised for a moment beside the real mesh (the merge gate's): nothing it
|
||||
// composes is to be written to the store or the bus.
|
||||
throwaway bool
|
||||
}
|
||||
|
||||
// Open connects to the inventory store.
|
||||
|
||||
@@ -284,6 +284,48 @@ func (i *Inventory) HoldSeat(ctx context.Context, seat, scope, nodeName, module
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
// PassSeat records `to` as the holder of a seat held once in place of `from`, only while `from` is still the
|
||||
// one on record: a compare-and-set in one transaction, so two compositions reading the same record cannot both
|
||||
// hand the seat over (novox/hq ADR 0449 §4). It answers false, with no error, when the record no longer names
|
||||
// `from` (another composition passed it first). The new holder must be an assignment.
|
||||
func (i *Inventory) PassSeat(ctx context.Context, seat, scope string, from, to catalogue.Held) (bool, error) {
|
||||
fromNode, err := i.NodeByName(ctx, from.Node)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
toNode, err := i.NodeByName(ctx, to.Node)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := i.mustBeAssigned(ctx, seat, to.Node, to.Module); err != nil {
|
||||
return false, err
|
||||
}
|
||||
tx, err := i.store.Pool().Begin(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer func() { _ = tx.Rollback(context.WithoutCancel(ctx)) }()
|
||||
gone, err := tx.Exec(ctx,
|
||||
`delete from seat_holding where seat = $1 and node = $2 and module = $3`,
|
||||
seat, fromNode.ID, from.Module)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("passing %s from %s on %s: %w", seat, from.Module, from.Node, err)
|
||||
}
|
||||
if gone.RowsAffected() == 0 {
|
||||
return false, nil
|
||||
}
|
||||
if _, err := tx.Exec(ctx,
|
||||
`delete from seat_holding where seat = $1`, seat); err != nil {
|
||||
return false, fmt.Errorf("passing %s: %w", seat, err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx,
|
||||
`insert into seat_holding (seat, scope, node, module) values ($1, $2, $3, $4)`,
|
||||
seat, scope, toNode.ID, to.Module); err != nil {
|
||||
return false, fmt.Errorf("recording %s on %s as the holder of %s: %w", to.Module, to.Node, seat, err)
|
||||
}
|
||||
return true, tx.Commit(ctx)
|
||||
}
|
||||
|
||||
// AddSeatHolder records one more assignment holding a replicated seat, beside those already on
|
||||
// record (novox/hq ADR 0223). Whether the seat may have several holders is the caller's to judge —
|
||||
// the seat's definition is compiled, and the store holds no column for it. Recording a holder
|
||||
|
||||
Reference in New Issue
Block a user