kevo/pkg/replication/replicator_test.go
Jeremy Tregunna 02febadf5d
feat: implement WAL replicator and entry serialization
- Add WAL replicator component with entry capture, buffering, and subscriptions
- Implement WAL entry serialization with checksumming
- Add batch serialization for network-efficient transfers
- Implement proper concurrency control with mutex protection
- Add utility functions for entry size estimation
- Create comprehensive test suite
2025-04-26 11:54:19 -06:00

402 lines
9.5 KiB
Go

package replication
import (
"sync"
"testing"
"time"
"github.com/KevoDB/kevo/pkg/wal"
)
// MockEntryProcessor implements the EntryProcessor interface for testing
type MockEntryProcessor struct {
mu sync.Mutex
processedEntries []*wal.Entry
processedBatches [][]*wal.Entry
entriesProcessed int
batchesProcessed int
failProcessEntry bool
failProcessBatch bool
}
func (m *MockEntryProcessor) ProcessEntry(entry *wal.Entry) error {
m.mu.Lock()
defer m.mu.Unlock()
m.processedEntries = append(m.processedEntries, entry)
m.entriesProcessed++
if m.failProcessEntry {
return ErrReplicatorClosed // Just use an existing error
}
return nil
}
func (m *MockEntryProcessor) ProcessBatch(entries []*wal.Entry) error {
m.mu.Lock()
defer m.mu.Unlock()
m.processedBatches = append(m.processedBatches, entries)
m.batchesProcessed++
if m.failProcessBatch {
return ErrReplicatorClosed
}
return nil
}
func (m *MockEntryProcessor) GetStats() (int, int) {
m.mu.Lock()
defer m.mu.Unlock()
return m.entriesProcessed, m.batchesProcessed
}
func TestWALReplicatorBasic(t *testing.T) {
replicator := NewWALReplicator(1000)
defer replicator.Close()
// Create some test entries
entry1 := &wal.Entry{
SequenceNumber: 1,
Type: wal.OpTypePut,
Key: []byte("key1"),
Value: []byte("value1"),
}
entry2 := &wal.Entry{
SequenceNumber: 2,
Type: wal.OpTypePut,
Key: []byte("key2"),
Value: []byte("value2"),
}
// Process some entries
replicator.OnEntryWritten(entry1)
replicator.OnEntryWritten(entry2)
// Check entry count
if count := replicator.GetEntryCount(); count != 2 {
t.Errorf("Expected 2 entries, got %d", count)
}
// Check highest timestamp
if ts := replicator.GetHighestTimestamp(); ts != 2 {
t.Errorf("Expected highest timestamp 2, got %d", ts)
}
// Get entries after timestamp 0
entries, err := replicator.GetEntriesAfter(ReplicationPosition{Timestamp: 0})
if err != nil {
t.Fatalf("Error getting entries: %v", err)
}
if len(entries) != 2 {
t.Fatalf("Expected 2 entries after timestamp 0, got %d", len(entries))
}
// Check entries are sorted by timestamp
if entries[0].SequenceNumber != 1 || entries[1].SequenceNumber != 2 {
t.Errorf("Entries not sorted by timestamp")
}
// Get entries after timestamp 1
entries, err = replicator.GetEntriesAfter(ReplicationPosition{Timestamp: 1})
if err != nil {
t.Fatalf("Error getting entries: %v", err)
}
if len(entries) != 1 {
t.Fatalf("Expected 1 entry after timestamp 1, got %d", len(entries))
}
if entries[0].SequenceNumber != 2 {
t.Errorf("Expected entry with timestamp 2, got %d", entries[0].SequenceNumber)
}
}
func TestWALReplicatorBatches(t *testing.T) {
replicator := NewWALReplicator(1000)
defer replicator.Close()
// Create a batch of entries
entries := []*wal.Entry{
{
SequenceNumber: 10,
Type: wal.OpTypePut,
Key: []byte("key1"),
Value: []byte("value1"),
},
{
SequenceNumber: 11,
Type: wal.OpTypePut,
Key: []byte("key2"),
Value: []byte("value2"),
},
}
// Process the batch
replicator.OnBatchWritten(entries)
// Check entry count
if count := replicator.GetEntryCount(); count != 2 {
t.Errorf("Expected 2 entries, got %d", count)
}
// Check batch count
if count := replicator.GetBatchCount(); count != 1 {
t.Errorf("Expected 1 batch, got %d", count)
}
// Check highest timestamp
if ts := replicator.GetHighestTimestamp(); ts != 11 {
t.Errorf("Expected highest timestamp 11, got %d", ts)
}
// Get entries after timestamp 9
result, err := replicator.GetEntriesAfter(ReplicationPosition{Timestamp: 9})
if err != nil {
t.Fatalf("Error getting entries: %v", err)
}
if len(result) != 2 {
t.Fatalf("Expected 2 entries after timestamp 9, got %d", len(result))
}
}
func TestWALReplicatorProcessors(t *testing.T) {
replicator := NewWALReplicator(1000)
defer replicator.Close()
// Create a processor
processor := &MockEntryProcessor{}
// Add the processor
replicator.AddProcessor(processor)
// Create an entry and a batch
entry := &wal.Entry{
SequenceNumber: 1,
Type: wal.OpTypePut,
Key: []byte("key1"),
Value: []byte("value1"),
}
batch := []*wal.Entry{
{
SequenceNumber: 10,
Type: wal.OpTypePut,
Key: []byte("key10"),
Value: []byte("value10"),
},
{
SequenceNumber: 11,
Type: wal.OpTypePut,
Key: []byte("key11"),
Value: []byte("value11"),
},
}
// Process the entry and batch
replicator.OnEntryWritten(entry)
replicator.OnBatchWritten(batch)
// Check processor stats
entriesProcessed, batchesProcessed := processor.GetStats()
if entriesProcessed != 1 {
t.Errorf("Expected 1 entry processed, got %d", entriesProcessed)
}
if batchesProcessed != 1 {
t.Errorf("Expected 1 batch processed, got %d", batchesProcessed)
}
}
func TestWALReplicatorSubscribe(t *testing.T) {
replicator := NewWALReplicator(1000)
defer replicator.Close()
// Subscribe to entries and batches
entryChannel := replicator.SubscribeToEntries()
batchChannel := replicator.SubscribeToBatches()
// Create an entry and a batch
entry := &wal.Entry{
SequenceNumber: 1,
Type: wal.OpTypePut,
Key: []byte("key1"),
Value: []byte("value1"),
}
batch := []*wal.Entry{
{
SequenceNumber: 10,
Type: wal.OpTypePut,
Key: []byte("key10"),
Value: []byte("value10"),
},
{
SequenceNumber: 11,
Type: wal.OpTypePut,
Key: []byte("key11"),
Value: []byte("value11"),
},
}
// Create channels to receive the results
entryReceived := make(chan *wal.Entry, 1)
batchReceived := make(chan []*wal.Entry, 1)
// Start goroutines to receive from the channels
go func() {
select {
case e := <-entryChannel:
entryReceived <- e
case <-time.After(time.Second):
close(entryReceived)
}
}()
go func() {
select {
case b := <-batchChannel:
batchReceived <- b
case <-time.After(time.Second):
close(batchReceived)
}
}()
// Process the entry and batch
replicator.OnEntryWritten(entry)
replicator.OnBatchWritten(batch)
// Check that we received the entry
select {
case receivedEntry := <-entryReceived:
if receivedEntry.SequenceNumber != 1 {
t.Errorf("Expected entry with timestamp 1, got %d", receivedEntry.SequenceNumber)
}
case <-time.After(time.Second):
t.Errorf("Timeout waiting for entry")
}
// Check that we received the batch
select {
case receivedBatch := <-batchReceived:
if len(receivedBatch) != 2 {
t.Errorf("Expected batch with 2 entries, got %d", len(receivedBatch))
}
case <-time.After(time.Second):
t.Errorf("Timeout waiting for batch")
}
}
func TestWALReplicatorCleanup(t *testing.T) {
// Create a replicator with a small buffer
replicator := NewWALReplicator(10)
defer replicator.Close()
// Add more entries than the buffer can hold
for i := 0; i < 20; i++ {
entry := &wal.Entry{
SequenceNumber: uint64(i),
Type: wal.OpTypePut,
Key: []byte("key"),
Value: []byte("value"),
}
replicator.OnEntryWritten(entry)
}
// Check that some entries were cleaned up
count := replicator.GetEntryCount()
if count > 20 {
t.Errorf("Expected fewer than 20 entries after cleanup, got %d", count)
}
// The most recent entries should still be there
entries, err := replicator.GetEntriesAfter(ReplicationPosition{Timestamp: 15})
if err != nil {
t.Fatalf("Error getting entries: %v", err)
}
if len(entries) == 0 {
t.Errorf("Expected some entries after timestamp 15")
}
}
func TestWALReplicatorClose(t *testing.T) {
replicator := NewWALReplicator(1000)
// Add some entries
entry := &wal.Entry{
SequenceNumber: 1,
Type: wal.OpTypePut,
Key: []byte("key"),
Value: []byte("value"),
}
replicator.OnEntryWritten(entry)
// Close the replicator
if err := replicator.Close(); err != nil {
t.Fatalf("Error closing replicator: %v", err)
}
// Check that we can't add more entries
replicator.OnEntryWritten(entry)
// Entry count should still be 0 after closure and cleanup
if count := replicator.GetEntryCount(); count != 0 {
t.Errorf("Expected 0 entries after close, got %d", count)
}
// Try to get entries (should return an error)
_, err := replicator.GetEntriesAfter(ReplicationPosition{Timestamp: 0})
if err != ErrReplicatorClosed {
t.Errorf("Expected ErrReplicatorClosed, got %v", err)
}
}
func TestFindOldestTimestamps(t *testing.T) {
// Create a map with some timestamps
m := map[uint64]string{
1: "one",
2: "two",
3: "three",
4: "four",
5: "five",
}
// Find the 2 oldest timestamps
result := findOldestTimestamps(m, 2)
// Check the result length
if len(result) != 2 {
t.Fatalf("Expected 2 timestamps, got %d", len(result))
}
// Check that the result contains the 2 smallest timestamps
for _, ts := range result {
if ts != 1 && ts != 2 {
t.Errorf("Expected timestamp 1 or 2, got %d", ts)
}
}
}
func TestSortEntriesByTimestamp(t *testing.T) {
// Create some entries with unsorted timestamps
entries := []*wal.Entry{
{SequenceNumber: 3},
{SequenceNumber: 1},
{SequenceNumber: 2},
}
// Sort the entries
sortEntriesByTimestamp(entries)
// Check that the entries are sorted
for i := 0; i < len(entries)-1; i++ {
if entries[i].SequenceNumber > entries[i+1].SequenceNumber {
t.Errorf("Entries not sorted at index %d: %d > %d",
i, entries[i].SequenceNumber, entries[i+1].SequenceNumber)
}
}
}