Take an older bus module's allowed as held, and name only remedies that work (review 31 of PR 188, hq issue 353)
mesh/merge-gate pass: builds mesh-controller → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered

After mesh-catalog #166 merges and before the bus upgrade, the bus's
machine cannot be pushed, so any change to the composed list made the
list check read behind for good: every ask was withheld and the condition
named a push that is refused. An older module's allowed now counts as
held, the unconfirmed reload said once; the worst case is one ask refused
in the seconds before a reload, which is asked again (issue 369).

The list check stays for a module that says not allowed or cannot answer,
and its remedy is the plan's own reason when the list cannot be composed,
the bus upgrade while sends there wait on it, and a push otherwise. An
in_force false is a wait only when the module gives both times; otherwise
what is not known is named.
This commit is contained in:
jochen
2026-10-12 13:21:32 +02:00
parent bcfcfaaaf3
commit f996cf7b6b
3 changed files with 189 additions and 96 deletions
+116 -62
View File
@@ -336,9 +336,9 @@ func composedGrantIn(open *stores) grantJudge {
}
}
// busGrantIn is the second fact, asked of the bus's own module on the machine holding the bus, with the
// judgement main made before it (novox/hq issue 353, PR 186: is the user list sent to the bus's machine the one
// composed now) as the floor for a bus module too old to say whether a grant is in force.
// busGrantIn is the second fact, asked of the bus's own module on the machine holding the bus, with the check
// main made before it (novox/hq issue 353, PR 186: is the user list sent to the bus's machine the one composed
// now) kept for the cases the module's word does not settle.
func busGrantIn(open *stores, conn *nats.Conn) grantJudge {
ask := func(ctx context.Context) (string, json.RawMessage, error) {
if open == nil || open.inventory == nil || conn == nil {
@@ -362,90 +362,144 @@ func busGrantIn(open *stores, conn *nats.Conn) grantJudge {
}
return h.Node, a.Result, nil
}
behind := func(ctx context.Context) (string, bool, error) {
list := func(ctx context.Context) (busListState, error) {
if open == nil || open.inventory == nil {
return "", false, errors.New("the mesh's records are not open")
return busListState{}, errors.New("the mesh's records are not open")
}
return brokerBehind(ctx, open, nil)
s, err := brokerListState(ctx, open, nil)
if err != nil || s.Machine == "" || !s.Behind {
return s, err
}
step, err := busHeld(ctx, open.inventory, []string{s.Machine})
if err != nil {
return busListState{}, err
}
s.BusStep = step[s.Machine]
return s, nil
}
return busGrantBy(ask, behind)
return busGrantBy(ask, list)
}
// busListState is the user list on the bus's machine as the controller's record knows it: whether the list
// sent there is behind the one composed now, why it could not be composed, and the bus step a send there waits
// on (that machine cannot be pushed until a person's `bus upgrade`).
type busListState struct {
Machine string
Behind bool
Uncomposed error
BusStep string
}
// remedy is the one thing that sends the bus's machine the list composed now: the plan's own reason when it
// cannot be composed, the bus upgrade while sends there wait on it, and otherwise a push of that machine.
func (s busListState) remedy(node string) string {
if s.Machine != "" {
node = s.Machine
}
switch {
case s.Uncomposed != nil:
return fmt.Sprintf("the user list for %s cannot be composed (%v); `plan %s` says what to fix first",
node, s.Uncomposed, node)
case s.BusStep != "":
return fmt.Sprintf("sends to %s wait on the bus's planned step, so a push will not carry it: %s", node, s.BusStep)
}
return fmt.Sprintf("`push %s` carries it", node)
}
// busGrantBy judges the bus's word, and names for each way it is not held the one thing that makes it held:
//
// - **the list there does not grant it**: `push <node>` sends the list composed now;
// - **written, not reloaded**: the bus reloads it in place within about 5 s, so the remedy is to wait;
// - **the module is older than `in_force`**: the bus cannot confirm a reload, so the floor is main's check
// before it — held while the list sent there is the one composed now, `push <node>` while it is behind, and
// the bus upgrade (which carries a module that answers `in_force`) when even that cannot be worked out. A
// bus module too old is not a reason to ask nothing: an order of merges that leaves it so would silence
// every ask until a person's bus upgrade (the review of PR 188).
// - **in force**: held;
// - **allowed, from a module older than `in_force`**: held, with the caveat that the reload is not confirmed.
// The file allows the publish, and the bus reloads within about 5 s of a change to it, so the worst case is
// one ask refused in those seconds, which is asked again (issue 369). The list check is not consulted:
// while a bus step is pending the bus's machine cannot be pushed, so any change to the composed list would
// read as behind for good and silence every ask until a person's bus upgrade (the review of PR 188);
// - **allowed, written, not reloaded** (the module gives both times): wait about 5 s;
// - **allowed, in force not known** (the module could not read a time): what is not known, said;
// - **not allowed, or the module cannot answer**: the list check decides — the remedy that sends the list
// when it is behind (busListState.remedy), and, for a module that cannot answer, held while the list sent
// is current, as main judged it.
//
// ask answers the machine holding the bus and its module's answer ("" when nothing holds the bus); behind is
// whether the list sent to that machine is behind what the mesh composes.
// ask answers the machine holding the bus and its module's answer ("" when nothing holds the bus); list is the
// record's word on the list sent to that machine.
func busGrantBy(ask func(ctx context.Context) (string, json.RawMessage, error),
behind func(ctx context.Context) (string, bool, error)) grantJudge {
list func(ctx context.Context) (busListState, error)) grantJudge {
return func(ctx context.Context) (bool, string, error) {
node, raw, err := ask(ctx)
if err != nil {
return false, "", err
}
if node == "" {
node, raw, askErr := ask(ctx)
if askErr == nil && node == "" {
return false, "no module holds the bus, so nothing says what it enforces", nil
}
answer, err := readBusAnswer(raw)
if err != nil {
return false, "", fmt.Errorf("the bus module's answer on %s could not be read: %w", node, err)
var answer busAnswer
if askErr == nil {
if answer, askErr = readBusAnswer(raw); askErr != nil {
askErr = fmt.Errorf("its answer could not be read: %w", askErr)
}
}
pushIt := func(what string) string {
return fmt.Sprintf("%s, so the bus has not been given the controller's grant to ask; `push %s` carries it",
what, node)
if askErr != nil {
if node == "" {
node = "the bus's machine"
}
s, err := list(ctx)
if err != nil {
return false, "", fmt.Errorf("the bus module on %s could not answer (%v), and whether the list sent "+
"there is the one composed now could not be worked out: %w", node, askErr, err)
}
if s.Behind {
return false, fmt.Sprintf("the bus module on %s could not answer (%v), and the user list sent there is "+
"behind what the mesh composes; %s", node, askErr, s.remedy(node)), nil
}
return true, fmt.Sprintf("the bus module on %s could not answer (%v); judged by the user list sent there, "+
"which is the one composed now", node, askErr), nil
}
notAllowed := func(older string) (bool, string, error) {
s, err := list(ctx)
if err != nil {
return false, "", fmt.Errorf("the user list on %s does not allow the controller to ask%s, and whether "+
"the list sent there is the one composed now could not be worked out: %w", node, older, err)
}
if s.Behind {
return false, fmt.Sprintf("the user list on %s does not allow the controller to ask%s: it is behind "+
"what the mesh composes; %s", node, older, s.remedy(node)), nil
}
return false, fmt.Sprintf("the user list on %s does not allow the controller to ask%s, though the list "+
"sent there is the one composed now; %s again", node, older, s.remedy(node)), nil
}
switch {
case answer.InForce == nil && answer.Allowed:
return true, fmt.Sprintf("the user list on %s allows the controller to ask, but its reload is not "+
"confirmed: the bus module there is older than the answer whether a grant is in force, and the bus "+
"upgrade (`bus upgrade`) carries one that confirms it", node), nil
case answer.InForce == nil:
const older = "the bus module there is older than the answer whether a grant is in force, so its " +
"reload is not confirmed"
if !answer.Allowed {
return false, pushIt(fmt.Sprintf("the user list on %s does not allow the controller to ask", node)) +
" (" + older + ")", nil
}
machine, late, err := behind(ctx)
if err != nil {
return false, "", fmt.Errorf("%s, and whether the list sent to %s is the one composed now could "+
"not be worked out (%v); the bus upgrade (`bus upgrade`) carries a module that says", older,
node, err)
}
if late {
if machine == "" {
machine = node
}
return false, fmt.Sprintf("the bus's user list on %s is behind what the mesh composes, so the bus has "+
"not been given the controller's grant to ask; `push %s` carries it (%s)", machine, machine, older), nil
}
return true, fmt.Sprintf("the user list on %s is the one composed now and allows the controller to ask; "+
"%s, and the bus upgrade (`bus upgrade`) carries a module that confirms it", node, older), nil
case !*answer.InForce && answer.Allowed:
why := "the server has not reloaded since it was written"
if answer.Why != "" {
why = answer.Why
}
return false, fmt.Sprintf("the bus on %s has the controller's grant to ask written but not in force yet "+
"(%s); the bus reloads it in place within about 5 s, so nothing is to be done but wait", node, why), nil
return notAllowed(" (the bus module there is older than the answer whether a grant is in force)")
case !answer.Allowed:
return false, pushIt(fmt.Sprintf("the user list on %s does not allow the controller to ask", node)), nil
return notAllowed("")
case *answer.InForce:
return true, "", nil
case answer.FileWritten != "" && answer.ServerLoaded != "":
return false, fmt.Sprintf("the bus on %s has the controller's grant to ask written (%s) but has not "+
"reloaded since (last load %s); it reloads in place within about 5 s, so nothing is to be done but wait",
node, answer.FileWritten, answer.ServerLoaded), nil
}
return true, "", nil
why := answer.Why
if why == "" {
why = "it gives no reason"
}
return false, fmt.Sprintf("the user list on %s allows the controller to ask, but whether the bus enforces it "+
"is not known: %s", node, why), nil
}
}
// busUserCanTool is the bus module's answer to whether a user may do something, and whether it is in force.
const busUserCanTool = "nats_user_can"
// busAnswer is the bus module's answer to busUserCanTool; InForce is nil from a module older than that field.
// busAnswer is the bus module's answer to busUserCanTool; InForce is nil from a module older than that field,
// and FileWritten and ServerLoaded are given only when it could read both times (mesh-catalog's nats InForce).
type busAnswer struct {
Allowed bool `json:"allowed"`
InForce *bool `json:"in_force"`
Why string `json:"why"`
Allowed bool `json:"allowed"`
InForce *bool `json:"in_force"`
Why string `json:"why"`
FileWritten string `json:"file_written"`
ServerLoaded string `json:"server_loaded"`
}
func readBusAnswer(raw json.RawMessage) (busAnswer, error) {