mesh/merge-gate pass: builds mesh-tools, node-tools → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of …
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery delivered
mesh/delivery-group group feat/asks-answered-on-any-channel stopped: a member was stopped
150 lines
5.8 KiB
Go
150 lines
5.8 KiB
Go
package runtime
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
|
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
|
|
)
|
|
|
|
// A bundle's seat traffic through moduleBus.Seat (novox/hq ADR 0259 §3), against a real bus: publish under
|
|
// its own name with the id prefixed and a duplicate said; the record read without naming the bucket; a
|
|
// worker and a proof subject bound once however often a restarted child asks; and nothing beyond what the
|
|
// membership lists.
|
|
func TestABundlesSeatTrafficGoesThroughItsModulesBus(t *testing.T) {
|
|
mesh := mt.New(t)
|
|
nc, err := nats.Connect(mt.URL(t))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(nc.Close)
|
|
js, _ := nc.JetStream()
|
|
if _, err := js.AddStream(&nats.StreamConfig{Name: "SEAT_OPERATOR_CHANNEL", Retention: nats.WorkQueuePolicy,
|
|
Subjects: []string{"mesh.seat.operator-channel.accept.>"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := js.AddConsumer("SEAT_OPERATOR_CHANNEL", &nats.ConsumerConfig{Durable: "SEAT_OPERATOR_CHANNEL_worker",
|
|
AckPolicy: nats.AckExplicitPolicy, FilterSubject: "mesh.seat.operator-channel.accept.>"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
kv, err := js.CreateKeyValue(&nats.KeyValueConfig{Bucket: "messenger_asks"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, _ = kv.Put("mesh-delivery.d-1", []byte(`{"state":"open"}`))
|
|
asker := mt.MembershipOf("mesh-delivery", "anchor", false, nil)
|
|
asker.SeatTraffic = &bus.SeatTraffic{Publish: []string{"mesh.seat.operator-channel.accept.ask.mesh-delivery"},
|
|
Records: []string{"$JS.API.DIRECT.GET.KV_messenger_asks.$KV.messenger_asks.mesh-delivery.>"}}
|
|
router := mt.MembershipOf("messenger", "anchor", false, nil)
|
|
router.SeatTraffic = &bus.SeatTraffic{Answers: []string{"mesh.seat.intake.proof.code.*"},
|
|
Workers: []bus.SeatWorker{{Stream: "SEAT_OPERATOR_CHANNEL", Consumer: "SEAT_OPERATOR_CHANNEL_worker",
|
|
Filter: "mesh.seat.operator-channel.accept.>"}}}
|
|
mesh.Issue(t, asker)
|
|
mesh.Issue(t, router)
|
|
conn := connect(t, "node-tools", "anchor")
|
|
conn.Follow("mesh-delivery")
|
|
conn.Follow("messenger")
|
|
mt.Until(t, func() error {
|
|
if conn.Membership("mesh-delivery") == nil || conn.Membership("messenger") == nil {
|
|
return errors.New("not issued yet")
|
|
}
|
|
return nil
|
|
})
|
|
all := &consumers{conn: conn, logf: t.Logf, of: map[string]*moduleEvents{}}
|
|
asking := all.forModule("mesh-delivery").(*moduleBus)
|
|
routing := all.forModule("messenger").(*moduleBus)
|
|
|
|
publish := json.RawMessage(`{"subject":"mesh.seat.operator-channel.accept.ask.mesh-delivery","body":{"id":"d-1"},"id":"d-1"}`)
|
|
first, err := asking.Seat("publish", publish, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
again, _ := asking.Seat("publish", publish, nil)
|
|
if !strings.Contains(string(first), `"duplicate":false`) || !strings.Contains(string(again), `"duplicate":true`) {
|
|
t.Errorf("the same id twice: %s, then %s", first, again)
|
|
}
|
|
if _, err := asking.Seat("publish", json.RawMessage(`{"subject":"mesh.seat.operator-channel.accept.ask.mesh-controller","body":{}}`), nil); err == nil {
|
|
t.Error("a module submitted under another's name")
|
|
}
|
|
got, err := asking.Seat("record", json.RawMessage(`{"key":"d-1"}`), nil)
|
|
if err != nil || !strings.Contains(string(got), `"state":"open"`) {
|
|
t.Errorf("the record read without its bucket: %s %v", got, err)
|
|
}
|
|
|
|
var mu sync.Mutex
|
|
var taken []string
|
|
handOn := func(method string, params any) (json.RawMessage, error) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
raw, _ := json.Marshal(params)
|
|
taken = append(taken, method+" "+string(raw))
|
|
return json.RawMessage(`{}`), nil
|
|
}
|
|
for i := 0; i < 2; i++ { // a child that started again asks again
|
|
if _, err := routing.Seat("take", json.RawMessage(`{"worker":"SEAT_OPERATOR_CHANNEL_worker"}`), handOn); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
deadline := time.Now().Add(10 * time.Second)
|
|
for {
|
|
mu.Lock()
|
|
n := len(taken)
|
|
mu.Unlock()
|
|
if n >= 1 || time.Now().After(deadline) {
|
|
break
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
time.Sleep(300 * time.Millisecond)
|
|
mu.Lock()
|
|
if len(taken) != 1 || !strings.Contains(taken[0], "mesh/work") || !strings.Contains(taken[0], `"x-event-id":"mesh-delivery.d-1"`) {
|
|
t.Errorf("taken %v", taken)
|
|
}
|
|
mu.Unlock()
|
|
if _, err := asking.Seat("take", json.RawMessage(`{"worker":"SEAT_OPERATOR_CHANNEL_worker"}`), handOn); err == nil {
|
|
t.Error("an asker took the router's work")
|
|
}
|
|
if _, err := asking.Seat("answer", json.RawMessage(`{"subject":"mesh.seat.intake.proof.code.*"}`), handOn); err == nil {
|
|
t.Error("an asker answered proofs")
|
|
}
|
|
if _, err := asking.Seat("anything", json.RawMessage(`{}`), nil); err == nil {
|
|
t.Error("an unknown verb was answered")
|
|
}
|
|
}
|
|
|
|
// A lost compare-and-set is answered with its code, so the SDK knows it without reading words.
|
|
func TestALostCompareAndSetCarriesItsCode(t *testing.T) {
|
|
mesh := mt.New(t)
|
|
mesh.Bucket(t, "messenger_asks")
|
|
m := mt.MembershipOf("messenger", "anchor", false, nil)
|
|
m.State = []bus.StateIssued{{Name: "asks", Bucket: "messenger_asks", Writes: true}}
|
|
mesh.Issue(t, m)
|
|
conn := connect(t, "node-tools", "anchor")
|
|
conn.Follow("messenger")
|
|
mt.Until(t, func() error {
|
|
if conn.Membership("messenger") == nil {
|
|
return errors.New("not issued yet")
|
|
}
|
|
return nil
|
|
})
|
|
b := (&consumers{conn: conn, logf: t.Logf, of: map[string]*moduleEvents{}}).forModule("messenger")
|
|
if _, err := b.State("create", json.RawMessage(`{"state":"asks","key":"a","value":{}}`)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := b.State("create", json.RawMessage(`{"state":"asks","key":"a","value":{}}`))
|
|
var coded interface{ ErrorCode() int }
|
|
if !errors.As(err, &coded) || coded.ErrorCode() != -32010 || !errors.Is(err, bus.ErrStateChanged) {
|
|
t.Errorf("a second create: %v", err)
|
|
}
|
|
if _, err := b.State("update", json.RawMessage(`{"state":"asks","key":"a","value":{},"revision":0}`)); err == nil {
|
|
t.Error("an update of revision zero was taken")
|
|
}
|
|
}
|