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.
355 lines
11 KiB
355 lines
11 KiB
package erasure_coding_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
erasure_coding "github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
)
|
|
|
|
func TestHasLiveNeedles_AllDeletedIsFalse(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
collection := "foo"
|
|
base := filepath.Join(dir, collection+"_1")
|
|
|
|
// Build an ecx file with only deleted entries.
|
|
// ecx file entries are the same format as .idx entries.
|
|
ecx := makeNeedleMapEntry(types.NeedleId(1), types.Offset{}, types.TombstoneFileSize)
|
|
if err := os.WriteFile(base+".ecx", ecx, 0644); err != nil {
|
|
t.Fatalf("write ecx: %v", err)
|
|
}
|
|
|
|
hasLive, err := erasure_coding.HasLiveNeedles(base)
|
|
if err != nil {
|
|
t.Fatalf("HasLiveNeedles: %v", err)
|
|
}
|
|
if hasLive {
|
|
t.Fatalf("expected no live entries")
|
|
}
|
|
}
|
|
|
|
func TestHasLiveNeedles_WithLiveEntryIsTrue(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
collection := "foo"
|
|
base := filepath.Join(dir, collection+"_1")
|
|
|
|
// Build an ecx file containing at least one live entry.
|
|
// ecx file entries are the same format as .idx entries.
|
|
live := makeNeedleMapEntry(types.NeedleId(1), types.Offset{}, types.Size(1))
|
|
if err := os.WriteFile(base+".ecx", live, 0644); err != nil {
|
|
t.Fatalf("write ecx: %v", err)
|
|
}
|
|
|
|
hasLive, err := erasure_coding.HasLiveNeedles(base)
|
|
if err != nil {
|
|
t.Fatalf("HasLiveNeedles: %v", err)
|
|
}
|
|
if !hasLive {
|
|
t.Fatalf("expected live entries")
|
|
}
|
|
}
|
|
|
|
func TestHasLiveNeedles_EmptyFileIsFalse(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
base := filepath.Join(dir, "foo_1")
|
|
|
|
// Create an empty ecx file.
|
|
if err := os.WriteFile(base+".ecx", []byte{}, 0644); err != nil {
|
|
t.Fatalf("write ecx: %v", err)
|
|
}
|
|
|
|
hasLive, err := erasure_coding.HasLiveNeedles(base)
|
|
if err != nil {
|
|
t.Fatalf("HasLiveNeedles: %v", err)
|
|
}
|
|
if hasLive {
|
|
t.Fatalf("expected no live entries for empty file")
|
|
}
|
|
}
|
|
|
|
func makeNeedleMapEntry(key types.NeedleId, offset types.Offset, size types.Size) []byte {
|
|
b := make([]byte, types.NeedleIdSize+types.OffsetSize+types.SizeSize)
|
|
types.NeedleIdToBytes(b[0:types.NeedleIdSize], key)
|
|
types.OffsetToBytes(b[types.NeedleIdSize:types.NeedleIdSize+types.OffsetSize], offset)
|
|
types.SizeToBytes(b[types.NeedleIdSize+types.OffsetSize:types.NeedleIdSize+types.OffsetSize+types.SizeSize], size)
|
|
return b
|
|
}
|
|
|
|
// TestWriteIdxFileFromEcIndex_PreservesDeletedNeedles verifies that WriteIdxFileFromEcIndex
|
|
// correctly marks deleted needles in the generated .idx file.
|
|
// This tests the fix for issue #7751 where deleted files in encoded volumes
|
|
// were not properly marked as deleted when decoded.
|
|
func TestWriteIdxFileFromEcIndex_PreservesDeletedNeedles(t *testing.T) {
|
|
dir := t.TempDir()
|
|
base := filepath.Join(dir, "foo_1")
|
|
|
|
// Create an .ecx file with one live needle and one deleted needle
|
|
needle1 := makeNeedleMapEntry(types.NeedleId(1), types.ToOffset(64), types.Size(100))
|
|
needle2 := makeNeedleMapEntry(types.NeedleId(2), types.ToOffset(128), types.TombstoneFileSize) // deleted
|
|
|
|
ecxData := append(needle1, needle2...)
|
|
if err := os.WriteFile(base+".ecx", ecxData, 0644); err != nil {
|
|
t.Fatalf("write ecx: %v", err)
|
|
}
|
|
|
|
// Generate .idx from .ecx
|
|
if err := erasure_coding.WriteIdxFileFromEcIndex(base); err != nil {
|
|
t.Fatalf("WriteIdxFileFromEcIndex: %v", err)
|
|
}
|
|
|
|
// Verify .idx file has the same content
|
|
idxData, err := os.ReadFile(base + ".idx")
|
|
if err != nil {
|
|
t.Fatalf("read idx: %v", err)
|
|
}
|
|
|
|
if len(idxData) != len(ecxData) {
|
|
t.Fatalf("idx file size mismatch: got %d, want %d", len(idxData), len(ecxData))
|
|
}
|
|
|
|
// Verify the second needle is still marked as deleted
|
|
entrySize := types.NeedleIdSize + types.OffsetSize + types.SizeSize
|
|
entry2 := idxData[entrySize : entrySize*2]
|
|
size2 := types.BytesToSize(entry2[types.NeedleIdSize+types.OffsetSize:])
|
|
if !size2.IsDeleted() {
|
|
t.Fatalf("expected needle 2 to be marked as deleted, got size: %d", size2)
|
|
}
|
|
}
|
|
|
|
// TestWriteIdxFileFromEcIndex_ProcessesEcjJournal verifies that WriteIdxFileFromEcIndex
|
|
// correctly processes deletions from the .ecj journal file.
|
|
func TestWriteIdxFileFromEcIndex_ProcessesEcjJournal(t *testing.T) {
|
|
dir := t.TempDir()
|
|
base := filepath.Join(dir, "foo_1")
|
|
|
|
// Create an .ecx file with two live needles
|
|
needle1 := makeNeedleMapEntry(types.NeedleId(1), types.ToOffset(64), types.Size(100))
|
|
needle2 := makeNeedleMapEntry(types.NeedleId(2), types.ToOffset(128), types.Size(200))
|
|
|
|
ecxData := append(needle1, needle2...)
|
|
if err := os.WriteFile(base+".ecx", ecxData, 0644); err != nil {
|
|
t.Fatalf("write ecx: %v", err)
|
|
}
|
|
|
|
// Create an .ecj file that records needle 2 as deleted
|
|
ecjData := make([]byte, types.NeedleIdSize)
|
|
types.NeedleIdToBytes(ecjData, types.NeedleId(2))
|
|
if err := os.WriteFile(base+".ecj", ecjData, 0644); err != nil {
|
|
t.Fatalf("write ecj: %v", err)
|
|
}
|
|
|
|
// Generate .idx from .ecx and .ecj
|
|
if err := erasure_coding.WriteIdxFileFromEcIndex(base); err != nil {
|
|
t.Fatalf("WriteIdxFileFromEcIndex: %v", err)
|
|
}
|
|
|
|
// Verify .idx file has 3 entries: 2 from .ecx + 1 deletion from .ecj
|
|
idxData, err := os.ReadFile(base + ".idx")
|
|
if err != nil {
|
|
t.Fatalf("read idx: %v", err)
|
|
}
|
|
|
|
entrySize := types.NeedleIdSize + types.OffsetSize + types.SizeSize
|
|
expectedSize := entrySize * 3 // 2 from ecx + 1 deletion append from ecj
|
|
if len(idxData) != expectedSize {
|
|
t.Fatalf("idx file size mismatch: got %d, want %d", len(idxData), expectedSize)
|
|
}
|
|
|
|
// The third entry should be the deletion record for needle 2
|
|
entry3 := idxData[entrySize*2 : entrySize*3]
|
|
key3 := types.BytesToNeedleId(entry3[0:types.NeedleIdSize])
|
|
size3 := types.BytesToSize(entry3[types.NeedleIdSize+types.OffsetSize:])
|
|
|
|
if key3 != types.NeedleId(2) {
|
|
t.Fatalf("expected needle id 2 in deletion record, got: %d", key3)
|
|
}
|
|
if !size3.IsDeleted() {
|
|
t.Fatalf("expected deletion record to have tombstone size, got: %d", size3)
|
|
}
|
|
}
|
|
|
|
// TestEcxFileDeletionVisibleAfterSync verifies that deletions made to .ecx
|
|
// via MarkNeedleDeleted are visible to other readers after Sync().
|
|
// This is a regression test for issue #7751.
|
|
func TestEcxFileDeletionVisibleAfterSync(t *testing.T) {
|
|
dir := t.TempDir()
|
|
base := filepath.Join(dir, "foo_1")
|
|
|
|
// Create an .ecx file with one live needle
|
|
needle1 := makeNeedleMapEntry(types.NeedleId(1), types.ToOffset(64), types.Size(100))
|
|
if err := os.WriteFile(base+".ecx", needle1, 0644); err != nil {
|
|
t.Fatalf("write ecx: %v", err)
|
|
}
|
|
|
|
// Open the file for writing (simulating what EcVolume does)
|
|
ecxFile, err := os.OpenFile(base+".ecx", os.O_RDWR, 0644)
|
|
if err != nil {
|
|
t.Fatalf("open ecx: %v", err)
|
|
}
|
|
|
|
// Mark needle as deleted using MarkNeedleDeleted
|
|
err = erasure_coding.MarkNeedleDeleted(ecxFile, 0)
|
|
if err != nil {
|
|
ecxFile.Close()
|
|
t.Fatalf("MarkNeedleDeleted: %v", err)
|
|
}
|
|
|
|
// Sync the file to ensure changes are visible to other readers
|
|
if err := ecxFile.Sync(); err != nil {
|
|
ecxFile.Close()
|
|
t.Fatalf("Sync: %v", err)
|
|
}
|
|
ecxFile.Close()
|
|
|
|
// Now open with a new file handle and verify deletion is visible
|
|
data, err := os.ReadFile(base + ".ecx")
|
|
if err != nil {
|
|
t.Fatalf("read ecx: %v", err)
|
|
}
|
|
|
|
size := types.BytesToSize(data[types.NeedleIdSize+types.OffsetSize:])
|
|
if !size.IsDeleted() {
|
|
t.Fatalf("expected needle to be marked as deleted after sync, got size: %d", size)
|
|
}
|
|
}
|
|
|
|
// TestEcxFileDeletionNotVisibleWithoutSync verifies that without Sync(),
|
|
// deletions may not be visible to other readers (demonstrating the bug).
|
|
// Note: This test may be flaky depending on OS caching behavior, but it
|
|
// documents the expected behavior.
|
|
func TestEcxFileDeletionWithSeparateHandles(t *testing.T) {
|
|
dir := t.TempDir()
|
|
base := filepath.Join(dir, "foo_1")
|
|
|
|
// Create an .ecx file with one live needle
|
|
needle1 := makeNeedleMapEntry(types.NeedleId(1), types.ToOffset(64), types.Size(100))
|
|
if err := os.WriteFile(base+".ecx", needle1, 0644); err != nil {
|
|
t.Fatalf("write ecx: %v", err)
|
|
}
|
|
|
|
// Open the file for writing (writer handle)
|
|
writerFile, err := os.OpenFile(base+".ecx", os.O_RDWR, 0644)
|
|
if err != nil {
|
|
t.Fatalf("open ecx for write: %v", err)
|
|
}
|
|
defer writerFile.Close()
|
|
|
|
// Open the file for reading (reader handle - simulating CopyFile behavior)
|
|
readerFile, err := os.OpenFile(base+".ecx", os.O_RDONLY, 0644)
|
|
if err != nil {
|
|
t.Fatalf("open ecx for read: %v", err)
|
|
}
|
|
defer readerFile.Close()
|
|
|
|
// Mark needle as deleted via writer handle
|
|
err = erasure_coding.MarkNeedleDeleted(writerFile, 0)
|
|
if err != nil {
|
|
t.Fatalf("MarkNeedleDeleted: %v", err)
|
|
}
|
|
|
|
// Sync the writer to flush changes
|
|
if err := writerFile.Sync(); err != nil {
|
|
t.Fatalf("Sync: %v", err)
|
|
}
|
|
|
|
// Read via reader handle - after sync, changes should be visible
|
|
data := make([]byte, types.NeedleIdSize+types.OffsetSize+types.SizeSize)
|
|
if _, err := readerFile.ReadAt(data, 0); err != nil {
|
|
t.Fatalf("ReadAt: %v", err)
|
|
}
|
|
|
|
size := types.BytesToSize(data[types.NeedleIdSize+types.OffsetSize:])
|
|
if !size.IsDeleted() {
|
|
t.Fatalf("expected deletion to be visible after Sync(), got size: %d", size)
|
|
}
|
|
}
|
|
|
|
// TestEcVolumeSyncEnsuresDeletionsVisible is an integration test for issue #7751
|
|
// that verifies the EcVolume.Sync() method ensures deleted needles are visible
|
|
// to external readers (like ec.decode's CopyFile).
|
|
func TestEcVolumeSyncEnsuresDeletionsVisible(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
collection := "test"
|
|
vid := 1
|
|
base := filepath.Join(dir, collection+"_1")
|
|
|
|
// Create initial .ecx file with live needle
|
|
needle1 := makeNeedleMapEntry(types.NeedleId(1), types.ToOffset(64), types.Size(100))
|
|
needle2 := makeNeedleMapEntry(types.NeedleId(2), types.ToOffset(128), types.Size(200))
|
|
ecxData := append(needle1, needle2...)
|
|
if err := os.WriteFile(base+".ecx", ecxData, 0644); err != nil {
|
|
t.Fatalf("write ecx: %v", err)
|
|
}
|
|
|
|
// Create empty .ecj file
|
|
if err := os.WriteFile(base+".ecj", []byte{}, 0644); err != nil {
|
|
t.Fatalf("write ecj: %v", err)
|
|
}
|
|
|
|
// Create minimal EC shard file to allow EcVolume creation
|
|
// Shards need super block header (8 bytes)
|
|
shardData := make([]byte, 8)
|
|
if err := os.WriteFile(base+".ec00", shardData, 0644); err != nil {
|
|
t.Fatalf("write ec00: %v", err)
|
|
}
|
|
|
|
// Create .vif file
|
|
if err := os.WriteFile(base+".vif", []byte{}, 0644); err != nil {
|
|
t.Fatalf("write vif: %v", err)
|
|
}
|
|
|
|
// Create EcVolume
|
|
ecVolume, err := erasure_coding.NewEcVolume(
|
|
"hdd",
|
|
dir, // data dir
|
|
dir, // idx dir
|
|
collection,
|
|
needle.VolumeId(vid),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewEcVolume: %v", err)
|
|
}
|
|
defer ecVolume.Close()
|
|
|
|
// Delete needle 2 via EcVolume (this writes to .ecx and .ecj)
|
|
if err := ecVolume.DeleteNeedleFromEcx(types.NeedleId(2)); err != nil {
|
|
t.Fatalf("DeleteNeedleFromEcx: %v", err)
|
|
}
|
|
|
|
// Before Sync: open a new reader handle to simulate CopyFile
|
|
readerBefore, err := os.OpenFile(base+".ecx", os.O_RDONLY, 0644)
|
|
if err != nil {
|
|
t.Fatalf("open ecx for read (before sync): %v", err)
|
|
}
|
|
defer readerBefore.Close()
|
|
|
|
// Now call Sync() - this is what fixes issue #7751
|
|
ecVolume.Sync()
|
|
|
|
// After Sync: open another reader handle
|
|
readerAfter, err := os.OpenFile(base+".ecx", os.O_RDONLY, 0644)
|
|
if err != nil {
|
|
t.Fatalf("open ecx for read (after sync): %v", err)
|
|
}
|
|
defer readerAfter.Close()
|
|
|
|
// Read needle 2's entry from the after-sync handle
|
|
entrySize := types.NeedleIdSize + types.OffsetSize + types.SizeSize
|
|
data := make([]byte, entrySize)
|
|
if _, err := readerAfter.ReadAt(data, int64(entrySize)); err != nil {
|
|
t.Fatalf("ReadAt after sync: %v", err)
|
|
}
|
|
|
|
// Verify needle 2 is marked as deleted
|
|
size := types.BytesToSize(data[types.NeedleIdSize+types.OffsetSize:])
|
|
if !size.IsDeleted() {
|
|
t.Fatalf("expected needle 2 to be deleted after Sync(), got size: %d", size)
|
|
}
|
|
}
|