The provisioner's watches of gitea.bound, mesh-vault.bound and postgres.bound failed every minute for an hour on
the control-node ("the current values ... did not arrive in 10s") and 1257 changes were refused as "names no
watch of this bundle". Both sides were at fault. The SDK answered a change only once its handler returned, so a
slow handler (Rejudge waits on the lock its composition holds) counted against the runtime's 10 s bound on the
current values; and when the runtime gave up, the SDK forgot the id at once. The runtime (mesh-tools node-tools
internal/bus/state.go StateWatch) stops its watcher on that bound but still drains and delivers the values already
buffered, to an id nobody holds.
Quick fix: a longer runtime bound. Thorough fix here: each state and provision watch has a follower that takes a
change in order and answers at once, its handler on its own goroutine; Watch still returns only once the current
values were handled, and a handler's error is said on standard error. A watch the runtime refuses is said given up
and kept by id, so a change still handed to it is refused saying so. KeptState.Follow asks again with backoff until
taken, the loop four modules wrote for themselves. A log watch keeps its answer after the handler: that is its
back-pressure.
96 lines
3.9 KiB
Go
96 lines
3.9 KiB
Go
package stdio
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// A module's code reads its values at runtime (novox/hq ADR 0349 clause 14, ADR 0370, and the part of the feature 57 design that lets a running module read, rather than be handed in a file, its provisioned values): its config
|
|
// values and its bindings' values that are not secret, for what its manifest declares, and watches them. The
|
|
// runtime that launched this bundle asks its machine's provisioner client **as this bundle's module** — the module it
|
|
// launched, never a name this code passes — and the client answers only what that module declares, only while the
|
|
// mesh places it on that machine. A secret is refused while the module's code runs in the tool runner: the vault
|
|
// hands a secret only to a caller the bus proves, and the bus proves only the tool runner there, so a module's
|
|
// secrets reach it through its files until it runs under a bus account of its own (novox/hq issue 569).
|
|
|
|
// ProvisionChange is what a watch hands over: the value now, or — once it is no longer answered, the module taken
|
|
// off the machine or the key no longer declared — Refused, why.
|
|
type ProvisionChange struct {
|
|
Kind string `json:"kind"`
|
|
Key string `json:"key"`
|
|
Value json.RawMessage `json:"value,omitempty"`
|
|
Refused string `json:"refused,omitempty"`
|
|
}
|
|
|
|
const provisionTimeout = 30 * time.Second
|
|
|
|
func provisionGet(kind, key string) (json.RawMessage, error) {
|
|
c, err := live()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.ask("mesh/provision.get", map[string]any{"kind": kind, "key": key}, provisionTimeout)
|
|
}
|
|
|
|
func provisionWatch(kind, key string, handle func(ProvisionChange) error) error {
|
|
if handle == nil {
|
|
return errors.New("a watch needs a handler")
|
|
}
|
|
c, err := live()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id := c.newID()
|
|
f := newFollower("the watch "+id+" of the "+kind+" "+key, handle, func(ch ProvisionChange) string { return ch.Kind + " " + ch.Key })
|
|
c.mu.Lock()
|
|
c.provisioned[id] = f
|
|
c.mu.Unlock()
|
|
if _, err := c.askAs(id, "mesh/provision.watch", map[string]any{"kind": kind, "key": key}, provisionTimeout); err != nil {
|
|
c.mu.Lock()
|
|
delete(c.provisioned, id)
|
|
c.gaveUp(id, fmt.Sprintf("the %s %s at %s: %v", kind, key, time.Now().Format(time.RFC3339), err))
|
|
c.mu.Unlock()
|
|
sayf("the watch %s of the %s %s is given up: %v", id, kind, key, err)
|
|
return err
|
|
}
|
|
<-f.handled()
|
|
return nil
|
|
}
|
|
|
|
// Config is one of this module's config values as mesh-config resolved it for this machine, as JSON: the key as
|
|
// its manifest declares it under `config`. A key it does not declare is refused, with the reason.
|
|
func Config(key string) (json.RawMessage, error) { return provisionGet("config", key) }
|
|
|
|
// WatchConfig hands handle the config value now and then every change, in order, and returns once the value now is
|
|
// handled. The watch lasts as long as the process.
|
|
func WatchConfig(key string, handle func(ProvisionChange) error) error {
|
|
return provisionWatch("config", key, handle)
|
|
}
|
|
|
|
// Bound is one value, not a secret, of what this module is bound to: the provision as its manifest requires it,
|
|
// and the key its provider answers (`${bound:<provision>:<key>}`).
|
|
func Bound(provision, key string) (json.RawMessage, error) {
|
|
return provisionGet("bound", provision+":"+key)
|
|
}
|
|
|
|
// WatchBound is WatchConfig for a bound value.
|
|
func WatchBound(provision, key string, handle func(ProvisionChange) error) error {
|
|
return provisionWatch("bound", provision+":"+key, handle)
|
|
}
|
|
|
|
// Secret is one of this module's secrets. Refused, with the reason, while the module's code runs in the tool
|
|
// runner (novox/hq ADR 0349 §11, ADR 0370 §2): its secrets reach it through its files.
|
|
func Secret(name string) ([]byte, error) {
|
|
raw, err := provisionGet("secret", name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var s string
|
|
if err := json.Unmarshal(raw, &s); err != nil {
|
|
return nil, errors.New("the runtime answered a secret that is not a string")
|
|
}
|
|
return []byte(s), nil
|
|
}
|