Hold a holder's caller list from its config watch, asked once, so a timed-out ask refuses no verb (issue 1966)
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -3,6 +3,7 @@ package stdio
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -183,3 +184,44 @@ func TestArgumentsOfNoCallHaveNoCaller(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"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
|
||||
@@ -122,6 +123,66 @@ func ConfiguredVerbCallers(key string, verbs []string, defaults VerbCallers) (Ve
|
||||
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) {
|
||||
var text string
|
||||
if err := json.Unmarshal(raw, &text); err == nil || len(bytes.TrimSpace(raw)) == 0 || string(bytes.TrimSpace(raw)) == "null" {
|
||||
|
||||
Reference in New Issue
Block a user