- 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
420 lines
10 KiB
Go
420 lines
10 KiB
Go
package replication
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"hash/crc32"
|
|
"testing"
|
|
|
|
"github.com/KevoDB/kevo/pkg/wal"
|
|
)
|
|
|
|
func TestEntrySerializer(t *testing.T) {
|
|
// Create a serializer
|
|
serializer := NewEntrySerializer()
|
|
|
|
// Test different entry types
|
|
testCases := []struct {
|
|
name string
|
|
entry *wal.Entry
|
|
}{
|
|
{
|
|
name: "Put operation",
|
|
entry: &wal.Entry{
|
|
SequenceNumber: 123,
|
|
Type: wal.OpTypePut,
|
|
Key: []byte("test-key"),
|
|
Value: []byte("test-value"),
|
|
},
|
|
},
|
|
{
|
|
name: "Delete operation",
|
|
entry: &wal.Entry{
|
|
SequenceNumber: 456,
|
|
Type: wal.OpTypeDelete,
|
|
Key: []byte("deleted-key"),
|
|
Value: nil,
|
|
},
|
|
},
|
|
{
|
|
name: "Large entry",
|
|
entry: &wal.Entry{
|
|
SequenceNumber: 789,
|
|
Type: wal.OpTypePut,
|
|
Key: bytes.Repeat([]byte("K"), 1000),
|
|
Value: bytes.Repeat([]byte("V"), 1000),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Serialize the entry
|
|
data := serializer.SerializeEntry(tc.entry)
|
|
|
|
// Deserialize back
|
|
result, err := serializer.DeserializeEntry(data)
|
|
if err != nil {
|
|
t.Fatalf("Error deserializing entry: %v", err)
|
|
}
|
|
|
|
// Compare entries
|
|
if result.SequenceNumber != tc.entry.SequenceNumber {
|
|
t.Errorf("Expected sequence number %d, got %d",
|
|
tc.entry.SequenceNumber, result.SequenceNumber)
|
|
}
|
|
|
|
if result.Type != tc.entry.Type {
|
|
t.Errorf("Expected type %d, got %d", tc.entry.Type, result.Type)
|
|
}
|
|
|
|
if !bytes.Equal(result.Key, tc.entry.Key) {
|
|
t.Errorf("Expected key %q, got %q", tc.entry.Key, result.Key)
|
|
}
|
|
|
|
if !bytes.Equal(result.Value, tc.entry.Value) {
|
|
t.Errorf("Expected value %q, got %q", tc.entry.Value, result.Value)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEntrySerializerChecksum(t *testing.T) {
|
|
// Create a serializer with checksums enabled
|
|
serializer := NewEntrySerializer()
|
|
serializer.ChecksumEnabled = true
|
|
|
|
// Create a test entry
|
|
entry := &wal.Entry{
|
|
SequenceNumber: 123,
|
|
Type: wal.OpTypePut,
|
|
Key: []byte("test-key"),
|
|
Value: []byte("test-value"),
|
|
}
|
|
|
|
// Serialize the entry
|
|
data := serializer.SerializeEntry(entry)
|
|
|
|
// Corrupt the data
|
|
data[10]++
|
|
|
|
// Try to deserialize - should fail with checksum error
|
|
_, err := serializer.DeserializeEntry(data)
|
|
if err != ErrInvalidChecksum {
|
|
t.Errorf("Expected checksum error, got %v", err)
|
|
}
|
|
|
|
// Now disable checksum verification and try again
|
|
serializer.ChecksumEnabled = false
|
|
result, err := serializer.DeserializeEntry(data)
|
|
if err != nil {
|
|
t.Errorf("Expected no error with checksums disabled, got %v", err)
|
|
}
|
|
if result == nil {
|
|
t.Fatal("Expected entry to be returned with checksums disabled")
|
|
}
|
|
}
|
|
|
|
func TestEntrySerializerInvalidFormat(t *testing.T) {
|
|
serializer := NewEntrySerializer()
|
|
|
|
// Test with empty data
|
|
_, err := serializer.DeserializeEntry([]byte{})
|
|
if err != ErrInvalidFormat {
|
|
t.Errorf("Expected format error for empty data, got %v", err)
|
|
}
|
|
|
|
// Test with insufficient data
|
|
_, err = serializer.DeserializeEntry(make([]byte, 10))
|
|
if err != ErrInvalidFormat {
|
|
t.Errorf("Expected format error for insufficient data, got %v", err)
|
|
}
|
|
|
|
// Test with invalid key length
|
|
data := make([]byte, entryHeaderSize+4)
|
|
offset := 4
|
|
binary.LittleEndian.PutUint64(data[offset:offset+8], 123) // timestamp
|
|
offset += 8
|
|
data[offset] = wal.OpTypePut // type
|
|
offset++
|
|
binary.LittleEndian.PutUint32(data[offset:offset+4], 1000) // key length (too large)
|
|
|
|
// Calculate a valid checksum for this data
|
|
checksum := crc32.ChecksumIEEE(data[4:])
|
|
binary.LittleEndian.PutUint32(data[0:4], checksum)
|
|
|
|
_, err = serializer.DeserializeEntry(data)
|
|
if err != ErrInvalidFormat {
|
|
t.Errorf("Expected format error for invalid key length, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBatchSerializer(t *testing.T) {
|
|
// Create batch serializer
|
|
serializer := NewBatchSerializer()
|
|
|
|
// Test batch with multiple entries
|
|
entries := []*wal.Entry{
|
|
{
|
|
SequenceNumber: 101,
|
|
Type: wal.OpTypePut,
|
|
Key: []byte("key1"),
|
|
Value: []byte("value1"),
|
|
},
|
|
{
|
|
SequenceNumber: 102,
|
|
Type: wal.OpTypeDelete,
|
|
Key: []byte("key2"),
|
|
Value: nil,
|
|
},
|
|
{
|
|
SequenceNumber: 103,
|
|
Type: wal.OpTypePut,
|
|
Key: []byte("key3"),
|
|
Value: []byte("value3"),
|
|
},
|
|
}
|
|
|
|
// Serialize batch
|
|
data := serializer.SerializeBatch(entries)
|
|
|
|
// Deserialize batch
|
|
result, err := serializer.DeserializeBatch(data)
|
|
if err != nil {
|
|
t.Fatalf("Error deserializing batch: %v", err)
|
|
}
|
|
|
|
// Verify batch
|
|
if len(result) != len(entries) {
|
|
t.Fatalf("Expected %d entries, got %d", len(entries), len(result))
|
|
}
|
|
|
|
for i, entry := range entries {
|
|
if result[i].SequenceNumber != entry.SequenceNumber {
|
|
t.Errorf("Entry %d: Expected sequence number %d, got %d",
|
|
i, entry.SequenceNumber, result[i].SequenceNumber)
|
|
}
|
|
if result[i].Type != entry.Type {
|
|
t.Errorf("Entry %d: Expected type %d, got %d",
|
|
i, entry.Type, result[i].Type)
|
|
}
|
|
if !bytes.Equal(result[i].Key, entry.Key) {
|
|
t.Errorf("Entry %d: Expected key %q, got %q",
|
|
i, entry.Key, result[i].Key)
|
|
}
|
|
if !bytes.Equal(result[i].Value, entry.Value) {
|
|
t.Errorf("Entry %d: Expected value %q, got %q",
|
|
i, entry.Value, result[i].Value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEmptyBatchSerialization(t *testing.T) {
|
|
// Create batch serializer
|
|
serializer := NewBatchSerializer()
|
|
|
|
// Test empty batch
|
|
entries := []*wal.Entry{}
|
|
|
|
// Serialize batch
|
|
data := serializer.SerializeBatch(entries)
|
|
|
|
// Deserialize batch
|
|
result, err := serializer.DeserializeBatch(data)
|
|
if err != nil {
|
|
t.Fatalf("Error deserializing empty batch: %v", err)
|
|
}
|
|
|
|
// Verify result is empty
|
|
if len(result) != 0 {
|
|
t.Errorf("Expected empty batch, got %d entries", len(result))
|
|
}
|
|
}
|
|
|
|
func TestBatchSerializerChecksum(t *testing.T) {
|
|
// Create batch serializer
|
|
serializer := NewBatchSerializer()
|
|
|
|
// Test batch with single entry
|
|
entries := []*wal.Entry{
|
|
{
|
|
SequenceNumber: 101,
|
|
Type: wal.OpTypePut,
|
|
Key: []byte("key1"),
|
|
Value: []byte("value1"),
|
|
},
|
|
}
|
|
|
|
// Serialize batch
|
|
data := serializer.SerializeBatch(entries)
|
|
|
|
// Corrupt data
|
|
data[8]++
|
|
|
|
// Attempt to deserialize - should fail
|
|
_, err := serializer.DeserializeBatch(data)
|
|
if err != ErrInvalidChecksum {
|
|
t.Errorf("Expected checksum error for corrupted batch, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBatchSerializerInvalidFormat(t *testing.T) {
|
|
serializer := NewBatchSerializer()
|
|
|
|
// Test with empty data
|
|
_, err := serializer.DeserializeBatch([]byte{})
|
|
if err != ErrInvalidFormat {
|
|
t.Errorf("Expected format error for empty data, got %v", err)
|
|
}
|
|
|
|
// Test with insufficient data
|
|
_, err = serializer.DeserializeBatch(make([]byte, 10))
|
|
if err != ErrInvalidFormat {
|
|
t.Errorf("Expected format error for insufficient data, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEstimateEntrySize(t *testing.T) {
|
|
// Test entries with different sizes
|
|
testCases := []struct {
|
|
name string
|
|
entry *wal.Entry
|
|
expected int
|
|
}{
|
|
{
|
|
name: "Basic put entry",
|
|
entry: &wal.Entry{
|
|
SequenceNumber: 101,
|
|
Type: wal.OpTypePut,
|
|
Key: []byte("key"),
|
|
Value: []byte("value"),
|
|
},
|
|
expected: entryHeaderSize + 3 + 4 + 5, // header + key_len + value_len + value
|
|
},
|
|
{
|
|
name: "Delete entry (no value)",
|
|
entry: &wal.Entry{
|
|
SequenceNumber: 102,
|
|
Type: wal.OpTypeDelete,
|
|
Key: []byte("delete-key"),
|
|
Value: nil,
|
|
},
|
|
expected: entryHeaderSize + 10, // header + key_len
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
size := EstimateEntrySize(tc.entry)
|
|
if size != tc.expected {
|
|
t.Errorf("Expected size %d, got %d", tc.expected, size)
|
|
}
|
|
|
|
// Verify estimate matches actual size
|
|
serializer := NewEntrySerializer()
|
|
data := serializer.SerializeEntry(tc.entry)
|
|
if len(data) != size {
|
|
t.Errorf("Estimated size %d doesn't match actual size %d",
|
|
size, len(data))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEstimateBatchSize(t *testing.T) {
|
|
// Test batches with different contents
|
|
testCases := []struct {
|
|
name string
|
|
entries []*wal.Entry
|
|
expected int
|
|
}{
|
|
{
|
|
name: "Empty batch",
|
|
entries: []*wal.Entry{},
|
|
expected: 12, // Just batch header
|
|
},
|
|
{
|
|
name: "Batch with one entry",
|
|
entries: []*wal.Entry{
|
|
{
|
|
SequenceNumber: 101,
|
|
Type: wal.OpTypePut,
|
|
Key: []byte("key1"),
|
|
Value: []byte("value1"),
|
|
},
|
|
},
|
|
expected: 12 + 4 + entryHeaderSize + 4 + 4 + 6, // batch header + entry size field + entry header + key + value size + value
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
size := EstimateBatchSize(tc.entries)
|
|
if size != tc.expected {
|
|
t.Errorf("Expected size %d, got %d", tc.expected, size)
|
|
}
|
|
|
|
// Verify estimate matches actual size
|
|
serializer := NewBatchSerializer()
|
|
data := serializer.SerializeBatch(tc.entries)
|
|
if len(data) != size {
|
|
t.Errorf("Estimated size %d doesn't match actual size %d",
|
|
size, len(data))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSerializeToBuffer(t *testing.T) {
|
|
serializer := NewEntrySerializer()
|
|
|
|
// Create a test entry
|
|
entry := &wal.Entry{
|
|
SequenceNumber: 101,
|
|
Type: wal.OpTypePut,
|
|
Key: []byte("key1"),
|
|
Value: []byte("value1"),
|
|
}
|
|
|
|
// Estimate the size
|
|
estimatedSize := EstimateEntrySize(entry)
|
|
|
|
// Create a buffer of the estimated size
|
|
buffer := make([]byte, estimatedSize)
|
|
|
|
// Serialize to buffer
|
|
n, err := serializer.SerializeEntryToBuffer(entry, buffer)
|
|
if err != nil {
|
|
t.Fatalf("Error serializing to buffer: %v", err)
|
|
}
|
|
|
|
// Check bytes written
|
|
if n != estimatedSize {
|
|
t.Errorf("Expected %d bytes written, got %d", estimatedSize, n)
|
|
}
|
|
|
|
// Verify by deserializing
|
|
result, err := serializer.DeserializeEntry(buffer)
|
|
if err != nil {
|
|
t.Fatalf("Error deserializing from buffer: %v", err)
|
|
}
|
|
|
|
// Check result
|
|
if result.SequenceNumber != entry.SequenceNumber {
|
|
t.Errorf("Expected sequence number %d, got %d",
|
|
entry.SequenceNumber, result.SequenceNumber)
|
|
}
|
|
if !bytes.Equal(result.Key, entry.Key) {
|
|
t.Errorf("Expected key %q, got %q", entry.Key, result.Key)
|
|
}
|
|
if !bytes.Equal(result.Value, entry.Value) {
|
|
t.Errorf("Expected value %q, got %q", entry.Value, result.Value)
|
|
}
|
|
|
|
// Test with too small buffer
|
|
smallBuffer := make([]byte, estimatedSize - 1)
|
|
_, err = serializer.SerializeEntryToBuffer(entry, smallBuffer)
|
|
if err != ErrBufferTooSmall {
|
|
t.Errorf("Expected buffer too small error, got %v", err)
|
|
}
|
|
} |