package wal import ( "errors" "math" "sync" "sync/atomic" "testing" "github.com/dailz/go-kv" ) func TestSequenceAllocation(t *testing.T) { sm := NewSequenceManager(0) base, err := sm.AllocateBatch(5) if err != nil { t.Fatalf("AllocateBatch(5): %v", err) } if base != 0 { t.Fatalf("expected base=0, got %d", base) } base, err = sm.AllocateBatch(3) if err != nil { t.Fatalf("AllocateBatch(3): %v", err) } if base != 5 { t.Fatalf("expected base=5, got %d", base) } base, err = sm.AllocateBatch(1) if err != nil { t.Fatalf("AllocateBatch(1): %v", err) } if base != 8 { t.Fatalf("expected base=8, got %d", base) } if sm.NextSequence() != 9 { t.Fatalf("expected NextSequence=9, got %d", sm.NextSequence()) } } func TestSequenceOverflow(t *testing.T) { nearMax := uint64(math.MaxUint64 - 2) sm := NewSequenceManager(nearMax) // Remaining: MaxUint64-2, MaxUint64-1, MaxUint64 = 3 slots. // Asking for 5 should overflow. _, err := sm.AllocateBatch(5) if !errors.Is(err, go_kv.ErrSequenceExhausted) { t.Fatalf("expected ErrSequenceExhausted, got %v", err) } // 3 should still succeed. base, err := sm.AllocateBatch(3) if err != nil { t.Fatalf("AllocateBatch(3): %v", err) } if base != nearMax { t.Fatalf("expected base=%d, got %d", nearMax, base) } // Now any further allocation should fail. _, err = sm.AllocateBatch(1) if !errors.Is(err, go_kv.ErrSequenceExhausted) { t.Fatalf("expected ErrSequenceExhausted after exhaustion, got %v", err) } } func TestPublishAdvance(t *testing.T) { sm := NewSequenceManager(0) sm.Publish(10) if sm.Published() != 10 { t.Fatalf("expected Published=10, got %d", sm.Published()) } // Publishing a lower value must not decrease the watermark. sm.Publish(5) if sm.Published() != 10 { t.Fatalf("expected Published=10 (no decrease), got %d", sm.Published()) } sm.Publish(15) if sm.Published() != 15 { t.Fatalf("expected Published=15, got %d", sm.Published()) } } func TestMarkDurable(t *testing.T) { sm := NewSequenceManager(0) sm.MarkDurable(8) if sm.Durable() != 8 { t.Fatalf("expected Durable=8, got %d", sm.Durable()) } // Lower value must not decrease. sm.MarkDurable(3) if sm.Durable() != 8 { t.Fatalf("expected Durable=8 (no decrease), got %d", sm.Durable()) } } func TestZeroCountRejected(t *testing.T) { sm := NewSequenceManager(0) _, err := sm.AllocateBatch(0) if !errors.Is(err, go_kv.ErrSequenceExhausted) { t.Fatalf("expected ErrSequenceExhausted for count=0, got %v", err) } } func TestConcurrentAllocation(t *testing.T) { const goroutines = 16 const batchSize uint32 = 100 sm := NewSequenceManager(0) var totalAllocated atomic.Uint64 var wg sync.WaitGroup wg.Add(goroutines) for i := 0; i < goroutines; i++ { go func() { defer wg.Done() for j := 0; j < 50; j++ { base, err := sm.AllocateBatch(batchSize) if err != nil { t.Errorf("AllocateBatch failed: %v", err) return } totalAllocated.Add(uint64(batchSize)) // Verify no overlap: base must be aligned to batchSize increments // and within valid range. The key property is no gaps. _ = base } }() } wg.Wait() expected := uint64(goroutines) * 50 * uint64(batchSize) if totalAllocated.Load() != expected { t.Fatalf("expected total allocated=%d, got %d", expected, totalAllocated.Load()) } if sm.NextSequence() != expected { t.Fatalf("expected NextSequence=%d, got %d", expected, sm.NextSequence()) } }