package storage import ( "bytes" "math/rand" "net" "os" "sync" "sync/atomic" "testing" "time" "golang.org/x/sync/semaphore" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" "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" ) // countingEcShardPeer stands in for a peer volume server that is having a // transient problem: every VolumeEcShardRead is counted and then held until the // test releases it, so a stalled fan-out is observable while it is stalled. type countingEcShardPeer struct { volume_server_pb.UnimplementedVolumeServerServer requests atomic.Int64 inFlight atomic.Int64 peak atomic.Int64 release chan struct{} releaseOnce sync.Once } func (p *countingEcShardPeer) VolumeEcShardRead(req *volume_server_pb.VolumeEcShardReadRequest, stream volume_server_pb.VolumeServer_VolumeEcShardReadServer) error { p.requests.Add(1) inFlight := p.inFlight.Add(1) for peak := p.peak.Load(); inFlight > peak; peak = p.peak.Load() { if p.peak.CompareAndSwap(peak, inFlight) { break } } defer p.inFlight.Add(-1) <-p.release return status.Error(codes.Unavailable, "transient failure") } func (p *countingEcShardPeer) releaseAll() { p.releaseOnce.Do(func() { close(p.release) }) } func startCountingEcShardPeer(t *testing.T) (*countingEcShardPeer, pb.ServerAddress) { t.Helper() peer := &countingEcShardPeer{release: make(chan struct{})} lis, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } srv := grpc.NewServer() volume_server_pb.RegisterVolumeServerServer(srv, peer) go srv.Serve(lis) t.Cleanup(func() { peer.releaseAll() srv.Stop() }) return peer, pb.NewServerAddressWithGrpcPort("127.0.0.1:1", lis.Addr().(*net.TCPAddr).Port) } // writeEcVolumeFiles writes one needle into a volume in dir and EC-encodes it in // place, leaving the .ec?? / .ecx / .vif set behind. func writeEcVolumeFiles(t *testing.T, dir string, vid needle.VolumeId) (baseFileName string, n *needle.Needle) { t.Helper() 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) } n = new(needle.Needle) n.Id = types.Uint64ToNeedleId(42) n.Data = make([]byte, 3*erasure_coding.ErasureCodingSmallBlockSize+1234) rand.New(rand.NewSource(42)).Read(n.Data) n.Checksum = needle.NewCRC(n.Data) 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.BackgroundECContext() 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: erasure_coding.DataShardsCount, ParityShards: erasure_coding.ParityShardsCount, // The encode writes the uniform layout; a .vif that omitted its // block size would mount these shards as legacy and read every // interval at the wrong offset. 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) } } return baseFileName, n } // mountLocalEcVolume writes an EC volume into the store's own disk and mounts // every shard, so a recovery can be served entirely from local shards. func mountLocalEcVolume(t *testing.T, store *Store, vid needle.VolumeId) (*erasure_coding.EcVolume, *needle.Needle) { t.Helper() _, n := writeEcVolumeFiles(t, store.Locations[0].Directory, vid) 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") } return ecVolume, n } // seedShardLocations points every shard of the volume at one address and marks // the cache fresh, so a read never reaches for the master this test does not run. func seedShardLocations(ecVolume *erasure_coding.EcVolume, addr pb.ServerAddress) { ecVolume.ShardLocationsLock.Lock() for shardId := 0; shardId < erasure_coding.TotalShardsCount; shardId++ { ecVolume.ShardLocations[erasure_coding.ShardId(shardId)] = []pb.ServerAddress{addr} } ecVolume.ShardLocationsRefreshTime = time.Now() ecVolume.ShardLocationsLock.Unlock() } // Recovery reconstructs a shard from the shards this server already holds, so a // store holding the whole volume never asks a peer for one. func TestRecoverOneRemoteEcShardIntervalUsesLocalShards(t *testing.T) { store := newTestStore(t, 1) peer, addr := startCountingEcShardPeer(t) ecVolume, n := mountLocalEcVolume(t, store, 7) seedShardLocations(ecVolume, addr) _, _, intervals, err := ecVolume.LocateEcShardNeedle(n.Id, ecVolume.Version) if err != nil { t.Fatalf("locate needle: %v", err) } shardIdToRecover, actualOffset := ecVolume.IntervalToShardIdAndOffset(intervals[0]) got := make([]byte, intervals[0].Size) nRead, _, err := store.recoverOneRemoteEcShardInterval(n.Id, ecVolume, shardIdToRecover, got, actualOffset) if err != nil { t.Fatalf("recover ec shard interval: %v", err) } if nRead != len(got) { t.Fatalf("recovered %d bytes, want %d", nRead, len(got)) } if requests := peer.requests.Load(); requests != 0 { t.Errorf("recovery asked peers for %d shard intervals, want 0 with every shard local", requests) } want := make([]byte, len(got)) if err := store.readLocalEcShardInterval(ecVolume, shardIdToRecover, want, actualOffset); err != nil { t.Fatalf("read local ec shard %d: %v", shardIdToRecover, err) } if !bytes.Equal(got, want) { t.Errorf("recovered shard %d bytes differ from the shard on disk", shardIdToRecover) } } // shardServingPeer answers VolumeEcShardRead out of an EC volume's shard files // and counts how many shard intervals the caller asked for. type shardServingPeer struct { volume_server_pb.UnimplementedVolumeServerServer baseFileName string requests atomic.Int64 } func (p *shardServingPeer) VolumeEcShardRead(req *volume_server_pb.VolumeEcShardReadRequest, stream volume_server_pb.VolumeServer_VolumeEcShardReadServer) error { p.requests.Add(1) f, err := os.Open(p.baseFileName + erasure_coding.ToExt(int(req.ShardId))) if err != nil { return err } defer f.Close() data := make([]byte, req.Size) if _, err := f.ReadAt(data, req.Offset); err != nil { return err } return stream.Send(&volume_server_pb.VolumeEcShardReadResponse{Data: data, EncodeTsNs: req.EncodeTsNs}) } // Reed-Solomon needs DataShards shards, so the fan-out stops there instead of // pulling every surviving shard off peers that may already be struggling. func TestRecoverOneRemoteEcShardIntervalFetchesOnlyWhatItNeeds(t *testing.T) { baseFileName, n := writeEcVolumeFiles(t, t.TempDir(), 7) peer := &shardServingPeer{baseFileName: baseFileName} lis, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } srv := grpc.NewServer() volume_server_pb.RegisterVolumeServerServer(srv, peer) go srv.Serve(lis) t.Cleanup(srv.Stop) addr := pb.NewServerAddressWithGrpcPort("127.0.0.1:1", lis.Addr().(*net.TCPAddr).Port) store := &Store{grpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials())} ecVolume := &erasure_coding.EcVolume{ VolumeId: 7, ShardLocations: make(map[erasure_coding.ShardId][]pb.ServerAddress), } seedShardLocations(ecVolume, addr) const shardIdToRecover = erasure_coding.ShardId(0) const offset = 0 got := make([]byte, 4096) if _, _, err := store.recoverOneRemoteEcShardInterval(n.Id, ecVolume, shardIdToRecover, got, offset); err != nil { t.Fatalf("recover ec shard interval: %v", err) } if requests := peer.requests.Load(); requests != int64(erasure_coding.DataShardsCount) { t.Errorf("recovery asked peers for %d shard intervals, want %d", requests, erasure_coding.DataShardsCount) } want := make([]byte, len(got)) f, err := os.Open(baseFileName + erasure_coding.ToExt(int(shardIdToRecover))) if err != nil { t.Fatalf("open shard %d: %v", shardIdToRecover, err) } defer f.Close() if _, err := f.ReadAt(want, offset); err != nil { t.Fatalf("read shard %d: %v", shardIdToRecover, err) } if !bytes.Equal(got, want) { t.Errorf("recovered shard %d bytes differ from the shard on disk", shardIdToRecover) } } // setEcRecoverBudget narrows the process-wide recovery budget for one test. func setEcRecoverBudget(t *testing.T, budget int64) { t.Helper() oldBudget, oldSem := ecRecoverBudget, ecRecoverSem ecRecoverBudget, ecRecoverSem = budget, semaphore.NewWeighted(budget) t.Cleanup(func() { ecRecoverBudget, ecRecoverSem = oldBudget, oldSem }) } // A burst of reads against a peer that has stopped answering queues on the // recovery budget instead of piling interval-sized buffers onto the heap. func TestRecoverOneRemoteEcShardIntervalBoundsInFlightBytes(t *testing.T) { const intervalSize = 1 << 20 const readers = 8 const budget = 24 << 20 setEcRecoverBudget(t, budget) peer, addr := startCountingEcShardPeer(t) store := &Store{grpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials())} ecVolume := &erasure_coding.EcVolume{ VolumeId: 7, ShardLocations: make(map[erasure_coding.ShardId][]pb.ServerAddress), } seedShardLocations(ecVolume, addr) var wg sync.WaitGroup for i := 0; i < readers; i++ { wg.Add(1) go func() { defer wg.Done() store.recoverOneRemoteEcShardInterval(types.Uint64ToNeedleId(42), ecVolume, 0, make([]byte, intervalSize), 0) }() } // let the fan-out reach the peer and settle there for deadline, settled, last := time.Now().Add(10*time.Second), 0, int64(-1); time.Now().Before(deadline) && settled < 5; { time.Sleep(50 * time.Millisecond) inFlight := peer.inFlight.Load() if inFlight > 0 && inFlight == last { settled++ } else { settled = 0 } last = inFlight } if peak, allowed := peer.peak.Load(), int64(budget/intervalSize); peak > allowed { t.Errorf("%d shard reads of %dMB in flight at once, more than the %dMB budget allows", peak, intervalSize>>20, budget>>20) } peer.releaseAll() wg.Wait() } // deletingEcShardPeer answers every shard read with the needle's deletion. type deletingEcShardPeer struct { volume_server_pb.UnimplementedVolumeServerServer } func (p *deletingEcShardPeer) VolumeEcShardRead(req *volume_server_pb.VolumeEcShardReadRequest, stream volume_server_pb.VolumeServer_VolumeEcShardReadServer) error { return stream.Send(&volume_server_pb.VolumeEcShardReadResponse{IsDeleted: true, EncodeTsNs: req.EncodeTsNs}) } // A needle the peers report deleted reads as deleted -- a 404 -- even when too // few shards came back to reconstruct it. func TestReadEcShardNeedleAnswersDeletedWhenRecoveryFallsShort(t *testing.T) { store := newTestStore(t, 1) store.grpcDialOption = grpc.WithTransportCredentials(insecure.NewCredentials()) const vid = needle.VolumeId(7) _, n := writeEcVolumeFiles(t, store.Locations[0].Directory, vid) // Too few local shards to reconstruct, and none for the shard the needle's // first interval lands on, so that interval has to recover from peers. for shardId := 1; shardId <= 5; 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") } lis, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } srv := grpc.NewServer() volume_server_pb.RegisterVolumeServerServer(srv, &deletingEcShardPeer{}) go srv.Serve(lis) t.Cleanup(srv.Stop) seedShardLocations(ecVolume, pb.NewServerAddressWithGrpcPort("127.0.0.1:1", lis.Addr().(*net.TCPAddr).Port)) ecVolume.ShardLocationsLock.Lock() delete(ecVolume.ShardLocations, erasure_coding.ShardId(0)) ecVolume.ShardLocationsLock.Unlock() got := new(needle.Needle) got.Id = n.Id if _, err := store.ReadEcShardNeedle(vid, got, nil); err != ErrorDeleted { t.Errorf("read a deleted needle returned %v, want %v", err, ErrorDeleted) } }