Files
mesh-sdk/go/process/state_test.go
T
jschoubben 44dbb22ea7 Wait out a bus that answers nothing for a few seconds instead of losing the write (issue 774)
At each mesh-grants restart and user-list reload (16:37:24, 16:55:15) nats-server 2.11's reloadAuthorization left
the mesh's account without its imports, $JS.API.> among them, for a few seconds: requests had no responders,
JetStream calls said it was not enabled, and a build-test result on ace was lost.

Quick fix, chosen as asked; the thorough fix is a reload that keeps the account's imports, in the bus's holder.
The package busretry asks a request again, backing off within about 30 s, only while its error says it reached no
one (no responders, JetStream not enabled, no response from the stream, or either in words): nothing was done, so
even a create or an update is safe to ask again. A timeout or any answer is never asked again. After the bound it
is said on standard error and returned. Used by process.State's requests, process.Conn's calls (asked again only
while $JS.API.INFO says the bus itself answers nothing, so a callee that is not running still fails at once) and
asks, and the bundle's state requests through the runtime.
2026-10-30 18:18:32 +01:00

347 lines
12 KiB
Go

package process
import (
"context"
"errors"
"sort"
"strings"
"sync"
"testing"
"time"
"github.com/nats-io/nats.go/jetstream"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asker"
"git.novox.be/novox/mesh-sdk/go/busretry"
)
// fakeEntry is a fakeKV's KeyValueEntry: only Key, Value and Revision are read by State.
type fakeEntry struct {
jetstream.KeyValueEntry
key string
value []byte
rev uint64
}
func (e fakeEntry) Key() string { return e.key }
func (e fakeEntry) Value() []byte { return e.value }
func (e fakeEntry) Revision() uint64 { return e.rev }
// fakeKV is an in-memory jetstream.KeyValue: embedding the nil interface satisfies every method this test
// never calls, so only Get, Create and Update need a body (1188/AC2's "a fake KV").
type fakeKV struct {
jetstream.KeyValue
mu sync.Mutex
values map[string][]byte
revs map[string]uint64
next uint64
// initial is what a Watch hands first, before its nil marker; watcher is the last one made.
initial []jetstream.KeyValueEntry
watcher *fakeWatcher
// withTTL says the last create of a key carried an option: its time to live.
withTTL map[string]bool
}
func newFakeKV() *fakeKV { return &fakeKV{values: map[string][]byte{}, revs: map[string]uint64{}} }
func (f *fakeKV) Get(_ context.Context, key string) (jetstream.KeyValueEntry, error) {
f.mu.Lock()
defer f.mu.Unlock()
v, ok := f.values[key]
if !ok {
return nil, jetstream.ErrKeyNotFound
}
return fakeEntry{key: key, value: v, rev: f.revs[key]}, nil
}
func (f *fakeKV) Create(_ context.Context, key string, value []byte, opts ...jetstream.KVCreateOpt) (uint64, error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.withTTL == nil {
f.withTTL = map[string]bool{}
}
f.withTTL[key] = len(opts) > 0
if _, ok := f.values[key]; ok {
return 0, jetstream.ErrKeyExists
}
f.next++
f.values[key], f.revs[key] = value, f.next
return f.next, nil
}
func (f *fakeKV) Update(_ context.Context, key string, value []byte, revision uint64) (uint64, error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.revs[key] != revision {
return 0, jetstream.ErrKeyRevisionMismatch
}
f.next++
f.values[key], f.revs[key] = value, f.next
return f.next, nil
}
func (f *fakeKV) Put(_ context.Context, key string, value []byte) (uint64, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.next++
f.values[key], f.revs[key] = value, f.next
return f.next, nil
}
func (f *fakeKV) Delete(_ context.Context, key string, _ ...jetstream.KVDeleteOpt) error {
f.mu.Lock()
defer f.mu.Unlock()
delete(f.values, key)
delete(f.revs, key)
return nil
}
func (f *fakeKV) Keys(_ context.Context, _ ...jetstream.WatchOpt) ([]string, error) {
f.mu.Lock()
defer f.mu.Unlock()
if len(f.values) == 0 {
return nil, jetstream.ErrNoKeysFound
}
keys := make([]string, 0, len(f.values))
for k := range f.values {
keys = append(keys, k)
}
sort.Sort(sort.Reverse(sort.StringSlice(keys))) // as a bucket lists them: in no order the caller may rely on
return keys, nil
}
// fakeWatcher is a jetstream.KeyWatcher fed by a test.
type fakeWatcher struct {
jetstream.KeyWatcher
updates chan jetstream.KeyValueEntry
pattern string
}
func (w *fakeWatcher) Updates() <-chan jetstream.KeyValueEntry { return w.updates }
func (w *fakeWatcher) Stop() error { return nil }
func (f *fakeKV) Watch(_ context.Context, pattern string, _ ...jetstream.WatchOpt) (jetstream.KeyWatcher, error) {
f.mu.Lock()
defer f.mu.Unlock()
w := &fakeWatcher{updates: make(chan jetstream.KeyValueEntry, 16), pattern: pattern}
f.watcher = w
for _, e := range f.initial {
w.updates <- e
}
w.updates <- nil
return w, nil
}
func stateOn(kv jetstream.KeyValue) State {
return State{Module: "mesh-mail", Name: "sends", kv: func(context.Context) (jetstream.KeyValue, error) { return kv, nil }}
}
// 1188/AC2: given the process connection's state type, when used as go/asker's Store, then it compiles and
// behaves as the interface says, against a fake KV.
func TestStateSatisfiesAskerStore(t *testing.T) {
var _ asker.Store = State{}
s := stateOn(newFakeKV())
if e, err := s.Get("ask.1"); err != nil || e != nil {
t.Fatalf("Get of an absent key: %v, %v", e, err)
}
rev, err := s.Create("ask.1", map[string]any{"a": 1})
if err != nil {
t.Fatalf("Create: %v", err)
}
e, err := s.Get("ask.1")
if err != nil || e == nil || e.Revision != rev {
t.Fatalf("Get after Create: %+v, %v", e, err)
}
if _, err := s.Create("ask.1", map[string]any{"a": 2}); !errors.Is(err, stdio.ErrChanged) {
t.Fatalf("Create of an existing key: %v, want stdio.ErrChanged", err)
}
rev2, err := s.Update("ask.1", map[string]any{"a": 3}, rev)
if err != nil {
t.Fatalf("Update at the revision read: %v", err)
}
if _, err := s.Update("ask.1", map[string]any{"a": 4}, rev); !errors.Is(err, stdio.ErrChanged) {
t.Fatalf("Update at a stale revision: %v, want stdio.ErrChanged", err)
}
if rev2 == rev {
t.Fatal("Update did not advance the revision")
}
// 1952 S6 (ADR 0564 clauses 4, 7): a key created with its own time to live is created once, a second create
// refused as an act begun already; a renewal writes it whole with a fresh time to live; a time to live the bus
// cannot keep is refused.
kv := newFakeKV()
s = stateOn(kv)
if _, err := s.CreateTTL("settle.H3", map[string]any{"a": 1}, time.Minute); err != nil || !kv.withTTL["settle.H3"] {
t.Fatalf("CreateTTL: %v, with a time to live %v", err, kv.withTTL["settle.H3"])
}
if _, err := s.CreateTTL("settle.H3", map[string]any{"a": 2}, time.Minute); !errors.Is(err, stdio.ErrChanged) {
t.Fatalf("CreateTTL of a live key: %v, want stdio.ErrChanged", err)
}
kv.withTTL["settle.H3"] = false
if _, err := s.RenewTTL("settle.H3", map[string]any{"a": 3}, time.Minute); err != nil || !kv.withTTL["settle.H3"] {
t.Fatalf("RenewTTL: %v, with a time to live %v", err, kv.withTTL["settle.H3"])
}
if e, _ := s.Get("settle.H3"); e == nil || string(e.Value) != `{"a":3}` {
t.Fatalf("after RenewTTL the key reads %+v", e)
}
if _, err := s.CreateTTL("pulse.S1", 1, 10*time.Millisecond); err == nil || !strings.Contains(err.Error(), "shorter") {
t.Fatalf("a time to live under a second: %v", err)
}
}
// 1840/S1: given a state, when a key is put twice, then each Put returns a new revision and the key reads
// back the last value, whether or not it had one.
func TestStatePutWritesAKeyWhole(t *testing.T) {
s := stateOn(newFakeKV())
rev, err := s.Put("k", map[string]any{"a": 1})
if err != nil || rev == 0 {
t.Fatalf("Put of a new key: %d, %v", rev, err)
}
rev2, err := s.Put("k", map[string]any{"a": 2})
if err != nil || rev2 <= rev {
t.Fatalf("Put over a key: %d, %v, want a revision after %d", rev2, err, rev)
}
e, err := s.Get("k")
if err != nil || e == nil || e.Revision != rev2 || string(e.Value) != `{"a":2}` {
t.Fatalf("Get after Put: %+v, %v", e, err)
}
// Issue 774: a bus that answers nothing for a moment (a user-list reload) is waited out, not a lost write.
busretry.FirstWait = time.Millisecond
kv, blackedOut := newFakeKV(), 2
dark := State{Module: "mesh-mail", Name: "sends", kv: func(context.Context) (jetstream.KeyValue, error) {
if blackedOut > 0 {
blackedOut--
return nil, jetstream.ErrJetStreamNotEnabled
}
return kv, nil
}}
if rev, err := dark.Put("k", 1); err != nil || rev == 0 || blackedOut != 0 {
t.Fatalf("a Put through a short blackout: %d, %v", rev, err)
}
}
// 1840/S1: given a state, when its keys are listed, then they come sorted, and none is an empty list, not an error.
func TestStateKeysAreSortedAndEmptyWhenNone(t *testing.T) {
s := stateOn(newFakeKV())
if keys, err := s.Keys(); err != nil || len(keys) != 0 {
t.Fatalf("Keys of an empty state: %v, %v", keys, err)
}
for _, k := range []string{"b", "c", "a"} {
if _, err := s.Put(k, 1); err != nil {
t.Fatal(err)
}
}
keys, err := s.Keys()
if err != nil || strings.Join(keys, ",") != "a,b,c" {
t.Fatalf("Keys: %v, %v, want a,b,c", keys, err)
}
}
type watchEntry struct {
fakeEntry
op jetstream.KeyValueOp
}
func (e watchEntry) Operation() jetstream.KeyValueOp { return e.op }
// 1840/S2: given a state with current values (one of them deleted), when it is watched, then the handler gets
// each current value marked Current and not the deleted one, Watch returns, and a later put and delete arrive
// in order unmarked.
func TestStateWatchHandsCurrentValuesThenChanges(t *testing.T) {
kv := newFakeKV()
kv.initial = []jetstream.KeyValueEntry{
watchEntry{fakeEntry{key: "a", value: []byte(`1`), rev: 1}, jetstream.KeyValuePut},
watchEntry{fakeEntry{key: "gone", rev: 2}, jetstream.KeyValueDelete},
watchEntry{fakeEntry{key: "b", value: []byte(`2`), rev: 3}, jetstream.KeyValuePut},
}
got := make(chan stdio.StateChange, 8)
s := stateOn(kv)
if err := s.Watch("", func(c stdio.StateChange) error { got <- c; return nil }); err != nil {
t.Fatal(err)
}
for _, want := range []stdio.StateChange{
{State: "sends", Key: "a", Op: "put", Value: []byte(`1`), Revision: 1, Current: true},
{State: "sends", Key: "b", Op: "put", Value: []byte(`2`), Revision: 3, Current: true},
} {
select {
case c := <-got:
if c.Key != want.Key || c.Op != want.Op || string(c.Value) != string(want.Value) || c.Revision != want.Revision || c.Current != want.Current || c.State != want.State {
t.Fatalf("got %+v, want %+v", c, want)
}
default:
t.Fatalf("Watch returned before handling %+v", want)
}
}
if len(got) != 0 {
t.Fatalf("a deleted key was handed: %+v", <-got)
}
kv.watcher.updates <- watchEntry{fakeEntry{key: "a", value: []byte(`3`), rev: 4}, jetstream.KeyValuePut}
kv.watcher.updates <- watchEntry{fakeEntry{key: "b", rev: 5}, jetstream.KeyValueDelete}
for _, want := range []stdio.StateChange{
{Key: "a", Op: "put", Revision: 4}, {Key: "b", Op: "delete", Revision: 5},
} {
select {
case c := <-got:
if c.Key != want.Key || c.Op != want.Op || c.Revision != want.Revision || c.Current {
t.Fatalf("got %+v, want %+v", c, want)
}
case <-time.After(5 * time.Second):
t.Fatalf("no change %+v", want)
}
}
}
// 1840/S2: a key narrowing reaches the bus as a subject pattern: `*` one name, `**` the rest, empty every key.
func TestStateWatchNarrowing(t *testing.T) {
for key, want := range map[string]string{"": ">", "a.b": "a.b", "a.*": "a.*", "a.**": "a.>", "**": ">"} {
if got := watchPattern(key); got != want {
t.Errorf("watchPattern(%q) = %q, want %q", key, got, want)
}
}
}
// 1840/S2: given a handler that refuses a current value, then Watch returns its error.
func TestStateWatchReturnsAHandlersErrorOnCurrentValues(t *testing.T) {
kv := newFakeKV()
kv.initial = []jetstream.KeyValueEntry{watchEntry{fakeEntry{key: "a", value: []byte(`1`), rev: 1}, jetstream.KeyValuePut}}
boom := errors.New("boom")
if err := stateOn(kv).Watch("", func(stdio.StateChange) error { return boom }); !errors.Is(err, boom) {
t.Fatalf("Watch: %v, want the handler's error", err)
}
}
// 1840/S2: given a connection that closes before the current values arrive, then Watch returns an error.
func TestStateWatchEndsWithAnErrorWhenTheConnectionCloses(t *testing.T) {
s := State{Module: "mesh-mail", Name: "sends", closed: func() bool { return true },
kv: func(context.Context) (jetstream.KeyValue, error) { return silentKV{}, nil }}
if err := s.Watch("", func(stdio.StateChange) error { return nil }); err == nil {
t.Fatal("Watch on a closed connection returned no error")
}
}
// silentKV watches but never delivers.
type silentKV struct{ jetstream.KeyValue }
func (silentKV) Watch(context.Context, string, ...jetstream.WatchOpt) (jetstream.KeyWatcher, error) {
return &fakeWatcher{updates: make(chan jetstream.KeyValueEntry)}, nil
}
// 1840/S1: given a key, when it is deleted, then it reads as absent; deleting a key that was not there is no error.
func TestStateDeleteRemovesAKeyAndAnAbsentOneIsNoError(t *testing.T) {
s := stateOn(newFakeKV())
if _, err := s.Put("k", 1); err != nil {
t.Fatal(err)
}
if err := s.Delete("k"); err != nil {
t.Fatalf("Delete of a key: %v", err)
}
if e, err := s.Get("k"); err != nil || e != nil {
t.Fatalf("Get after Delete: %+v, %v, want absent", e, err)
}
if err := s.Delete("never"); err != nil {
t.Fatalf("Delete of an absent key: %v", err)
}
}