package storage import ( "bytes" "math/rand" "os" "testing" "time" "github.com/seaweedfs/seaweedfs/weed/pb" "github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb" "github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding" "github.com/seaweedfs/seaweedfs/weed/storage/needle" "github.com/seaweedfs/seaweedfs/weed/storage/super_block" "github.com/seaweedfs/seaweedfs/weed/storage/types" "github.com/seaweedfs/seaweedfs/weed/storage/volume_info" ) // encodeAndMountEcVolume writes needles into a fresh volume, EC-encodes it // (uniform layout), stamps the .vif the way VolumeEcShardsGenerate does, and // mounts every shard on the store. func encodeAndMountEcVolume(t *testing.T, store *Store, vid needle.VolumeId, needles []*needle.Needle) *erasure_coding.EcVolume { t.Helper() dir := store.Locations[0].Directory v, err := NewVolume(dir, dir, "", vid, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0) if err != nil { t.Fatalf("new volume: %v", err) } for _, n := range needles { if _, _, _, err := v.writeNeedle2(n, true, false, false); err != nil { t.Fatalf("write needle: %v", err) } } baseFileName := v.DataFileName() v.Close() datSize, err := os.Stat(baseFileName + ".dat") if err != nil { t.Fatalf("stat .dat: %v", err) } ecCtx := erasure_coding.NewDefaultECContext("", vid) if _, err := erasure_coding.WriteEcFiles(baseFileName, ecCtx); err != nil { t.Fatalf("write ec files: %v", err) } if err := erasure_coding.WriteSortedFileFromIdx(baseFileName, ".ecx"); err != nil { t.Fatalf("write .ecx: %v", err) } if err := os.WriteFile(baseFileName+".ecj", nil, 0o644); err != nil { t.Fatalf("write .ecj: %v", err) } if err := volume_info.SaveVolumeInfo(baseFileName+".vif", &volume_server_pb.VolumeInfo{ Version: uint32(needle.GetCurrentVersion()), DatFileSize: datSize.Size(), EcShardConfig: &volume_server_pb.EcShardConfig{ DataShards: uint32(ecCtx.DataShards), ParityShards: uint32(ecCtx.ParityShards), BlockSize: ecCtx.BlockSize, }, }); err != nil { t.Fatalf("save .vif: %v", err) } for _, ext := range []string{".dat", ".idx"} { if err := os.Remove(baseFileName + ext); err != nil { t.Fatalf("remove %s: %v", ext, err) } } for shardId := 0; shardId < erasure_coding.TotalShardsCount; shardId++ { if err := store.MountEcShards("", vid, erasure_coding.ShardId(shardId), ""); err != nil { t.Fatalf("mount shard %d: %v", shardId, err) } } ecVolume, found := store.Locations[0].FindEcVolume(vid) if !found { t.Fatal("ec volume not mounted") } if ecVolume.ECContext.BlockSize != ecCtx.BlockSize { t.Fatalf("mounted BlockSize = %d, want %d", ecVolume.ECContext.BlockSize, ecCtx.BlockSize) } // Every shard is local, so seed the location cache to keep the read off the // master this test does not have. ecVolume.ShardLocationsLock.Lock() for shardId := 0; shardId < erasure_coding.TotalShardsCount; shardId++ { ecVolume.ShardLocations[erasure_coding.ShardId(shardId)] = []pb.ServerAddress{"localhost:8080"} } ecVolume.ShardLocationsRefreshTime = time.Now() ecVolume.ShardLocationsLock.Unlock() return ecVolume } func randomNeedleOfSize(id uint64, size int) *needle.Needle { n := new(needle.Needle) n.Id = types.Uint64ToNeedleId(id) n.Data = make([]byte, size) rand.New(rand.NewSource(int64(id))).Read(n.Data) n.Checksum = needle.NewCRC(n.Data) return n } // A needle larger than one EC block is split over consecutive blocks, which // live on different shards. The intervals are read concurrently, so check they // still come back in order. func TestReadEcShardNeedleSpanningBlocks(t *testing.T) { store := newTestStore(t, 1) const vid = needle.VolumeId(7) n := randomNeedleOfSize(42, 3*erasure_coding.ErasureCodingSmallBlockSize+1234) ecVolume := encodeAndMountEcVolume(t, store, vid, []*needle.Needle{n}) _, _, intervals, err := ecVolume.LocateEcShardNeedle(n.Id, ecVolume.Version) if err != nil { t.Fatalf("locate needle: %v", err) } if len(intervals) < 2 { t.Fatalf("needle covers %d interval(s), want it split over several", len(intervals)) } got := new(needle.Needle) got.Id = n.Id if _, err := store.ReadEcShardNeedle(vid, got, nil); err != nil { t.Fatalf("read ec needle: %v", err) } if !bytes.Equal(got.Data, n.Data) { t.Fatalf("read back %d bytes, want the %d written", len(got.Data), len(n.Data)) } } // On a volume big enough for multi-MB blocks, the uniform layout keeps a // needle smaller than the block in one interval on one shard, and reads every // needle back intact. func TestReadEcShardNeedleUniformLayout(t *testing.T) { store := newTestStore(t, 1) const vid = needle.VolumeId(8) // ~26MB of needles: block size becomes 3MB, so the layouts diverge and a // 1MB needle would stripe across shards under the legacy layout. var needles []*needle.Needle for i := uint64(1); i <= 5; i++ { needles = append(needles, randomNeedleOfSize(i, 4*1024*1024)) } for i := uint64(6); i <= 11; i++ { needles = append(needles, randomNeedleOfSize(i, 1024*1024)) } ecVolume := encodeAndMountEcVolume(t, store, vid, needles) if ecVolume.ECContext.BlockSize <= erasure_coding.ErasureCodingSmallBlockSize { t.Fatalf("block size %d does not diverge from the legacy layout", ecVolume.ECContext.BlockSize) } // With 3MB blocks a 1MB needle sits inside one block (bar a boundary // straddle), where the legacy layout would stripe it at 1MB granularity. singleInterval := false for _, n := range needles[5:] { _, _, intervals, err := ecVolume.LocateEcShardNeedle(n.Id, ecVolume.Version) if err != nil { t.Fatalf("locate needle %v: %v", n.Id, err) } if len(intervals) == 1 { singleInterval = true } } if !singleInterval { t.Fatal("no 1MB needle mapped to a single interval; uniform layout not in effect") } for _, n := range needles { got := new(needle.Needle) got.Id = n.Id if _, err := store.ReadEcShardNeedle(vid, got, nil); err != nil { t.Fatalf("read needle %v: %v", n.Id, err) } if !bytes.Equal(got.Data, n.Data) { t.Fatalf("needle %v: read back %d bytes that do not match the %d written", n.Id, len(got.Data), len(n.Data)) } } }