package storage import ( "bytes" "os" "path/filepath" "testing" "github.com/seaweedfs/seaweedfs/weed/storage/needle" "github.com/seaweedfs/seaweedfs/weed/storage/super_block" "github.com/seaweedfs/seaweedfs/weed/storage/types" ) // Pointing -dir.idx at a directory with no .idx used to abort the whole volume // server in checkIdxFile; the index is derivable from the .dat, so it must be // rebuilt in place instead. func TestLoad_MovedIdxDirectory_RebuildsIdx(t *testing.T) { root := t.TempDir() dataDir := filepath.Join(root, "data") oldIdxDir := filepath.Join(root, "idxA") newIdxDir := filepath.Join(root, "idxB") for _, dir := range []string{dataDir, oldIdxDir, newIdxDir} { if err := os.MkdirAll(dir, 0755); err != nil { t.Fatalf("mkdir %s: %v", dir, err) } } v, err := NewVolume(dataDir, oldIdxDir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0) if err != nil { t.Fatalf("create volume: %v", err) } for id := uint64(1); id <= 3; id++ { if _, _, _, err := v.writeNeedle2(newRandomNeedle(id), true, false, false); err != nil { t.Fatalf("seed write %d: %v", id, err) } } if _, err := v.deleteNeedle2(newRandomNeedle(2)); err != nil { t.Fatalf("seed delete: %v", err) } wantCount, wantDeleted := v.nm.FileCount(), v.nm.DeletedCount() v.Close() if _, err := os.Stat(filepath.Join(oldIdxDir, "1.idx")); err != nil { t.Fatalf("seeded idx should live in the old idx dir: %v", err) } v2, err := NewVolume(dataDir, newIdxDir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0) if err != nil { t.Fatalf("reload against the new idx dir: %v", err) } defer v2.Close() if _, err := os.Stat(filepath.Join(newIdxDir, "1.idx")); err != nil { t.Fatalf("idx not rebuilt in the new idx dir: %v", err) } if got := v2.nm.FileCount(); got != wantCount { t.Errorf("file count = %d, want %d", got, wantCount) } if got := v2.nm.DeletedCount(); got != wantDeleted { t.Errorf("deleted count = %d, want %d", got, wantDeleted) } old, err := os.ReadFile(filepath.Join(oldIdxDir, "1.idx")) if err != nil { t.Fatalf("read the seeded idx: %v", err) } rebuilt, err := os.ReadFile(filepath.Join(newIdxDir, "1.idx")) if err != nil { t.Fatalf("read the rebuilt idx: %v", err) } if !bytes.Equal(old, rebuilt) { t.Errorf("rebuilt idx (%d bytes) differs from the one the server wrote (%d bytes)", len(rebuilt), len(old)) } if v2.noWriteOrDelete { t.Errorf("volume marked read-only after the rebuild") } } // A .dat padded with zeros must not be indexed as needle 0 rows: the walk stops // where the records do. func TestRebuildIdx_StopsAtZeroPaddedDatTail(t *testing.T) { dir := t.TempDir() v, err := NewVolume(dir, dir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0) if err != nil { t.Fatalf("create volume: %v", err) } if _, _, _, err := v.writeNeedle2(newRandomNeedle(1), true, false, false); err != nil { t.Fatalf("seed write: %v", err) } v.Close() base := VolumeFileName(dir, "", 1) seeded, err := os.ReadFile(base + ".idx") if err != nil { t.Fatalf("read the seeded idx: %v", err) } datSize, err := os.Stat(base + ".dat") if err != nil { t.Fatalf("stat dat: %v", err) } if err := os.Truncate(base+".dat", datSize.Size()+4096); err != nil { t.Fatalf("pad dat: %v", err) } if err := os.Remove(base + ".idx"); err != nil { t.Fatalf("drop idx: %v", err) } v2, err := NewVolume(dir, dir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0) if err != nil { t.Fatalf("reload: %v", err) } defer v2.Close() rebuilt, err := os.ReadFile(base + ".idx") if err != nil { t.Fatalf("read the rebuilt idx: %v", err) } if !bytes.Equal(seeded, rebuilt) { t.Errorf("rebuilt idx has %d bytes, want the %d the server wrote", len(rebuilt), len(seeded)) } } // A .dat whose last append was torn mid-body must not gain an index row that // points past the end of the file. func TestRebuildIdx_SkipsTruncatedDatTail(t *testing.T) { dir := t.TempDir() v, err := NewVolume(dir, dir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0) if err != nil { t.Fatalf("create volume: %v", err) } if _, _, _, err := v.writeNeedle2(newRandomNeedle(1), true, false, false); err != nil { t.Fatalf("seed write: %v", err) } base := VolumeFileName(dir, "", 1) kept, err := os.ReadFile(base + ".idx") if err != nil { t.Fatalf("read the seeded idx: %v", err) } if _, _, _, err := v.writeNeedle2(newRandomNeedle(2), true, false, false); err != nil { t.Fatalf("seed torn write: %v", err) } v.Close() // Chop the second needle's body, leaving its header intact. datSize, err := os.Stat(base + ".dat") if err != nil { t.Fatalf("stat dat: %v", err) } if err := os.Truncate(base+".dat", datSize.Size()-8); err != nil { t.Fatalf("tear dat: %v", err) } if err := os.Remove(base + ".idx"); err != nil { t.Fatalf("drop idx: %v", err) } v2, err := NewVolume(dir, dir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0) if err != nil { t.Fatalf("reload: %v", err) } defer v2.Close() rebuilt, err := os.ReadFile(base + ".idx") if err != nil { t.Fatalf("read the rebuilt idx: %v", err) } if !bytes.Equal(kept, rebuilt) { t.Errorf("rebuilt idx has %d bytes, want the %d covering only the intact needle", len(rebuilt), len(kept)) } if maxEnd := v2.nm.MaxNeedleEnd(); maxEnd > datSize.Size()-8 { t.Errorf("rebuilt idx reaches %d, past the %d-byte .dat", maxEnd, datSize.Size()-8) } } // A corrupt header carrying a negative size advances the .dat walk backwards, // which cycles forever between it and the record before it. func TestRebuildIdx_StopsAtNegativeSizeHeader(t *testing.T) { dir := t.TempDir() v, err := NewVolume(dir, dir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0) if err != nil { t.Fatalf("create volume: %v", err) } if _, _, _, err := v.writeNeedle2(newRandomNeedle(1), true, false, false); err != nil { t.Fatalf("seed write: %v", err) } base := VolumeFileName(dir, "", 1) kept, err := os.ReadFile(base + ".idx") if err != nil { t.Fatalf("read the seeded idx: %v", err) } if _, _, _, err := v.writeNeedle2(newRandomNeedle(2), true, false, false); err != nil { t.Fatalf("seed second write: %v", err) } nv, ok := v.nm.Get(2) if !ok { t.Fatalf("second needle missing from the index") } corruptAt := nv.Offset.ToActualOffset() v.Close() // Overwrite the second needle's size field with a negative int32. dat, err := os.OpenFile(base+".dat", os.O_WRONLY, 0644) if err != nil { t.Fatalf("open dat: %v", err) } if _, err := dat.WriteAt([]byte{0xff, 0xff, 0xf0, 0x00}, corruptAt+types.CookieSize+types.NeedleIdSize); err != nil { t.Fatalf("corrupt size field: %v", err) } dat.Close() if err := os.Remove(base + ".idx"); err != nil { t.Fatalf("drop idx: %v", err) } // Pre-fix the rebuild walked backwards from here and never terminated. v2, err := NewVolume(dir, dir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0) if err != nil { t.Fatalf("reload: %v", err) } defer v2.Close() rebuilt, err := os.ReadFile(base + ".idx") if err != nil { t.Fatalf("read the rebuilt idx: %v", err) } if !bytes.Equal(kept, rebuilt) { t.Errorf("rebuilt idx has %d bytes, want the %d covering only the intact needle", len(rebuilt), len(kept)) } } // A rebuild that cannot write skips just this volume; it used to call // glog.Fatalf and take the whole volume server down with it. func TestRebuildIdx_UnwritableIdxDirSkipsOnlyThisVolume(t *testing.T) { if os.Geteuid() == 0 { t.Skip("root ignores directory permissions") } root := t.TempDir() dataDir := filepath.Join(root, "data") idxDir := filepath.Join(root, "idx") for _, dir := range []string{dataDir, idxDir} { if err := os.MkdirAll(dir, 0755); err != nil { t.Fatalf("mkdir %s: %v", dir, err) } } v, err := NewVolume(dataDir, dataDir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0) if err != nil { t.Fatalf("create volume: %v", err) } if _, _, _, err := v.writeNeedle2(newRandomNeedle(1), true, false, false); err != nil { t.Fatalf("seed write: %v", err) } v.Close() if err := os.Remove(VolumeFileName(dataDir, "", 1) + ".idx"); err != nil { t.Fatalf("drop idx: %v", err) } if err := os.Chmod(idxDir, 0555); err != nil { t.Fatalf("seal idx dir: %v", err) } defer os.Chmod(idxDir, 0755) if _, err := NewVolume(dataDir, idxDir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0); err == nil { t.Errorf("expected a load error for an unwritable idx dir") } }