You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

386 lines
9.4 KiB

package blockvol
import (
"bytes"
"log"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestFlusher(t *testing.T) {
tests := []struct {
name string
run func(t *testing.T)
}{
{name: "flush_moves_data", run: testFlushMovesData},
{name: "flush_idempotent", run: testFlushIdempotent},
{name: "flush_concurrent_writes", run: testFlushConcurrentWrites},
{name: "flush_frees_wal_space", run: testFlushFreesWALSpace},
{name: "flush_partial", run: testFlushPartial},
// Phase 3 Task 1.6: NotifyUrgent.
{name: "flusher_notify_urgent_triggers_flush", run: testFlusherNotifyUrgentTriggersFlush},
// Phase 3 bug fix: P3-BUG-4 error logging.
{name: "flusher_error_logged", run: testFlusherErrorLogged},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.run(t)
})
}
}
func createTestVolWithFlusher(t *testing.T) (*BlockVol, *Flusher) {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, "test.blockvol")
v, err := CreateBlockVol(path, CreateOptions{
VolumeSize: 1 * 1024 * 1024, // 1MB
BlockSize: 4096,
WALSize: 256 * 1024, // 256KB WAL
})
if err != nil {
t.Fatalf("CreateBlockVol: %v", err)
}
f := NewFlusher(FlusherConfig{
FD: v.fd,
Super: &v.super,
SuperMu: &v.superMu,
WAL: v.wal,
DirtyMap: v.dirtyMap,
Interval: 1 * time.Hour, // don't auto-flush in tests
})
return v, f
}
func testFlushMovesData(t *testing.T) {
v, f := createTestVolWithFlusher(t)
defer v.Close()
// Write 10 blocks.
for i := uint64(0); i < 10; i++ {
if err := v.WriteLBA(i, makeBlock(byte('A'+i))); err != nil {
t.Fatalf("WriteLBA(%d): %v", i, err)
}
}
if v.dirtyMap.Len() != 10 {
t.Fatalf("dirty map len = %d, want 10", v.dirtyMap.Len())
}
// Run flusher.
if err := f.FlushOnce(); err != nil {
t.Fatalf("FlushOnce: %v", err)
}
// Dirty map should be empty.
if v.dirtyMap.Len() != 0 {
t.Errorf("after flush: dirty map len = %d, want 0", v.dirtyMap.Len())
}
// Checkpoint should have advanced.
if f.CheckpointLSN() == 0 {
t.Error("checkpoint LSN should be > 0 after flush")
}
// Read from extent (dirty map is empty, so reads go to extent).
for i := uint64(0); i < 10; i++ {
got, err := v.ReadLBA(i, 4096)
if err != nil {
t.Fatalf("ReadLBA(%d) after flush: %v", i, err)
}
if !bytes.Equal(got, makeBlock(byte('A'+i))) {
t.Errorf("block %d: data mismatch after flush", i)
}
}
}
func testFlushIdempotent(t *testing.T) {
v, f := createTestVolWithFlusher(t)
defer v.Close()
data := makeBlock('X')
if err := v.WriteLBA(0, data); err != nil {
t.Fatalf("WriteLBA: %v", err)
}
// Flush twice.
if err := f.FlushOnce(); err != nil {
t.Fatalf("FlushOnce 1: %v", err)
}
if err := f.FlushOnce(); err != nil {
t.Fatalf("FlushOnce 2: %v", err)
}
// Data should still be correct.
got, err := v.ReadLBA(0, 4096)
if err != nil {
t.Fatalf("ReadLBA after double flush: %v", err)
}
if !bytes.Equal(got, data) {
t.Error("data mismatch after double flush")
}
}
func testFlushConcurrentWrites(t *testing.T) {
v, f := createTestVolWithFlusher(t)
defer v.Close()
// Write blocks 0-4.
for i := uint64(0); i < 5; i++ {
if err := v.WriteLBA(i, makeBlock(byte('A'+i))); err != nil {
t.Fatalf("WriteLBA(%d): %v", i, err)
}
}
// Flush (moves blocks 0-4 to extent).
if err := f.FlushOnce(); err != nil {
t.Fatalf("FlushOnce: %v", err)
}
// Write blocks 5-9 AFTER flush.
for i := uint64(5); i < 10; i++ {
if err := v.WriteLBA(i, makeBlock(byte('A'+i))); err != nil {
t.Fatalf("WriteLBA(%d): %v", i, err)
}
}
// Blocks 0-4 should read from extent, blocks 5-9 from WAL.
for i := uint64(0); i < 10; i++ {
got, err := v.ReadLBA(i, 4096)
if err != nil {
t.Fatalf("ReadLBA(%d): %v", i, err)
}
if !bytes.Equal(got, makeBlock(byte('A'+i))) {
t.Errorf("block %d: data mismatch", i)
}
}
// Dirty map should have 5 entries (blocks 5-9).
if v.dirtyMap.Len() != 5 {
t.Errorf("dirty map len = %d, want 5", v.dirtyMap.Len())
}
// Also: overwrite block 0 after flush -- new write should go to WAL.
newData := makeBlock('Z')
if err := v.WriteLBA(0, newData); err != nil {
t.Fatalf("WriteLBA(0) overwrite: %v", err)
}
got, err := v.ReadLBA(0, 4096)
if err != nil {
t.Fatalf("ReadLBA(0) after overwrite: %v", err)
}
if !bytes.Equal(got, newData) {
t.Error("block 0: should return overwritten data 'Z'")
}
}
func testFlushFreesWALSpace(t *testing.T) {
v, f := createTestVolWithFlusher(t)
defer v.Close()
// Write enough blocks to fill a significant portion of WAL.
entrySize := uint64(walEntryHeaderSize + 4096)
walCapacity := v.super.WALSize / entrySize
// Write ~80% of capacity.
writeCount := int(walCapacity * 80 / 100)
for i := 0; i < writeCount; i++ {
if err := v.WriteLBA(uint64(i), makeBlock(byte(i%26+'A'))); err != nil {
t.Fatalf("WriteLBA(%d): %v", i, err)
}
}
// Try to write more -- should eventually fail with WAL full.
var walFullBefore bool
for i := writeCount; i < writeCount+int(walCapacity); i++ {
if err := v.WriteLBA(uint64(i%writeCount), makeBlock('X')); err != nil {
walFullBefore = true
break
}
}
// Flush to free WAL space.
if err := f.FlushOnce(); err != nil {
t.Fatalf("FlushOnce: %v", err)
}
// WAL tail should have advanced (free space available).
// New writes should succeed.
if err := v.WriteLBA(0, makeBlock('Y')); err != nil {
t.Fatalf("WriteLBA after flush: %v", err)
}
// Log whether WAL was full before flush.
if walFullBefore {
t.Log("WAL was full before flush, writes succeeded after flush")
}
}
func testFlushPartial(t *testing.T) {
v, f := createTestVolWithFlusher(t)
defer v.Close()
// Write blocks 0-4.
for i := uint64(0); i < 5; i++ {
if err := v.WriteLBA(i, makeBlock(byte('A'+i))); err != nil {
t.Fatalf("WriteLBA(%d): %v", i, err)
}
}
// Flush once (all 5 blocks).
if err := f.FlushOnce(); err != nil {
t.Fatalf("FlushOnce: %v", err)
}
checkpointAfterFirst := f.CheckpointLSN()
// Write blocks 5-9.
for i := uint64(5); i < 10; i++ {
if err := v.WriteLBA(i, makeBlock(byte('A'+i))); err != nil {
t.Fatalf("WriteLBA(%d): %v", i, err)
}
}
// Simulate partial flush: flusher runs again, should handle new entries.
if err := f.FlushOnce(); err != nil {
t.Fatalf("FlushOnce 2: %v", err)
}
checkpointAfterSecond := f.CheckpointLSN()
if checkpointAfterSecond <= checkpointAfterFirst {
t.Errorf("checkpoint should advance: first=%d, second=%d", checkpointAfterFirst, checkpointAfterSecond)
}
// All blocks should be readable from extent.
for i := uint64(0); i < 10; i++ {
got, err := v.ReadLBA(i, 4096)
if err != nil {
t.Fatalf("ReadLBA(%d) after two flushes: %v", i, err)
}
if !bytes.Equal(got, makeBlock(byte('A'+i))) {
t.Errorf("block %d: data mismatch after two flushes", i)
}
}
}
// --- Phase 3 Task 1.6: NotifyUrgent test ---
func testFlusherNotifyUrgentTriggersFlush(t *testing.T) {
v, f := createTestVolWithFlusher(t)
defer v.Close()
data := makeBlock('U')
if err := v.WriteLBA(0, data); err != nil {
t.Fatalf("WriteLBA: %v", err)
}
if v.dirtyMap.Len() != 1 {
t.Fatalf("dirty map len = %d, want 1", v.dirtyMap.Len())
}
// Start flusher in background with long interval.
go f.Run()
defer f.Stop()
// NotifyUrgent should trigger a flush.
f.NotifyUrgent()
// Wait for flush to complete.
deadline := time.After(2 * time.Second)
for {
if v.dirtyMap.Len() == 0 {
break
}
select {
case <-deadline:
t.Fatal("NotifyUrgent did not trigger flush within 2s")
default:
time.Sleep(5 * time.Millisecond)
}
}
got, err := v.ReadLBA(0, 4096)
if err != nil {
t.Fatalf("ReadLBA after urgent flush: %v", err)
}
if !bytes.Equal(got, data) {
t.Error("data mismatch after urgent flush")
}
}
// testFlusherErrorLogged verifies that flusher I/O errors are logged
// and that consecutive errors are deduplicated.
func testFlusherErrorLogged(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "errlog.blockvol")
v, err := CreateBlockVol(path, CreateOptions{
VolumeSize: 1 * 1024 * 1024,
BlockSize: 4096,
WALSize: 256 * 1024,
})
if err != nil {
t.Fatalf("CreateBlockVol: %v", err)
}
// Write a block so dirty map has an entry.
if err := v.WriteLBA(0, makeBlock('E')); err != nil {
t.Fatalf("WriteLBA: %v", err)
}
// Create a flusher with a captured logger and a CLOSED fd to force error.
var logBuf strings.Builder
logger := log.New(&logBuf, "", 0)
closedFD, err := openAndClose(path)
if err != nil {
t.Fatalf("openAndClose: %v", err)
}
f := NewFlusher(FlusherConfig{
FD: closedFD,
Super: &v.super,
SuperMu: &v.superMu,
WAL: v.wal,
DirtyMap: v.dirtyMap,
Interval: 1 * time.Hour,
Logger: logger,
})
// Run flusher briefly -- FlushOnce should error, and Run should log it.
go f.Run()
f.Notify()
time.Sleep(50 * time.Millisecond)
// Send another notify to test dedup.
f.Notify()
time.Sleep(50 * time.Millisecond)
f.Stop()
logged := logBuf.String()
if !strings.Contains(logged, "flusher error:") {
t.Fatalf("expected 'flusher error:' in log, got: %q", logged)
}
// Should only be logged once (dedup of consecutive errors).
count := strings.Count(logged, "flusher error:")
if count != 1 {
t.Errorf("expected 1 log line (dedup), got %d: %q", count, logged)
}
v.Close()
}
// openAndClose opens a file and immediately closes the fd, returning
// the now-invalid *os.File for injection into flusher tests.
func openAndClose(path string) (*os.File, error) {
fd, err := os.Open(path)
if err != nil {
return nil, err
}
fd.Close()
return fd, nil
}