Merge pull request 'The store seat promises two verbs, databases and query (ADR 0159)' (#186) from feat/the-store-seat-has-verbs into main
This commit was merged in pull request #186.
This commit is contained in:
@@ -716,7 +716,9 @@ func claimsFor(ctx context.Context, inv *inventory.Inventory, m catalogue.Manife
|
||||
claimed := seatClaimed{Seat: c.Name, Scope: c.At()}
|
||||
if s, known := byName[c.Name]; known {
|
||||
claimed.Scope = s.Scope
|
||||
claimed.Serves = catalogue.VerbNames(s.Serves)
|
||||
// The verbs the runtime serves for the seat: the claim's own when it names them
|
||||
// (ADR 0160), else every verb the seat promises, which its tools then answer.
|
||||
claimed.Serves = c.ServesFor(catalogue.Manifest{Tools: catalogue.VerbNames(s.Serves)})
|
||||
}
|
||||
out = append(out, claimed)
|
||||
}
|
||||
|
||||
@@ -107,6 +107,27 @@ func TestCanHoldJudgesClaimScopeAndWhatTheSeatDelivers(t *testing.T) {
|
||||
if err := CanHold(cannotAnswer, seat); err == nil || !strings.Contains(err.Error(), `does not provide "mesh-bus"`) {
|
||||
t.Fatalf("a holder that cannot answer for the seat was allowed: %v", err)
|
||||
}
|
||||
// A holder's own tools need not be the seat's verbs: the claim may name what it serves for the
|
||||
// role (ADR 0160), and then only those count — and only the seat's verbs may be named.
|
||||
promising := seat
|
||||
promising.Serves = []Verb{{Name: "databases"}, {Name: "query"}}
|
||||
engine := newBroker()
|
||||
engine.Tools = []string{"engine_list_databases", "engine_query"}
|
||||
if err := CanHold(engine, promising); err == nil || !strings.Contains(err.Error(), "does not serve databases, query") {
|
||||
t.Fatalf("a holder whose tools are not the seat's verbs was allowed without saying what it serves: %v", err)
|
||||
}
|
||||
engine.Claims[0].Serves = []string{"databases", "query"}
|
||||
if err := CanHold(engine, promising); err != nil {
|
||||
t.Fatalf("a claim naming the seat's verbs was refused: %v", err)
|
||||
}
|
||||
engine.Claims[0].Serves = []string{"databases"}
|
||||
if err := CanHold(engine, promising); err == nil || !strings.Contains(err.Error(), "does not serve query") {
|
||||
t.Fatalf("a claim naming half the verbs was allowed: %v", err)
|
||||
}
|
||||
engine.Claims[0].Serves = []string{"databases", "query", "engine_query"}
|
||||
if err := CanHold(engine, promising); err == nil || !strings.Contains(err.Error(), "does not promise") {
|
||||
t.Fatalf("a claim naming a verb the seat never promised was allowed: %v", err)
|
||||
}
|
||||
// And the judgement follows the store's row, not a compiled copy.
|
||||
busSeatDelivering(t, "amqp")
|
||||
seat, _ = SeatNamed("mesh-broker")
|
||||
|
||||
@@ -52,6 +52,22 @@ type Claim struct {
|
||||
Name string `json:"name"`
|
||||
// Scope defaults to the node, which is where nearly everything singular is singular.
|
||||
Scope string `json:"scope,omitempty"`
|
||||
// Serves names the seat's verbs this module implements for the role, when its own tools are
|
||||
// not the seat's (novox/hq ADR 0159, 0160): the store's `databases` is not postgres's
|
||||
// `postgres_list_databases`, and a holder may well serve both. The runtime serves an
|
||||
// implementation registered under the seat's name on the seat's subjects. Absent, the
|
||||
// module's own `tools` must list every verb the seat promises, which is how a module named
|
||||
// like its seat — the catalogue, the records — says they are one and the same.
|
||||
Serves []string `json:"serves,omitempty"`
|
||||
}
|
||||
|
||||
// ServesFor is what this claim offers a seat's protocol: the verbs it names, else the module's
|
||||
// own tools.
|
||||
func (c Claim) ServesFor(m Manifest) []string {
|
||||
if len(c.Serves) > 0 {
|
||||
return c.Serves
|
||||
}
|
||||
return m.Tools
|
||||
}
|
||||
|
||||
// At is this claim's scope, with the default applied.
|
||||
|
||||
@@ -59,7 +59,16 @@ var defaultSeats = []Seat{
|
||||
{Name: ControllerSeatName, Scope: ScopeMesh, Decision: "novox/hq ADR 0079",
|
||||
Emits: []string{"applied", "refused", "built-before"},
|
||||
Serves: ControllerVerbs},
|
||||
{Name: "mesh-store", Scope: ScopeMesh, Delivers: "postgres-database", Decision: "novox/hq ADR 0079"},
|
||||
// The store's first verbs (novox/hq ADR 0159): the smallest set that makes the store askable,
|
||||
// served by whichever module holds the seat with tools of these names.
|
||||
{Name: "mesh-store", Scope: ScopeMesh, Delivers: "postgres-database", Decision: "novox/hq ADR 0079",
|
||||
Serves: []Verb{
|
||||
{Name: "databases", Description: "Every database the store holds, with its on-disk size.",
|
||||
Input: schema(map[string]string{}, nil)},
|
||||
{Name: "query", Description: "One read-only statement against one database the store holds.",
|
||||
Input: schema(map[string]string{"database": "the database to query", "sql": "the read-only statement"},
|
||||
[]string{"database", "sql"})},
|
||||
}},
|
||||
// **Delivers the mesh's own bus, not `amqp`.** Those were the same word until
|
||||
// ADR 0127 separated them: `amqp` is a backing service a module may require, and this seat is
|
||||
// the mesh's own transport. ADR 0128 then made that connection something a module requires
|
||||
@@ -286,11 +295,19 @@ func CanHold(m Manifest, seat Seat) error {
|
||||
// **Serving the seat's tools is a condition of holding it** (novox/hq ADR 0132). A holder that
|
||||
// does not answer what the role promises is every caller's timeout, found at registration and
|
||||
// at handover instead, naming the verbs rather than the fact that something is missing.
|
||||
if missing := unservedVerbs(m.Tools, seat.Serves); len(missing) > 0 {
|
||||
if missing := unservedVerbs(claimed.ServesFor(m), seat.Serves); len(missing) > 0 {
|
||||
return fmt.Errorf("%s claims %s but does not serve %s, which that seat's protocol promises "+
|
||||
"(novox/hq ADR 0132) — a holder lists every verb its seat declares under tools",
|
||||
"(novox/hq ADR 0132) — a holder names every verb its seat declares, under the claim's "+
|
||||
"serves or among its own tools",
|
||||
m.Module, seat.Name, strings.Join(missing, ", "))
|
||||
}
|
||||
// And nothing the seat does not promise: a verb named here that the protocol lacks is served
|
||||
// to nobody, which is a typo the holder would otherwise discover as a caller's timeout.
|
||||
if extra := unpromised(claimed.Serves, seat.Serves); len(extra) > 0 {
|
||||
return fmt.Errorf("%s claims %s and says it serves %s, which that seat's protocol does not "+
|
||||
"promise — a claim's serves names the seat's verbs and nothing else",
|
||||
m.Module, seat.Name, strings.Join(extra, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ func CatalogueProblems(shelf Shelf) []string {
|
||||
}
|
||||
// A holder that does not answer what the seat promises is a caller's timeout, found
|
||||
// at assignment instead.
|
||||
if missing := unserved(m, s); len(missing) > 0 {
|
||||
if missing := unserved(m, c, s); len(missing) > 0 {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s claims %s but does not serve %s, which that seat's protocol promises",
|
||||
module, c.Name, strings.Join(missing, ", ")))
|
||||
@@ -221,10 +221,10 @@ func CatalogueProblems(shelf Shelf) []string {
|
||||
|
||||
// unserved is what a seat's protocol promises and the claimant does not answer. Only the tools
|
||||
// are checked: `accepts` and `emits` are wired by the runtime from the declaration, while a tool
|
||||
// is code the module either has or has not written.
|
||||
func unserved(m Manifest, s SeatDeclaration) []string {
|
||||
// is code the module either has or has not written — under the claim's serves, or among its own.
|
||||
func unserved(m Manifest, c Claim, s SeatDeclaration) []string {
|
||||
has := map[string]bool{}
|
||||
for _, t := range m.Tools {
|
||||
for _, t := range c.ServesFor(m) {
|
||||
has[t] = true
|
||||
}
|
||||
var missing []string
|
||||
|
||||
@@ -70,6 +70,22 @@ func TestAHolderMustServeWhatItsSeatPromises(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The claim may say what it serves for the role instead, when the module's own tools are not the
|
||||
// seat's verbs (ADR 0160).
|
||||
func TestAClaimMayNameWhatItServesForTheSeat(t *testing.T) {
|
||||
m := telegram()
|
||||
m.Tools = []string{"telegram_send"}
|
||||
m.Claims = append([]Claim(nil), m.Claims...)
|
||||
for i := range m.Claims {
|
||||
if m.Claims[i].Name == "telegram-sender" {
|
||||
m.Claims[i].Serves = []string{"status"}
|
||||
}
|
||||
}
|
||||
if got := problemsFor(t, Shelf{"telegram": m}); strings.Contains(got, "does not serve") {
|
||||
t.Fatalf("a claim naming the seat's verb was refused: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A seat with no protocol is a marker: which module is this node's showcase, or its packet filter.
|
||||
// Most node-scoped seats are markers, so refusing one would refuse the majority of the set.
|
||||
func TestASeatWithoutAProtocolIsAMarkerNotAMistake(t *testing.T) {
|
||||
|
||||
@@ -133,7 +133,22 @@ func schema(properties map[string]string, required []string) map[string]any {
|
||||
return out
|
||||
}
|
||||
|
||||
// unservedVerbs is what a seat promises and a claimant's `tools` does not answer.
|
||||
// unpromised is what a claim says it serves and the seat's protocol never promised.
|
||||
func unpromised(serves []string, promised []Verb) []string {
|
||||
has := map[string]bool{}
|
||||
for _, v := range promised {
|
||||
has[v.Name] = true
|
||||
}
|
||||
var extra []string
|
||||
for _, s := range serves {
|
||||
if !has[s] {
|
||||
extra = append(extra, s)
|
||||
}
|
||||
}
|
||||
return extra
|
||||
}
|
||||
|
||||
// unservedVerbs is what a seat promises and a claimant's offer for it does not answer.
|
||||
func unservedVerbs(tools []string, promised []Verb) []string {
|
||||
has := map[string]bool{}
|
||||
for _, t := range tools {
|
||||
|
||||
Reference in New Issue
Block a user