diff --git a/go/caller_test.go b/go/caller_test.go index d08b88f..72e90b2 100644 --- a/go/caller_test.go +++ b/go/caller_test.go @@ -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) + } +} diff --git a/go/callers.go b/go/callers.go index 6fe6a49..2e0f5aa 100644 --- a/go/callers.go +++ b/go/callers.go @@ -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" {