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
154 lines
4.7 KiB
Go
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)
|
|
}
|
|
}
|