|
@@ -1,381 +0,0 @@
|
|
|
-package raft
|
|
|
|
|
-
|
|
|
|
|
-import (
|
|
|
|
|
- "encoding/json"
|
|
|
|
|
- "os"
|
|
|
|
|
- "testing"
|
|
|
|
|
- "time"
|
|
|
|
|
-)
|
|
|
|
|
-
|
|
|
|
|
-// TestCompactionBasic tests basic log compaction
|
|
|
|
|
-func TestCompactionBasic(t *testing.T) {
|
|
|
|
|
- dir, err := os.MkdirTemp("", "raft-compaction-test")
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
- defer os.RemoveAll(dir)
|
|
|
|
|
-
|
|
|
|
|
- // Create storage with small memory capacity to test file reads
|
|
|
|
|
- storage, err := NewHybridStorage(dir, 200, nil)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
- defer storage.Close()
|
|
|
|
|
-
|
|
|
|
|
- // Append 100 entries - these will stay in memory
|
|
|
|
|
- entries := make([]LogEntry, 100)
|
|
|
|
|
- for i := 0; i < 100; i++ {
|
|
|
|
|
- entries[i] = LogEntry{
|
|
|
|
|
- Index: uint64(i + 1),
|
|
|
|
|
- Term: 1,
|
|
|
|
|
- Type: EntryNormal,
|
|
|
|
|
- Command: []byte("test"),
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- if err := storage.AppendEntries(entries); err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Verify entries exist
|
|
|
|
|
- if storage.GetLastIndex() != 100 {
|
|
|
|
|
- t.Fatalf("expected lastIndex=100, got %d", storage.GetLastIndex())
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Verify entry before compaction
|
|
|
|
|
- entry, err := storage.GetEntry(50)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.Fatalf("should be able to read entry 50 before compaction: %v", err)
|
|
|
|
|
- }
|
|
|
|
|
- if entry.Index != 50 {
|
|
|
|
|
- t.Fatalf("expected entry with index=50, got index=%d", entry.Index)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Compact up to index 50
|
|
|
|
|
- if err := storage.TruncateBefore(50); err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Verify firstIndex updated
|
|
|
|
|
- if storage.GetFirstIndex() != 50 {
|
|
|
|
|
- t.Fatalf("expected firstIndex=50, got %d", storage.GetFirstIndex())
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Verify old entries are inaccessible
|
|
|
|
|
- _, err = storage.GetEntry(49)
|
|
|
|
|
- if err != ErrCompacted {
|
|
|
|
|
- t.Fatalf("expected ErrCompacted for index 49, got %v", err)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Verify entries at and after compaction point are accessible
|
|
|
|
|
- // Entry 50 should still be in memory after TruncateBefore
|
|
|
|
|
- entry, err = storage.GetEntry(51)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.Fatalf("should be able to read entry 51: %v", err)
|
|
|
|
|
- }
|
|
|
|
|
- if entry.Index != 51 {
|
|
|
|
|
- t.Fatalf("expected index=51, got %d", entry.Index)
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// TestSnapshotSaveAndLoad tests snapshot persistence
|
|
|
|
|
-func TestSnapshotSaveAndLoad(t *testing.T) {
|
|
|
|
|
- dir, err := os.MkdirTemp("", "raft-snapshot-test")
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
- defer os.RemoveAll(dir)
|
|
|
|
|
-
|
|
|
|
|
- // Create storage
|
|
|
|
|
- storage, err := NewHybridStorage(dir, 1000, nil)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Create snapshot data
|
|
|
|
|
- snapshotData := map[string]string{
|
|
|
|
|
- "key1": "value1",
|
|
|
|
|
- "key2": "value2",
|
|
|
|
|
- }
|
|
|
|
|
- data, _ := json.Marshal(snapshotData)
|
|
|
|
|
-
|
|
|
|
|
- // Save snapshot
|
|
|
|
|
- if err := storage.SaveSnapshot(data, 100, 5); err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
- storage.Close()
|
|
|
|
|
-
|
|
|
|
|
- // Reopen storage and verify snapshot
|
|
|
|
|
- storage2, err := NewHybridStorage(dir, 1000, nil)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
- defer storage2.Close()
|
|
|
|
|
-
|
|
|
|
|
- loadedData, lastIndex, lastTerm, err := storage2.GetSnapshot()
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if lastIndex != 100 {
|
|
|
|
|
- t.Fatalf("expected lastIndex=100, got %d", lastIndex)
|
|
|
|
|
- }
|
|
|
|
|
- if lastTerm != 5 {
|
|
|
|
|
- t.Fatalf("expected lastTerm=5, got %d", lastTerm)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- var loadedSnapshot map[string]string
|
|
|
|
|
- if err := json.Unmarshal(loadedData, &loadedSnapshot); err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if loadedSnapshot["key1"] != "value1" || loadedSnapshot["key2"] != "value2" {
|
|
|
|
|
- t.Fatalf("snapshot data mismatch: %v", loadedSnapshot)
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// TestCompactionWithKVServer tests compaction through KVServer
|
|
|
|
|
-func TestCompactionWithKVServer(t *testing.T) {
|
|
|
|
|
- dir, err := os.MkdirTemp("", "raft-kv-compaction-test")
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
- defer os.RemoveAll(dir)
|
|
|
|
|
-
|
|
|
|
|
- config := DefaultConfig()
|
|
|
|
|
- config.NodeID = "test-node"
|
|
|
|
|
- config.ListenAddr = "127.0.0.1:19001"
|
|
|
|
|
- config.DataDir = dir
|
|
|
|
|
- config.ClusterNodes = map[string]string{
|
|
|
|
|
- "test-node": "127.0.0.1:19001",
|
|
|
|
|
- }
|
|
|
|
|
- // Lower threshold for testing
|
|
|
|
|
- config.SnapshotThreshold = 100
|
|
|
|
|
- config.SnapshotMinRetention = 10
|
|
|
|
|
-
|
|
|
|
|
- server, err := NewKVServer(config)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if err := server.Start(); err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
- defer server.Stop()
|
|
|
|
|
-
|
|
|
|
|
- // Wait for leader election
|
|
|
|
|
- time.Sleep(500 * time.Millisecond)
|
|
|
|
|
-
|
|
|
|
|
- // Write enough entries to trigger compaction
|
|
|
|
|
- for i := 0; i < 200; i++ {
|
|
|
|
|
- key := "key"
|
|
|
|
|
- val := "value"
|
|
|
|
|
- // We just set the same key 200 times
|
|
|
|
|
- if err := server.Set(key, val); err != nil {
|
|
|
|
|
- t.Fatalf("failed to set key: %v", err)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Check that FSM has the correct value
|
|
|
|
|
- val, ok := server.Get("key")
|
|
|
|
|
- if !ok {
|
|
|
|
|
- t.Fatal("key not found")
|
|
|
|
|
- }
|
|
|
|
|
- if val != "value" {
|
|
|
|
|
- t.Fatalf("expected 'value', got '%s'", val)
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// TestDataConsistencyAfterCompaction tests that data is consistent after compaction
|
|
|
|
|
-func TestDataConsistencyAfterCompaction(t *testing.T) {
|
|
|
|
|
- dir, err := os.MkdirTemp("", "raft-consistency-test")
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
- defer os.RemoveAll(dir)
|
|
|
|
|
-
|
|
|
|
|
- // Create KV state machine
|
|
|
|
|
- fsm := NewKVStateMachine()
|
|
|
|
|
-
|
|
|
|
|
- // Apply a series of operations
|
|
|
|
|
- operations := []KVCommand{
|
|
|
|
|
- {Type: KVSet, Key: "counter", Value: "1"},
|
|
|
|
|
- {Type: KVSet, Key: "counter", Value: "2"},
|
|
|
|
|
- {Type: KVSet, Key: "counter", Value: "3"},
|
|
|
|
|
- {Type: KVSet, Key: "name", Value: "alice"},
|
|
|
|
|
- {Type: KVSet, Key: "counter", Value: "4"},
|
|
|
|
|
- {Type: KVDel, Key: "name"},
|
|
|
|
|
- {Type: KVSet, Key: "counter", Value: "5"},
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- for _, op := range operations {
|
|
|
|
|
- data, _ := json.Marshal(op)
|
|
|
|
|
- fsm.Apply(data)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Take snapshot
|
|
|
|
|
- snapshot, err := fsm.Snapshot()
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Create new FSM and restore from snapshot
|
|
|
|
|
- fsm2 := NewKVStateMachine()
|
|
|
|
|
- if err := fsm2.Restore(snapshot); err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Verify data consistency
|
|
|
|
|
- val1, ok1 := fsm.Get("counter")
|
|
|
|
|
- val2, ok2 := fsm2.Get("counter")
|
|
|
|
|
-
|
|
|
|
|
- if val1 != val2 || ok1 != ok2 {
|
|
|
|
|
- t.Fatalf("counter mismatch: original=%s(%v), restored=%s(%v)", val1, ok1, val2, ok2)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if val1 != "5" {
|
|
|
|
|
- t.Fatalf("expected counter=5, got %s", val1)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // name should not exist (was deleted)
|
|
|
|
|
- _, ok := fsm2.Get("name")
|
|
|
|
|
- if ok {
|
|
|
|
|
- t.Fatal("name should have been deleted")
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// TestCompactionDoesNotLoseData tests that no committed data is lost during compaction
|
|
|
|
|
-func TestCompactionDoesNotLoseData(t *testing.T) {
|
|
|
|
|
- dir, err := os.MkdirTemp("", "raft-no-loss-test")
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
- defer os.RemoveAll(dir)
|
|
|
|
|
-
|
|
|
|
|
- // Use larger memory capacity to keep all entries in memory
|
|
|
|
|
- storage, err := NewHybridStorage(dir, 2000, nil)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
- defer storage.Close()
|
|
|
|
|
-
|
|
|
|
|
- // Write 1000 entries with unique values
|
|
|
|
|
- entries := make([]LogEntry, 1000)
|
|
|
|
|
- for i := 0; i < 1000; i++ {
|
|
|
|
|
- cmd := KVCommand{Type: KVSet, Key: "key", Value: string(rune('A' + i%26))}
|
|
|
|
|
- data, _ := json.Marshal(cmd)
|
|
|
|
|
- entries[i] = LogEntry{
|
|
|
|
|
- Index: uint64(i + 1),
|
|
|
|
|
- Term: 1,
|
|
|
|
|
- Type: EntryNormal,
|
|
|
|
|
- Command: data,
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- if err := storage.AppendEntries(entries); err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Save snapshot at index 500
|
|
|
|
|
- fsm := NewKVStateMachine()
|
|
|
|
|
- for i := 0; i < 500; i++ {
|
|
|
|
|
- fsm.Apply(entries[i].Command)
|
|
|
|
|
- }
|
|
|
|
|
- snapshot, _ := fsm.Snapshot()
|
|
|
|
|
- if err := storage.SaveSnapshot(snapshot, 500, 1); err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Compact log
|
|
|
|
|
- if err := storage.TruncateBefore(500); err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Verify entries 501-1000 still accessible (500 is the compaction point)
|
|
|
|
|
- for i := 501; i <= 1000; i++ {
|
|
|
|
|
- entry, err := storage.GetEntry(uint64(i))
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.Fatalf("entry %d should be accessible: %v", i, err)
|
|
|
|
|
- }
|
|
|
|
|
- if entry.Index != uint64(i) {
|
|
|
|
|
- t.Fatalf("entry index mismatch at %d", i)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Verify entries before 500 return ErrCompacted
|
|
|
|
|
- for i := 1; i < 500; i++ {
|
|
|
|
|
- _, err := storage.GetEntry(uint64(i))
|
|
|
|
|
- if err != ErrCompacted {
|
|
|
|
|
- t.Fatalf("entry %d should return ErrCompacted, got: %v", i, err)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-func mustMarshal(v interface{}) []byte {
|
|
|
|
|
- data, _ := json.Marshal(v)
|
|
|
|
|
- return data
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// TestDynamicCompactionThreshold tests that compaction threshold increases dynamically
|
|
|
|
|
-func TestDynamicCompactionThreshold(t *testing.T) {
|
|
|
|
|
- dir, err := os.MkdirTemp("", "raft-dynamic-threshold-test")
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
- defer os.RemoveAll(dir)
|
|
|
|
|
-
|
|
|
|
|
- config := DefaultConfig()
|
|
|
|
|
- config.NodeID = "test-node"
|
|
|
|
|
- config.ListenAddr = "127.0.0.1:19002"
|
|
|
|
|
- config.DataDir = dir
|
|
|
|
|
- config.ClusterNodes = map[string]string{
|
|
|
|
|
- "test-node": "127.0.0.1:19002",
|
|
|
|
|
- }
|
|
|
|
|
- // Set low thresholds for testing
|
|
|
|
|
- config.SnapshotThreshold = 100
|
|
|
|
|
- config.SnapshotMinRetention = 10
|
|
|
|
|
- config.Logger = nil // Suppress logs
|
|
|
|
|
-
|
|
|
|
|
- server, err := NewKVServer(config)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if err := server.Start(); err != nil {
|
|
|
|
|
- t.Fatal(err)
|
|
|
|
|
- }
|
|
|
|
|
- defer server.Stop()
|
|
|
|
|
-
|
|
|
|
|
- // Wait for leader election
|
|
|
|
|
- time.Sleep(500 * time.Millisecond)
|
|
|
|
|
-
|
|
|
|
|
- // Get initial threshold (should be 0, meaning use SnapshotThreshold)
|
|
|
|
|
- initialThreshold := server.Raft.nextCompactionThreshold
|
|
|
|
|
- if initialThreshold != 0 {
|
|
|
|
|
- t.Fatalf("expected initial threshold to be 0, got %d", initialThreshold)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Propose enough entries to trigger first compaction
|
|
|
|
|
- // We need > 100 entries to trigger compaction
|
|
|
|
|
- for i := 0; i < 150; i++ {
|
|
|
|
|
- cmd := KVCommand{Type: KVSet, Key: "key", Value: "value"}
|
|
|
|
|
- data, _ := json.Marshal(cmd)
|
|
|
|
|
- server.Raft.Propose(data)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Wait for apply and potential compaction
|
|
|
|
|
- time.Sleep(500 * time.Millisecond)
|
|
|
|
|
-
|
|
|
|
|
- // Check that dynamic threshold was set after compaction
|
|
|
|
|
- // After compaction with 150 entries and minRetention=10, we should have ~10 entries
|
|
|
|
|
- // So next threshold should be around 10 * 1.5 = 15, but at least 100 (initial threshold)
|
|
|
|
|
- newThreshold := server.Raft.nextCompactionThreshold
|
|
|
|
|
-
|
|
|
|
|
- // The threshold should now be set (> 0) or remain at initial if compaction happened
|
|
|
|
|
- // Key point: it should be >= SnapshotThreshold to prevent thrashing
|
|
|
|
|
- if newThreshold > 0 && newThreshold < config.SnapshotThreshold {
|
|
|
|
|
- t.Fatalf("dynamic threshold %d should not be less than initial threshold %d",
|
|
|
|
|
- newThreshold, config.SnapshotThreshold)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- t.Logf("Dynamic threshold after first compaction: %d (initial: %d)",
|
|
|
|
|
- newThreshold, config.SnapshotThreshold)
|
|
|
|
|
-}
|
|
|