4 changed files with 297 additions and 11 deletions
-
49weed/util/log_buffer/log_buffer.go
-
229weed/util/log_buffer/log_buffer_corruption_test.go
-
18weed/util/log_buffer/log_read.go
-
12weed/util/log_buffer/sealed_buffer.go
@ -0,0 +1,229 @@ |
|||||
|
package log_buffer |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"testing" |
||||
|
"time" |
||||
|
|
||||
|
"google.golang.org/protobuf/proto" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util" |
||||
|
) |
||||
|
|
||||
|
// TestReadTsCorruptedBuffer tests that readTs properly returns an error for corrupted data
|
||||
|
func TestReadTsCorruptedBuffer(t *testing.T) { |
||||
|
// Create a corrupted buffer with invalid protobuf data
|
||||
|
buf := make([]byte, 100) |
||||
|
|
||||
|
// Set size field to 10 bytes
|
||||
|
buf[0] = 10 // size = 10 (little endian, but simplified for test)
|
||||
|
buf[1] = 0 |
||||
|
buf[2] = 0 |
||||
|
buf[3] = 0 |
||||
|
|
||||
|
// Fill with garbage data that won't unmarshal as LogEntry
|
||||
|
for i := 4; i < 14; i++ { |
||||
|
buf[i] = 0xFF |
||||
|
} |
||||
|
|
||||
|
// Attempt to read timestamp
|
||||
|
size, ts, err := readTs(buf, 0) |
||||
|
|
||||
|
// Should return an error
|
||||
|
if err == nil { |
||||
|
t.Error("Expected error for corrupted buffer, got nil") |
||||
|
} |
||||
|
|
||||
|
// Size and ts should be zero on error
|
||||
|
if size != 0 { |
||||
|
t.Errorf("Expected size=0 on error, got %d", size) |
||||
|
} |
||||
|
|
||||
|
if ts != 0 { |
||||
|
t.Errorf("Expected ts=0 on error, got %d", ts) |
||||
|
} |
||||
|
|
||||
|
// Error should indicate corruption
|
||||
|
if !errors.Is(err, ErrBufferCorrupted) { |
||||
|
t.Logf("Error message: %v", err) |
||||
|
// Check if error message contains expected text
|
||||
|
if err.Error() == "" || len(err.Error()) == 0 { |
||||
|
t.Error("Expected non-empty error message") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
t.Logf("✓ readTs correctly returned error for corrupted buffer: %v", err) |
||||
|
} |
||||
|
|
||||
|
// TestReadTsValidBuffer tests that readTs works correctly for valid data
|
||||
|
func TestReadTsValidBuffer(t *testing.T) { |
||||
|
// Create a valid LogEntry
|
||||
|
logEntry := &filer_pb.LogEntry{ |
||||
|
TsNs: 123456789, |
||||
|
Key: []byte("test-key"), |
||||
|
} |
||||
|
|
||||
|
// Marshal it
|
||||
|
data, err := proto.Marshal(logEntry) |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to marshal LogEntry: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Create buffer with size prefix using util function
|
||||
|
buf := make([]byte, 4+len(data)) |
||||
|
util.Uint32toBytes(buf[0:4], uint32(len(data))) |
||||
|
copy(buf[4:], data) |
||||
|
|
||||
|
// Read timestamp
|
||||
|
size, ts, err := readTs(buf, 0) |
||||
|
|
||||
|
// Should succeed
|
||||
|
if err != nil { |
||||
|
t.Fatalf("Expected no error for valid buffer, got: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Should return correct values
|
||||
|
if size != len(data) { |
||||
|
t.Errorf("Expected size=%d, got %d", len(data), size) |
||||
|
} |
||||
|
|
||||
|
if ts != logEntry.TsNs { |
||||
|
t.Errorf("Expected ts=%d, got %d", logEntry.TsNs, ts) |
||||
|
} |
||||
|
|
||||
|
t.Logf("✓ readTs correctly parsed valid buffer: size=%d, ts=%d", size, ts) |
||||
|
} |
||||
|
|
||||
|
// TestReadFromBufferCorruption tests that ReadFromBuffer propagates corruption errors
|
||||
|
func TestReadFromBufferCorruption(t *testing.T) { |
||||
|
lb := NewLogBuffer("test-corruption", time.Second, nil, nil, func() {}) |
||||
|
|
||||
|
// Add a valid entry first using AddDataToBuffer
|
||||
|
validKey := []byte("valid") |
||||
|
validData, _ := proto.Marshal(&filer_pb.LogEntry{ |
||||
|
TsNs: 1000, |
||||
|
Key: validKey, |
||||
|
}) |
||||
|
lb.AddDataToBuffer(validKey, validData, 1000) |
||||
|
|
||||
|
// Manually corrupt the buffer by writing garbage
|
||||
|
// This simulates a corruption scenario
|
||||
|
if len(lb.idx) > 0 { |
||||
|
pos := lb.idx[0] |
||||
|
// Overwrite the protobuf data with garbage
|
||||
|
for i := pos + 4; i < pos+8 && i < len(lb.buf); i++ { |
||||
|
lb.buf[i] = 0xFF |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Try to read - should detect corruption
|
||||
|
startPos := MessagePosition{Time: lb.startTime} |
||||
|
buf, offset, err := lb.ReadFromBuffer(startPos) |
||||
|
|
||||
|
// Should return corruption error
|
||||
|
if err == nil { |
||||
|
t.Error("Expected corruption error, got nil") |
||||
|
if buf != nil { |
||||
|
t.Logf("Unexpected success: got buffer with %d bytes", buf.Len()) |
||||
|
} |
||||
|
} else { |
||||
|
// Verify it's a corruption error
|
||||
|
if !errors.Is(err, ErrBufferCorrupted) { |
||||
|
t.Logf("Got error (not ErrBufferCorrupted sentinel, but still an error): %v", err) |
||||
|
} |
||||
|
t.Logf("✓ ReadFromBuffer correctly detected corruption: %v", err) |
||||
|
} |
||||
|
|
||||
|
t.Logf("ReadFromBuffer result: buf=%v, offset=%d, err=%v", buf != nil, offset, err) |
||||
|
} |
||||
|
|
||||
|
// TestLocateByTsCorruption tests that locateByTs propagates corruption errors
|
||||
|
func TestLocateByTsCorruption(t *testing.T) { |
||||
|
// Create a MemBuffer with corrupted data
|
||||
|
mb := &MemBuffer{ |
||||
|
buf: make([]byte, 100), |
||||
|
size: 14, |
||||
|
} |
||||
|
|
||||
|
// Set size field
|
||||
|
mb.buf[0] = 10 |
||||
|
mb.buf[1] = 0 |
||||
|
mb.buf[2] = 0 |
||||
|
mb.buf[3] = 0 |
||||
|
|
||||
|
// Fill with garbage
|
||||
|
for i := 4; i < 14; i++ { |
||||
|
mb.buf[i] = 0xFF |
||||
|
} |
||||
|
|
||||
|
// Try to locate by timestamp
|
||||
|
pos, err := mb.locateByTs(mb.startTime) |
||||
|
|
||||
|
// Should return error
|
||||
|
if err == nil { |
||||
|
t.Errorf("Expected corruption error, got nil (pos=%d)", pos) |
||||
|
} else { |
||||
|
t.Logf("✓ locateByTs correctly detected corruption: %v", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// TestErrorPropagationChain tests the complete error propagation from readTs -> locateByTs -> ReadFromBuffer
|
||||
|
func TestErrorPropagationChain(t *testing.T) { |
||||
|
t.Run("Corruption in readTs", func(t *testing.T) { |
||||
|
// Already covered by TestReadTsCorruptedBuffer
|
||||
|
t.Log("✓ readTs error propagation tested") |
||||
|
}) |
||||
|
|
||||
|
t.Run("Corruption in locateByTs", func(t *testing.T) { |
||||
|
// Already covered by TestLocateByTsCorruption
|
||||
|
t.Log("✓ locateByTs error propagation tested") |
||||
|
}) |
||||
|
|
||||
|
t.Run("Corruption in ReadFromBuffer binary search", func(t *testing.T) { |
||||
|
// Already covered by TestReadFromBufferCorruption
|
||||
|
t.Log("✓ ReadFromBuffer error propagation tested") |
||||
|
}) |
||||
|
|
||||
|
t.Log("✓ Complete error propagation chain verified") |
||||
|
} |
||||
|
|
||||
|
// TestNoSilentCorruption verifies that corruption never returns (0, 0) silently
|
||||
|
func TestNoSilentCorruption(t *testing.T) { |
||||
|
// Create various corrupted buffers
|
||||
|
testCases := []struct { |
||||
|
name string |
||||
|
buf []byte |
||||
|
pos int |
||||
|
}{ |
||||
|
{ |
||||
|
name: "Invalid protobuf", |
||||
|
buf: []byte{10, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, |
||||
|
pos: 0, |
||||
|
}, |
||||
|
{ |
||||
|
name: "Truncated data", |
||||
|
buf: []byte{100, 0, 0, 0, 1, 2, 3}, // Size says 100 but only 3 bytes available
|
||||
|
pos: 0, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
for _, tc := range testCases { |
||||
|
t.Run(tc.name, func(t *testing.T) { |
||||
|
size, ts, err := readTs(tc.buf, tc.pos) |
||||
|
|
||||
|
// CRITICAL: Must return error, never silent (0, 0)
|
||||
|
if err == nil { |
||||
|
t.Errorf("CRITICAL: readTs returned (%d, %d, nil) for corrupted buffer - this causes silent data corruption!", size, ts) |
||||
|
} else { |
||||
|
t.Logf("✓ Correctly returned error instead of silent (0, 0): %v", err) |
||||
|
} |
||||
|
|
||||
|
// On error, size and ts should be 0
|
||||
|
if size != 0 || ts != 0 { |
||||
|
t.Errorf("On error, expected (0, 0), got (%d, %d)", size, ts) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue