Keep a consumer bound where its data is; only a pin moves it (hq ADR 0232, issue 273)

Issue 258's fix let a mesh seat's holder elsewhere answer before this machine's own provider. Right
for the resolver, which any provider answers alike; for the store's seat it re-bound every database
consumer on a machine running its own store to the holder on another, each was given a fresh, empty
database there, and nothing said so for twenty hours.

- An offer says whether it keeps its consumers' data (`keeps-consumer-data`); unsaid, a provider
  that grants each consumer a credential does. For such a provision the seat's holder no longer
  overrules a provider beside the consumer; a pin still does.
- Where each such consumer was sent is recorded (migration 0071). A resolution that would bind it
  elsewhere keeps the recorded provider and says the move; one whose provider is gone is refused,
  never answered by another.
- A push says a kept move and raises it as an urgent condition at once; the self-check's D12 raises
  it every run, with a pinned move not yet sent as a warning and any unasked move as urgent.
This commit is contained in:
jochen
2026-10-06 15:19:23 +02:00
parent 4f4d365360
commit 8bfaf1523e
14 changed files with 1072 additions and 9 deletions
+48 -6
View File
@@ -54,6 +54,11 @@ type World struct {
// provider appears, and one recorded and then made unnecessary should not quietly stop applying
// either.
Pinned map[string]Chosen
// Bound is where each of this machine's consumers was bound for a provision that keeps its data
// (novox/hq ADR 0232), by consumer module then provision: the provider it was last sent, as the
// store recorded it. A resolution that would answer such a consumer from anywhere else keeps it
// where it is and says so (Resolution.Kept); only a pin naming the other provider moves it.
Bound map[string]map[string]Chosen
// Licences is every provision answered by a **record rather than a node**, by provision name.
//
// novox/hq ADR 0024: a hosted model is on nobody's machine and is reached over the public
@@ -151,6 +156,10 @@ type Resolution struct {
// 0207) — reported rather than refused while enforceSeatDependencies is off, so a node short of a
// holder still converges and `status` says what it is short of.
Unheld []Unheld
// Kept is every consumer this resolution would have moved to another provider of a provision
// that keeps its data, and did not (novox/hq ADR 0232): it stays bound where its data is, and the
// controller raises an urgent condition naming the move until a person pins one or the other.
Kept []KeptBinding
}
// Unhostable is one directly-assigned module the machine cannot run.
@@ -194,6 +203,13 @@ type Needed struct {
// state, not a consumer missing its key. Set by the plan, which is the only layer that knows a
// licence's manager; empty for every consumer.
Manager bool
// Module is the providing module on From, empty for a need answered by a record or by nothing
// that serves it. A provider is a (node, module) pair (novox/hq to-be 23), and a binding to data
// is a binding to the pair (ADR 0232).
Module string
// KeepsData is set when the provision keeps what its consumer writes (novox/hq ADR 0232): the
// binding is to the consumer's data, recorded when it is sent and moved only by a pin.
KeepsData bool
// Identity is the longest consumer identity the answering provision keeps (novox/hq ADR 0225),
// from the provider's own offer: what the mesh judges this consumer's identity against, on the
// consumer's side for `status` and on the provider's before it grants. No bound for a provision
@@ -229,10 +245,15 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
// one happened to answer it.
brokered := map[string]bool{}
local := map[string]bool{}
// And which of them keep their consumers' data (novox/hq ADR 0232) — also a property of the name.
keeps := map[string]bool{}
for _, m := range catalogue {
for _, o := range m.Provides {
if o.At() == ScopeMesh {
brokered[o.Name] = true
if m.KeepsConsumerData(o.Name) {
keeps[o.Name] = true
}
continue
}
local[o.Name] = true
@@ -346,7 +367,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
// trust boundary the moment both ends are containers**, and treating it as one gave the
// commonest arrangement of all — a service and its database on one node — the weakest
// handling, silently.
if satisfied[want] && !isModule(catalogue, want) && !answeredElsewhere(want, node, world, brokered) {
if satisfied[want] && !isModule(catalogue, want) && !answeredElsewhere(want, node, world, brokered, keeps) {
here := func(name string) bool { return chosen[name] || assignedHere[name] }
local := providersHere(catalogue, here, want)
// Which of them it matters to choose between. A plain capability — a shell, a display
@@ -406,7 +427,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
at = "127.0.0.1"
}
needs = append(needs, Needed{
Name: want, From: node.Name, At: at,
Name: want, From: node.Name, At: at, Module: by.Module,
Serves: servedByOne(by, want), For: because[want],
SharedOwn: sharedByOne(by, want), Identity: by.IdentityBoundOf(want)})
} else if served := servedByOne(by, want); len(served) > 0 {
@@ -428,7 +449,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
at = "127.0.0.1"
}
needs = append(needs, Needed{
Name: want, From: node.Name, At: at, Serves: served, For: because[want]})
Name: want, From: node.Name, At: at, Serves: served, For: because[want], Module: by.Module})
}
continue
}
@@ -459,7 +480,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
shared, _ = pm.SharedCredentialOf(want)
bound = pm.IdentityBoundOf(want)
}
needs = append(needs, Needed{Name: want, From: p.Node, At: p.At,
needs = append(needs, Needed{Name: want, From: p.Node, At: p.At, Module: p.Module,
Serves: p.Serves, For: because[want], SharedOwn: shared, Identity: bound})
}
switch {
@@ -619,6 +640,17 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
// The record pass below already gets this right and says so. It is the same rule.
needs = perConsumer(needs, order, catalogue)
// **A consumer bound to its data stays bound to it** (novox/hq ADR 0232). Per consumer, after
// the fan-out, because a binding is a consumer's: the walk above chose one provider per name for
// the whole machine, and two consumers of it may have been bound at different times.
var kept []KeptBinding
if !world.Unchecked {
here := func(name string) bool { return chosen[name] || assignedHere[name] }
var stuck []string
needs, kept, stuck = keepBound(needs, catalogue, node, world, keeps, here)
problems = append(problems, stuck...)
}
// What is answered by a record rather than by a machine.
//
// A post-pass, deliberately: nothing about it depends on the order requirements were walked
@@ -668,7 +700,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
resolution := Resolution{Node: node.Name, At: node.At, PublicDomain: node.PublicDomain,
Account: node.Account, AccountHome: node.AccountHome,
Because: because, Needs: needs, Unhostable: unhostable}
Because: because, Needs: needs, Unhostable: unhostable, Kept: kept}
for _, n := range providersFirst(order, catalogue) {
resolution.Modules = append(resolution.Modules, catalogue[n])
}
@@ -1215,13 +1247,23 @@ func machineReachRemedy(catalogue map[string]Manifest, want, node string) string
// overrule a pin somebody set on this machine.
//
// Not in the first pass, which asks only what this machine offers and has no providers to read.
func answeredElsewhere(want string, node Node, world World, brokered map[string]bool) bool {
//
// **And never by the seat alone for a provision that keeps its consumers' data** (novox/hq ADR
// 0232). The rule above was written for the mesh's resolver, which any provider answers alike. Read
// for the store's seat, it re-bound every database consumer on a machine running its own store to
// the seat's holder on another machine, which made each of them a fresh, empty database there and
// left their data behind (novox/hq issue 273). A provider beside a consumer of its data is where that
// data is; only a pin — a person — answers it from elsewhere.
func answeredElsewhere(want string, node Node, world World, brokered, keeps map[string]bool) bool {
if world.Unchecked || !brokered[want] {
return false
}
if c, pinned := world.Pinned[want]; pinned {
return c.Node != node.Name
}
if keeps[want] {
return false
}
// **A machine holding the seat answers itself** (novox/hq ADR 0223). With a replicated seat
// another machine holds it too, and the first holder in the providers' order may be that one; a
// holder is still where this machine's own requirement is answered, so its resolver file lists