- 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
402 lines
9.5 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|