kevo/pkg/replication/batch_test.go
Jeremy Tregunna 01cd007e51 feat: Extend WAL to support observers & replication protocol
- WAL package now can notify observers when it writes entries
- WAL can retrieve entries by sequence number
- WAL implements file retention management
- Add replication protocol defined using protobufs
- Implemented compression support for zstd and snappy
- State machine for replication added
- Batch management for streaming from the WAL
2025-04-29 15:03:03 -06:00

355 lines
9.1 KiB
Go

package replication
import (
"errors"
"testing"
proto "github.com/KevoDB/kevo/pkg/replication/proto"
"github.com/KevoDB/kevo/pkg/wal"
)
func TestWALBatcher(t *testing.T) {
// Create a new batcher with a small max batch size
batcher := NewWALBatcher(10, proto.CompressionCodec_NONE, false)
// Create test entries
entries := []*wal.Entry{
{
SequenceNumber: 1,
Type: wal.OpTypePut,
Key: []byte("key1"),
Value: []byte("value1"),
},
{
SequenceNumber: 2,
Type: wal.OpTypePut,
Key: []byte("key2"),
Value: []byte("value2"),
},
{
SequenceNumber: 3,
Type: wal.OpTypeDelete,
Key: []byte("key3"),
},
}
// Add entries and check batch status
for i, entry := range entries {
ready, err := batcher.AddEntry(entry)
if err != nil {
t.Fatalf("Failed to add entry %d: %v", i, err)
}
// The batch shouldn't be ready yet with these small entries
if ready {
t.Logf("Batch ready after entry %d (expected to fit more entries)", i)
}
}
// Verify batch content
if batcher.GetBatchCount() != 3 {
t.Errorf("Expected batch to contain 3 entries, got %d", batcher.GetBatchCount())
}
// Get the batch and verify it's the correct format
batch := batcher.GetBatch()
if len(batch.Entries) != 3 {
t.Errorf("Expected batch to contain 3 entries, got %d", len(batch.Entries))
}
if batch.Compressed {
t.Errorf("Expected batch to be uncompressed")
}
if batch.Codec != proto.CompressionCodec_NONE {
t.Errorf("Expected codec to be NONE, got %v", batch.Codec)
}
// Verify batch is now empty
if batcher.GetBatchCount() != 0 {
t.Errorf("Expected batch to be empty after GetBatch(), got %d entries", batcher.GetBatchCount())
}
}
func TestWALBatcherSizeLimit(t *testing.T) {
// Create a batcher with a very small limit (2KB)
batcher := NewWALBatcher(2, proto.CompressionCodec_NONE, false)
// Create a large entry (approximately 1.5KB)
largeValue := make([]byte, 1500)
for i := range largeValue {
largeValue[i] = byte(i % 256)
}
entry1 := &wal.Entry{
SequenceNumber: 1,
Type: wal.OpTypePut,
Key: []byte("large-key-1"),
Value: largeValue,
}
// Add the first large entry
ready, err := batcher.AddEntry(entry1)
if err != nil {
t.Fatalf("Failed to add large entry 1: %v", err)
}
if ready {
t.Errorf("Batch shouldn't be ready after first large entry")
}
// Create another large entry
entry2 := &wal.Entry{
SequenceNumber: 2,
Type: wal.OpTypePut,
Key: []byte("large-key-2"),
Value: largeValue,
}
// Add the second large entry, this should make the batch ready
ready, err = batcher.AddEntry(entry2)
if err != nil {
t.Fatalf("Failed to add large entry 2: %v", err)
}
if !ready {
t.Errorf("Batch should be ready after second large entry")
}
// Verify batch is not empty
batchCount := batcher.GetBatchCount()
if batchCount == 0 {
t.Errorf("Expected batch to contain entries, got 0")
}
// Get the batch and verify
batch := batcher.GetBatch()
if len(batch.Entries) == 0 {
t.Errorf("Expected batch to contain entries, got 0")
}
}
func TestWALBatcherWithTransactionBoundaries(t *testing.T) {
// Create a batcher that respects transaction boundaries
batcher := NewWALBatcher(10, proto.CompressionCodec_NONE, true)
// Create a batch entry (simulating a transaction start)
batchEntry := &wal.Entry{
SequenceNumber: 1,
Type: wal.OpTypeBatch,
Key: []byte{}, // Batch entries might have a special format
}
// Add the batch entry
ready, err := batcher.AddEntry(batchEntry)
if err != nil {
t.Fatalf("Failed to add batch entry: %v", err)
}
// Add a few more entries
for i := 2; i <= 5; i++ {
entry := &wal.Entry{
SequenceNumber: uint64(i),
Type: wal.OpTypePut,
Key: []byte("key"),
Value: []byte("value"),
}
ready, err = batcher.AddEntry(entry)
if err != nil {
t.Fatalf("Failed to add entry %d: %v", i, err)
}
// When we reach sequence 1 (the transaction boundary), the batch should be ready
if i == 1 && ready {
t.Logf("Batch correctly marked as ready at transaction boundary")
}
}
// Get the batch
batch := batcher.GetBatch()
if len(batch.Entries) != 5 {
t.Errorf("Expected batch to contain 5 entries, got %d", len(batch.Entries))
}
}
func TestWALBatcherReset(t *testing.T) {
// Create a batcher
batcher := NewWALBatcher(10, proto.CompressionCodec_NONE, false)
// Add an entry
entry := &wal.Entry{
SequenceNumber: 1,
Type: wal.OpTypePut,
Key: []byte("key"),
Value: []byte("value"),
}
_, err := batcher.AddEntry(entry)
if err != nil {
t.Fatalf("Failed to add entry: %v", err)
}
// Verify the entry is in the buffer
if batcher.GetBatchCount() != 1 {
t.Errorf("Expected batch to contain 1 entry, got %d", batcher.GetBatchCount())
}
// Reset the batcher
batcher.Reset()
// Verify the buffer is empty
if batcher.GetBatchCount() != 0 {
t.Errorf("Expected batch to be empty after reset, got %d entries", batcher.GetBatchCount())
}
}
func TestWALBatchApplier(t *testing.T) {
// Create a batch applier starting at sequence 0
applier := NewWALBatchApplier(0)
// Create a set of proto entries with sequential sequence numbers
protoEntries := createSequentialProtoEntries(1, 5)
// Mock apply function that just counts calls
applyCount := 0
applyFn := func(entry *wal.Entry) error {
applyCount++
return nil
}
// Apply the entries
maxApplied, hasGap, err := applier.ApplyEntries(protoEntries, applyFn)
if err != nil {
t.Fatalf("Failed to apply entries: %v", err)
}
if hasGap {
t.Errorf("Unexpected gap reported")
}
if maxApplied != 5 {
t.Errorf("Expected max applied sequence to be 5, got %d", maxApplied)
}
if applyCount != 5 {
t.Errorf("Expected apply function to be called 5 times, got %d", applyCount)
}
// Verify tracking
if applier.GetMaxApplied() != 5 {
t.Errorf("Expected GetMaxApplied to return 5, got %d", applier.GetMaxApplied())
}
if applier.GetExpectedNext() != 6 {
t.Errorf("Expected GetExpectedNext to return 6, got %d", applier.GetExpectedNext())
}
// Test acknowledgement
applier.AcknowledgeUpTo(5)
if applier.GetLastAcknowledged() != 5 {
t.Errorf("Expected GetLastAcknowledged to return 5, got %d", applier.GetLastAcknowledged())
}
}
func TestWALBatchApplierWithGap(t *testing.T) {
// Create a batch applier starting at sequence 0
applier := NewWALBatchApplier(0)
// Create a set of proto entries with a gap
protoEntries := createSequentialProtoEntries(2, 5) // Start at 2 instead of expected 1
// Apply the entries
_, hasGap, err := applier.ApplyEntries(protoEntries, func(entry *wal.Entry) error {
return nil
})
// Should detect a gap
if !hasGap {
t.Errorf("Expected gap to be detected")
}
if err == nil {
t.Errorf("Expected error for sequence gap")
}
}
func TestWALBatchApplierWithApplyError(t *testing.T) {
// Create a batch applier starting at sequence 0
applier := NewWALBatchApplier(0)
// Create a set of proto entries
protoEntries := createSequentialProtoEntries(1, 5)
// Mock apply function that returns an error
applyErr := errors.New("apply error")
applyFn := func(entry *wal.Entry) error {
return applyErr
}
// Apply the entries
_, _, err := applier.ApplyEntries(protoEntries, applyFn)
if err == nil {
t.Errorf("Expected error from apply function")
}
}
func TestWALBatchApplierReset(t *testing.T) {
// Create a batch applier and apply some entries
applier := NewWALBatchApplier(0)
// Apply entries up to sequence 5
protoEntries := createSequentialProtoEntries(1, 5)
applier.ApplyEntries(protoEntries, func(entry *wal.Entry) error {
return nil
})
// Reset to sequence 10
applier.Reset(10)
// Verify state was reset
if applier.GetMaxApplied() != 10 {
t.Errorf("Expected max applied to be 10 after reset, got %d", applier.GetMaxApplied())
}
if applier.GetLastAcknowledged() != 10 {
t.Errorf("Expected last acknowledged to be 10 after reset, got %d", applier.GetLastAcknowledged())
}
if applier.GetExpectedNext() != 11 {
t.Errorf("Expected expected next to be 11 after reset, got %d", applier.GetExpectedNext())
}
// Apply entries starting from sequence 11
protoEntries = createSequentialProtoEntries(11, 15)
_, hasGap, err := applier.ApplyEntries(protoEntries, func(entry *wal.Entry) error {
return nil
})
// Should not detect a gap
if hasGap {
t.Errorf("Unexpected gap detected after reset")
}
if err != nil {
t.Errorf("Unexpected error after reset: %v", err)
}
}
// Helper function to create a sequence of proto entries
func createSequentialProtoEntries(start, end uint64) []*proto.WALEntry {
var entries []*proto.WALEntry
for seq := start; seq <= end; seq++ {
// Create a simple WAL entry
walEntry := &wal.Entry{
SequenceNumber: seq,
Type: wal.OpTypePut,
Key: []byte("key"),
Value: []byte("value"),
}
// Serialize it
payload, _ := SerializeWALEntry(walEntry)
// Create proto entry
protoEntry := &proto.WALEntry{
SequenceNumber: seq,
Payload: payload,
FragmentType: proto.FragmentType_FULL,
}
entries = append(entries, protoEntry)
}
return entries
}