Files
mesh-catalog/modules/nats/cmd/nats-tools/kept_integration_test.go
T
jschoubben c2e017a4c1 The bus module takes and restores a restore point of a module's kept data, beside the live data (milestone G, W14.2)
Seat verbs snapshot-kept, restore-kept, discard-aside and aside, handed to the keeper; state kept; tool nats_kept.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 12:05:54 +01:00

154 lines
4.7 KiB
Go

//go:build integration
package main
import (
"context"
"errors"
"os"
"path/filepath"
"sync"
"testing"
"time"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"nats-tools/internal/bussnap"
"nats-tools/internal/keeper"
)
// mapStore is the keeper's own state in memory.
type mapStore struct {
mu sync.Mutex
kv map[string][]byte
}
func (s *mapStore) Get(state, key string) ([]byte, uint64, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
v, ok := s.kv[state+"/"+key]
return v, 1, ok, nil
}
func (s *mapStore) Put(state, key string, v []byte) (uint64, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.kv[state+"/"+key] = v
return 1, nil
}
func (s *mapStore) Delete(state, key string) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.kv, state+"/"+key)
return nil
}
func (s *mapStore) Keys(string) ([]string, error) { return nil, nil }
func (s *mapStore) Create(state, key string, v []byte) (uint64, error) {
return 0, errors.New("not used")
}
// A module's kept data on the real bus is backed up and restored beside the live data (design 47, "A module's kept data on
// the bus, backed up and restored with its delivery"): the bucket as the server's own snapshot, the module's consumer on
// another stream at its position; after a change, a restore brings the bucket and the position back, and the live bucket
// is kept whole in the data set aside.
func TestAKeptDataIsRestoredBesideTheLiveDataOnTheRealBus(t *testing.T) {
s, err := server.NewServer(&server.Options{Port: -1, JetStream: true, StoreDir: t.TempDir(), NoLog: true, NoSigs: true})
if err != nil {
t.Fatal(err)
}
go s.Start()
if !s.ReadyForConnections(5 * time.Second) {
t.Fatal("the server did not start")
}
defer s.Shutdown()
nc, err := nats.Connect(s.ClientURL())
if err != nil {
t.Fatal(err)
}
defer nc.Close()
js, err := jetstream.New(nc)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: "idx_modules"})
if err != nil {
t.Fatal(err)
}
forge, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "forge", Subjects: []string{"forge.>"}})
if err != nil {
t.Fatal(err)
}
for i := 0; i < 10; i++ {
if _, err := js.Publish(ctx, "forge.merged", []byte("m")); err != nil {
t.Fatal(err)
}
}
cons, err := forge.CreateConsumer(ctx, jetstream.ConsumerConfig{Durable: "ace_idx", AckPolicy: jetstream.AckExplicitPolicy})
if err != nil {
t.Fatal(err)
}
batch, err := cons.Fetch(4)
if err != nil {
t.Fatal(err)
}
for m := range batch.Messages() {
_ = m.DoubleAck(ctx)
}
if _, err := kv.PutString(ctx, "ace", "fingerprint-1"); err != nil {
t.Fatal(err)
}
dir := t.TempDir()
var acts []string
k := &keeper.Keeper{Bus: keeper.JetStream{JS: js, Snap: bussnap.Into(nc, dir)}, Store: &mapStore{kv: map[string][]byte{}},
Kept: keeper.KeptJetStream{NC: nc, JS: js}, KeptDir: dir,
Act: func(what, why, cause string) error { acts = append(acts, what); return nil }}
take := &keeper.Request{Verb: keeper.VerbSnapshotKept, Module: "idx", Local: "modules", Form: 0, Fingerprint: "abc123", CatchUp: "indexer_take_in", Why: "before form 1"}
if _, err := k.SnapshotKept(ctx, take); err != nil {
t.Fatal(err)
}
// The form-1 build rewrites the kept data, and its consumer moves on.
if _, err := kv.PutString(ctx, "ace", "fingerprint-2-form-1"); err != nil {
t.Fatal(err)
}
batch, err = cons.Fetch(5)
if err != nil {
t.Fatal(err)
}
for m := range batch.Messages() {
_ = m.DoubleAck(ctx)
}
restore := &keeper.Request{Verb: keeper.VerbRestoreKept, Module: "idx", Local: "modules", Form: 0, Fingerprint: "abc123", From: take.Dir, Why: "revert"}
if _, err := k.RestoreKept(ctx, restore); err != nil {
t.Fatal(err)
}
kv, err = js.KeyValue(ctx, "idx_modules")
if err != nil {
t.Fatal(err)
}
if e, err := kv.Get(ctx, "ace"); err != nil || string(e.Value()) != "fingerprint-1" {
t.Errorf("the restored bucket holds %v (%v), want fingerprint-1", e, err)
}
if restore.CatchUp != "indexer_take_in" {
t.Errorf("catch-up %q", restore.CatchUp)
}
forge, _ = js.Stream(ctx, "forge")
ci, err := forge.Consumer(ctx, "ace_idx")
if err != nil || ci.CachedInfo().Config.OptStartSeq != 5 {
t.Errorf("the consumer is %+v (%v): it starts after the 4 messages it had acknowledged", ci.CachedInfo().Config, err)
}
kept, err := os.ReadDir(filepath.Join(dir, "aside"))
if err != nil || len(kept) != 1 {
t.Fatalf("aside holds %v (%v)", kept, err)
}
if _, err := os.Stat(filepath.Join(dir, "aside", kept[0].Name(), keeper.ArchiveFile)); err != nil {
t.Error("the live data set aside is not kept whole")
}
if len(acts) != 2 {
t.Errorf("hand acts %v: the restore point and the restore", acts)
}
}