Merge pull request 'Hold a holder's caller list from its config watch, so a timed-out ask refuses no verb (issue 1966; group with mesh-catalog, merge first)' (#182) from fix/1966-callers-config-read into main

This commit was merged in pull request #182.
This commit is contained in:
2026-11-09 22:00:44 +00:00
2 changed files with 103 additions and 0 deletions
+42
View File
@@ -3,6 +3,7 @@ package stdio
import ( import (
"bufio" "bufio"
"encoding/json" "encoding/json"
"errors"
"io" "io"
"testing" "testing"
"time" "time"
@@ -183,3 +184,44 @@ func TestArgumentsOfNoCallHaveNoCaller(t *testing.T) {
t.Fatalf("CallerOf(nil) answered %q", got) t.Fatalf("CallerOf(nil) answered %q", got)
} }
} }
// A holder's caller list is held (issue 1966): the runtime is asked once, by a watch; a later call never asks again, so
// an ask that timed out refuses no verb, and a change that cannot be read keeps the list last read.
func TestAHeldCallerListIsAskedOnceAndSurvivesAnUnreadableChange(t *testing.T) {
verbs, defaults := []string{"publish"}, VerbCallers{"publish": {"a"}}
var asks int
var push func(ProvisionChange) error
fail := true
read := holdVerbCallers(func(key string, h func(ProvisionChange) error) error {
asks++
if fail {
return errors.New("nats: timeout")
}
push = h
return h(ProvisionChange{Kind: "config", Key: key, Value: json.RawMessage(`{"publish":["b"]}`)})
}, "callers", verbs, defaults)
if _, err := read(); err == nil {
t.Fatal("a list never read was answered")
}
fail = false
c, err := read()
if err != nil || c.Allowed("publish", "ace.b") != nil || asks != 2 {
t.Fatalf("after the ask answered: %v, %v, asked %d times", c, err, asks)
}
for i := 0; i < 3; i++ {
if _, err := read(); err != nil {
t.Fatal(err)
}
}
if asks != 2 {
t.Errorf("a held list was asked again: %d asks", asks)
}
_ = push(ProvisionChange{Kind: "config", Key: "callers", Value: json.RawMessage(`{"nothing":["x"]}`)})
if c, err := read(); err != nil || c.Allowed("publish", "ace.b") != nil {
t.Errorf("an unreadable change dropped the list last read: %v, %v", c, err)
}
_ = push(ProvisionChange{Kind: "config", Key: "callers", Value: json.RawMessage(`{"publish":["c"]}`)})
if c, _ := read(); c.Allowed("publish", "ace.c") != nil || c.Allowed("publish", "ace.b") == nil {
t.Errorf("a change was not held: %v", c)
}
}
+61
View File
@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"slices" "slices"
"strings" "strings"
"sync"
) )
// Callers is a module's trusted list of who may call a verb of its (novox/hq ADR 0459 §2, ADR 0563 §8), as the mesh // Callers is a module's trusted list of who may call a verb of its (novox/hq ADR 0459 §2, ADR 0563 §8), as the mesh
@@ -122,6 +123,66 @@ func ConfiguredVerbCallers(key string, verbs []string, defaults VerbCallers) (Ve
return parseConfiguredVerbCallers(raw, verbs, defaults) return parseConfiguredVerbCallers(raw, verbs, defaults)
} }
// HoldVerbCallers is ConfiguredVerbCallers held: the returned reader asks the runtime once, by a watch of the config
// value key (WatchConfig), and from then on answers the list it holds, which each change of the value replaces. A call
// never asks the runtime again, so a runtime that is slow or away for a moment (a timed-out ask) refuses no verb; a
// change that cannot be read keeps the list last read and is said on the bundle's standard error, so a broken list is
// said and never widened. Only a reader that has not yet read any value answers an error, and its next call asks again.
func HoldVerbCallers(key string, verbs []string, defaults VerbCallers) func() (VerbCallers, error) {
return holdVerbCallers(WatchConfig, key, verbs, defaults)
}
func holdVerbCallers(watch func(string, func(ProvisionChange) error) error, key string, verbs []string, defaults VerbCallers) func() (VerbCallers, error) {
var start, mu sync.Mutex
var held VerbCallers
var bad error
handle := func(c ProvisionChange) error {
mu.Lock()
defer mu.Unlock()
if c.Refused != "" {
bad = fmt.Errorf("the config value %s is not answered: %s", key, c.Refused)
} else if l, err := parseConfiguredVerbCallers(c.Value, verbs, defaults); err != nil {
bad = fmt.Errorf("the config value %s cannot be read: %w", key, err)
} else {
held, bad = l, nil
return nil
}
if held != nil {
sayf("%v: the list last read stays", bad)
}
return nil
}
read := func() (VerbCallers, error) {
mu.Lock()
defer mu.Unlock()
if held != nil {
return held, nil
}
if bad != nil {
return nil, bad
}
return nil, nil
}
return func() (VerbCallers, error) {
if l, err := read(); l != nil || err != nil {
return l, err
}
start.Lock()
defer start.Unlock()
if l, err := read(); l != nil || err != nil {
return l, err
}
if err := watch(key, handle); err != nil {
return nil, fmt.Errorf("the config value %s cannot be read: %w", key, err)
}
l, err := read()
if l == nil && err == nil {
err = fmt.Errorf("the config value %s cannot be read: the runtime handed over no value", key)
}
return l, err
}
}
func parseConfiguredVerbCallers(raw json.RawMessage, verbs []string, defaults VerbCallers) (VerbCallers, error) { func parseConfiguredVerbCallers(raw json.RawMessage, verbs []string, defaults VerbCallers) (VerbCallers, error) {
var text string var text string
if err := json.Unmarshal(raw, &text); err == nil || len(bytes.TrimSpace(raw)) == 0 || string(bytes.TrimSpace(raw)) == "null" { if err := json.Unmarshal(raw, &text); err == nil || len(bytes.TrimSpace(raw)) == 0 || string(bytes.TrimSpace(raw)) == "null" {