//go:build integration package views_test import ( "context" "encoding/json" "testing" "time" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" "git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/testbus" "git.novox.be/novox/mesh-sdk/go/views" "git.novox.be/novox/mesh-sdk/go/views/viewstest" ) // An owner's change is an ordinary event on the shared events stream (novox/hq ADR 0635 ยง5): a reader that follows the // owner's state by its read tool hears it on the module's durable consumer of that stream, with no stream of the owner's, // and a message that is no views event goes to Other. func TestAReaderHearsAnOwnersChangesAsOrdinaryEvents(t *testing.T) { nc, err := nats.Connect(testbus.URL(t)) if err != nil { t.Fatal(err) } defer nc.Close() js, err := jetstream.New(nc) if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() cred := process.Account{Node: "n1", Module: "reader"} kv := viewstest.Serve(t, ctx, js, nc, "o", "modules", "o.o_read_modules", cred.EventConsumerName()) if _, err := kv.Put(ctx, "a", []byte(`{"n":1}`)); err != nil { t.Fatal(err) } other := make(chan string, 1) r := &views.ProcessReader{Module: "reader", Other: func(m jetstream.Msg) { other <- m.Subject(); _ = m.Ack() }, Logf: t.Logf, Call: func(tool string, body any) (json.RawMessage, error) { raw, _ := json.Marshal(body) m, err := nc.Request("mesh.mod.o.call.o_read_modules.n0", raw, 5*time.Second) if err != nil { return nil, err } return m.Data, nil }} if _, err := r.Follow("o", "modules", "o.o_read_modules"); err != nil { t.Fatal(err) } r.Start(ctx, js, cred, nil) <-r.Ready() pic, err := r.KeyValue(ctx, "o_modules") if err != nil { t.Fatal(err) } w, err := pic.Watch(ctx, "b") if err != nil { t.Fatal(err) } if _, err := kv.Put(ctx, "b", []byte(`{"n":2}`)); err != nil { t.Fatal(err) } for e := range w.Updates() { if e != nil && e.Key() == "b" { break } } if s := r.Followers()[0].Status(); s.Asks != 1 || s.Keys != 2 { t.Fatalf("the change was not heard as an event alone: %+v", s) } if _, err := js.Publish(ctx, "mesh.mod.o.event.something-else", []byte("{}")); err != nil { t.Fatal(err) } if got := <-other; got != "mesh.mod.o.event.something-else" { t.Fatalf("other got %s", got) } } // A module has one durable consumer on the shared events stream, so the loops that hear events in one module must share // one pull (process.EventHub): two handlers and the views reader on the hub each get their own messages and no other's, // and a message nobody takes is acknowledged. func TestTheViewsReaderAndTwoHandlersShareTheModulesOneDurable(t *testing.T) { nc, err := nats.Connect(testbus.URL(t)) if err != nil { t.Fatal(err) } defer nc.Close() js, err := jetstream.New(nc) if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() cred := process.Account{Node: "n1", Module: "reader"} kv := viewstest.Serve(t, ctx, js, nc, "o", "modules", "o.o_read_modules", cred.EventConsumerName()) hub := process.NewEventHub(t.Logf) got := map[string]chan string{"a": make(chan string, 8), "b": make(chan string, 8)} for name, pattern := range map[string]string{"a": "mesh.mod.p.event.a", "b": "mesh.mod.q.event.b.*"} { if err := hub.Handle(pattern, func(m jetstream.Msg) { _ = m.Ack(); got[name] <- m.Subject() }); err != nil { t.Fatal(err) } } r := &views.ProcessReader{Module: "reader", Hub: hub, Logf: t.Logf, Call: func(tool string, body any) (json.RawMessage, error) { raw, _ := json.Marshal(body) m, err := nc.Request("mesh.mod.o.call.o_read_modules.n0", raw, 5*time.Second) if err != nil { return nil, err } return m.Data, nil }} if _, err := r.Follow("o", "modules", "o.o_read_modules"); err != nil { t.Fatal(err) } r.Start(ctx, js, cred, nil) if err := hub.Start(ctx, js, cred); err != nil { t.Fatal(err) } <-r.Ready() pic, err := r.KeyValue(ctx, "o_modules") if err != nil { t.Fatal(err) } w, err := pic.Watch(ctx, "b") if err != nil { t.Fatal(err) } for _, s := range []string{"mesh.mod.p.event.a", "mesh.mod.q.event.b.c", "mesh.mod.z.event.nobody", "mesh.mod.p.event.a"} { if _, err := js.Publish(ctx, s, []byte("{}")); err != nil { t.Fatal(err) } } if _, err := kv.Put(ctx, "b", []byte(`{"n":2}`)); err != nil { t.Fatal(err) } for e := range w.Updates() { if e != nil && e.Key() == "b" { break } } for _, want := range []struct{ who, subject string }{{"a", "mesh.mod.p.event.a"}, {"b", "mesh.mod.q.event.b.c"}, {"a", "mesh.mod.p.event.a"}} { if s := <-got[want.who]; s != want.subject { t.Fatalf("handler %s got %s, wanted %s", want.who, s, want.subject) } } if len(got["a"])+len(got["b"]) != 0 { t.Fatal("a handler heard a message that was another's") } cons, err := js.Consumer(ctx, process.EventsStream, cred.EventConsumerName()) if err != nil { t.Fatal(err) } if info, err := cons.Info(ctx); err != nil || info.NumAckPending != 0 { t.Fatalf("a message was left unacknowledged: %+v %v", info, err) } // A hub started before the bus holds its durable says so, and binds at a later start: a module is not lost for the // order in which the bus makes its consumer. late := process.Account{Node: "n1", Module: "late"} lateHub := process.NewEventHub(t.Logf) heard := make(chan string, 1) if err := lateHub.Handle("mesh.mod.l.event.>", func(m jetstream.Msg) { _ = m.Ack(); heard <- m.Subject() }); err != nil { t.Fatal(err) } defer lateHub.Stop() if err := lateHub.Start(ctx, js, late); err == nil { t.Fatal("a hub with no durable on the bus started") } if _, err := js.CreateOrUpdateConsumer(ctx, process.EventsStream, jetstream.ConsumerConfig{Durable: late.EventConsumerName(), AckPolicy: jetstream.AckExplicitPolicy, FilterSubjects: []string{"mesh.mod.l.event.>"}}); err != nil { t.Fatal(err) } if err := lateHub.Start(ctx, js, late); err != nil { t.Fatalf("the hub did not bind at its second start: %v", err) } if _, err := js.Publish(ctx, "mesh.mod.l.event.x", []byte("{}")); err != nil { t.Fatal(err) } select { case <-heard: case <-time.After(10 * time.Second): t.Fatal("the hub started late heard nothing") } }