From c16e6213e1e6edff5a4e60b68108c7ed04ab6275 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 18:05:16 +0100 Subject: [PATCH] Define the seat mesh-placement with its seven verbs, so placement has one contract whoever holds it (ADR 0567, issue 1126) --- go/catalogue/placement_seat.go | 74 ++++++++++++++++++++++++++++++++++ go/catalogue/seats.go | 3 ++ go/catalogue/seats_test.go | 10 ++++- 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 go/catalogue/placement_seat.go diff --git a/go/catalogue/placement_seat.go b/go/catalogue/placement_seat.go new file mode 100644 index 0000000..c400d08 --- /dev/null +++ b/go/catalogue/placement_seat.go @@ -0,0 +1,74 @@ +package catalogue + +// The seat of placement (novox/hq ADR 0567): which module runs on which node, with its host ports, bindings, the +// node's pins and holdings, the config placement and values, and what a person decides of the node, is state on the +// bus held by this seat, changed only by a proposal the nodes it touches answer and the operator approves. Held once +// for the mesh by the assigner (mesh-delivery-assigner), beside its wants. Its states are named after the holding +// module, as every module state is (delivery.PlacementBucket, PlacementProposalsBucket, PlacementCandidatesBucket); +// they are the seat's contract, so every reader takes their names from mesh-sdk, and a change of holder is a migration +// the new holder's record decides (ADR 0567 §1). Its events are the holder's too, carried on the holder's stream +// (delivery.PlacementStream): the names are in delivery (PlacementProposedEvent and the rest). + +// PlacementSeat is the seat the assigner holds for placement. +const PlacementSeat = "mesh-placement" + +// The seat's seven verbs (ADR 0567 §10). +const ( + PlacementVerbPlacement = "placement" + PlacementVerbProposals = "proposals" + PlacementVerbPropose = "propose" + PlacementVerbWithdraw = "withdraw" + PlacementVerbAgain = "again" + PlacementVerbAdopt = "adopt" + PlacementVerbHandBack = "hand-back" +) + +// placementVerbs are the seat's verbs: what reads all it keeps and does by hand all it does by itself (ADR 0567 §10). +// Optional while the holder that serves them is not yet on every node (design 33 §7): the seat is new in the same +// delivery as its holder. +func placementVerbs() []Verb { + changes := map[string]any{"type": "array", "minItems": 1, "description": "the changes, each an object with `op`: " + + "place (node, module, ports, bindings), take-off (node, module), pin (node, provision, provider {node, module}), " + + "unpin (node, provision), hold (node, module, claim, scope, kind, site), unhold (node, module, claim), decide " + + "(node, field, value: a decided field of the node, its address among them), config (node, module, mesh-wide, " + + "layer, mesh-wide-values, values)", + "items": map[string]any{"type": "object", "required": []string{"op", "node"}}} + return []Verb{ + {Name: PlacementVerbPlacement, Optional: true, Description: "Read placement: one node's key, the nodes one " + + "module is placed on, or every key, each with its revision, and how full the state is against its cap. With " + + "history, one node's or one module's changes in order: a revision the state keeps whole, an older one as the " + + "proposal's changes from its placement-changed event.", + Input: schema(map[string]string{"node": "one node", "module": "one module: the nodes it is placed on", + "history": "\"true\": the changes in order, from the stream and the state's revisions"}, nil, "history")}, + {Name: PlacementVerbProposals, Optional: true, Description: "Every open proposal of placement: its changes, " + + "the revision of each key it was laid over, its candidates, every node's answer, the differences computed and " + + "answered, its verdict and its ask. Narrowed to one by id.", + Input: schema(map[string]string{"id": "one proposal's id"}, nil)}, + {Name: PlacementVerbPropose, Optional: true, Description: "Propose a change of placement, with why: laid over " + + "the current keys and refused at once, naming every finding, when the result cannot be placed (two modules " + + "on one host port, a provider not placed, a seat held once held twice, two nodes with one address, two " + + "hubs, a module the indexer does not hold, a node with no record). Otherwise the nodes it touches compose " + + "it in shadow and answer, and on a passing verdict the operator is asked to approve it. Who proposes is the " + + "caller, as the runtime proves it.", + Input: map[string]any{"type": "object", "additionalProperties": false, "required": []string{"why", "changes"}, + "properties": map[string]any{ + "why": map[string]any{"type": "string", "description": "why, shown to the operator with the proposal"}, + "changes": changes, + }}}, + {Name: PlacementVerbWithdraw, Optional: true, Description: "Withdraw an open proposal: its key and its " + + "candidates are deleted and placement-withdrawn carries it whole. With why.", + Input: schema(map[string]string{"id": "the proposal's id", "why": "why, kept in the event"}, []string{"id", "why"})}, + {Name: PlacementVerbAgain, Optional: true, Description: "Judge an open proposal now with the answers that " + + "stand, and raise its ask again when its verdict passes and the operator's channel does not hold the ask. " + + "Nothing more: the recovery of a proposal whose judgement or ask was lost.", + Input: schema(map[string]string{"id": "the proposal's id"}, []string{"id"})}, + {Name: PlacementVerbAdopt, Optional: true, Description: "Take placement over from the controller: its " + + "placement-export is proposed whole as the seed, every node's key, which every node must answer as the same. " + + "Its Approve is the take-over; at the warrant the export is read again and a changed one is proposed anew.", + Input: schema(map[string]string{"why": "why, shown to the operator"}, []string{"why"})}, + {Name: PlacementVerbHandBack, Optional: true, Description: "The operator's alone, recorded as a hand act: " + + "delete every placement key, the history kept, so every node returns to the controller's compositions. " + + "Refused after the lease write.", + Input: schema(map[string]string{"why": "why, recorded with the hand act"}, []string{"why"})}, + } +} diff --git a/go/catalogue/seats.go b/go/catalogue/seats.go index 312c3b0..3f7690a 100644 --- a/go/catalogue/seats.go +++ b/go/catalogue/seats.go @@ -258,6 +258,9 @@ var defaultSeats = append([]Seat{ // machine, and a group of them, recorded and ordered by its holder, which asks the controller for each act. // What it says it says as its module (`mesh-delivery.transition`), through the runtime that launches it. {Name: DeliverySeat, Scope: ScopeMesh, Serves: deliveryVerbs(), Decision: "novox/hq ADR 0239"}, + // Placement (novox/hq ADR 0567): which module runs on which node, state on the bus held by the assigner, changed only by + // a proposal the nodes answer and the operator approves. Its states and events are the holder module's. + {Name: PlacementSeat, Scope: ScopeMesh, Serves: placementVerbs(), Decision: "novox/hq ADR 0567"}, // Deferred renames (novox/hq ADR 0121): these deliver a provision, so renaming them is a // delivering-seat migration with a mesh-wide cascade if a holder stops resolving mid-flight. // They keep their names until that migration is done deliberately, apart from the node-* pass. diff --git a/go/catalogue/seats_test.go b/go/catalogue/seats_test.go index 62043e1..6f58a11 100644 --- a/go/catalogue/seats_test.go +++ b/go/catalogue/seats_test.go @@ -1,6 +1,7 @@ package catalogue import ( + "reflect" "regexp" "strings" "testing" @@ -43,6 +44,11 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) { delivered[s.Delivers] = s.Name } } + // Forty-seven with mesh-placement (novox/hq ADR 0567), serving its seven verbs. + if s, ok := SeatNamed(PlacementSeat); !ok || s.Scope != ScopeMesh || !reflect.DeepEqual(VerbNames(s.Serves), + []string{"placement", "proposals", "propose", "withdraw", "again", "adopt", "hand-back"}) { + t.Errorf("the placement seat is %v, %v", ok, s) + } // Forty-six with the three build seats replacing node-build-agent and the retired mesh-build-machine (novox/hq // ADR 0525 §2, §10); forty-five with node-files (novox/hq ADR 0394); forty-four again once node-reads goes. // Forty-four with node-provisioner (novox/hq ADR 0349 clause 3); forty-three with mesh-provisioner (novox/hq ADR 0349 clause 3); forty-two with mesh-config (novox/hq ADR 0348 §9); forty-one with node-reads (novox/hq ADR 0310); forty with node-nfs-server and node-mounts (novox/hq ADR 0263); thirty-eight with node-resolver (novox/hq ADR 0247); thirty-seven with mesh-delivery (novox/hq ADR 0239); thirty-six since node-resolver-config retired @@ -56,8 +62,8 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) { // node-container-runtime (ADR 0207); nineteen with node-environment and node-login-shell (ADR 0203, // ADR 0204); seventeen with node-build-agent (ADR 0190). One fewer once the retired // mesh-build-machine row goes, when no registered manifest claims it. - if len(Seats()) != 46 { - t.Errorf("the mesh defines %d seats rather than 46; the set is closed, so a change here is "+ + if len(Seats()) != 47 { + t.Errorf("the mesh defines %d seats rather than 47; the set is closed, so a change here is "+ "a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames()) } }