package weed_server import ( "context" "net" "os" "path/filepath" "testing" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "github.com/seaweedfs/seaweedfs/weed/pb" "github.com/seaweedfs/seaweedfs/weed/pb/master_pb" "github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb" "github.com/seaweedfs/seaweedfs/weed/stats" "github.com/seaweedfs/seaweedfs/weed/storage" "github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding" "github.com/seaweedfs/seaweedfs/weed/storage/needle" "github.com/seaweedfs/seaweedfs/weed/storage/types" "github.com/seaweedfs/seaweedfs/weed/storage/volume_info" "github.com/seaweedfs/seaweedfs/weed/util" ) // fakeMaster serves only LookupEcVolume, returning the configured holders for // every shard of one volume. Used to drive the receiver's peer discovery. type fakeMaster struct { master_pb.UnimplementedSeaweedServer volumeId uint32 locations []*master_pb.Location } func (m *fakeMaster) LookupEcVolume(_ context.Context, req *master_pb.LookupEcVolumeRequest) (*master_pb.LookupEcVolumeResponse, error) { resp := &master_pb.LookupEcVolumeResponse{VolumeId: req.VolumeId} if req.VolumeId != m.volumeId { return resp, nil } // Recovery only needs one peer holding the index; report a couple of shards // pointing at the holder, independent of the EC ratio. for shardId := 0; shardId < 2; shardId++ { resp.ShardIdLocations = append(resp.ShardIdLocations, &master_pb.LookupEcVolumeResponse_EcShardIdLocation{ ShardId: uint32(shardId), Locations: m.locations, }) } return resp, nil } // serveGrpc registers register(s) on a fresh localhost listener and returns its // grpc port, stopping the server on test cleanup. func serveGrpc(t *testing.T, register func(*grpc.Server)) int { t.Helper() lis, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } srv := grpc.NewServer() register(srv) go srv.Serve(lis) t.Cleanup(srv.Stop) return lis.Addr().(*net.TCPAddr).Port } func newRecoverTestStore(t *testing.T, dir string) *storage.Store { t.Helper() if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatalf("mkdir %s: %v", dir, err) } store := storage.NewStore(nil, "localhost", 8080, 18080, "http://localhost:8080", "store-id", []string{dir}, []int32{100}, []util.MinFreeSpace{{}}, "", storage.NeedleMapInMemory, []types.DiskType{types.HardDriveType}, nil, 3, stats.DefaultDiskIOProbeConfig()) done := make(chan struct{}) go func() { for { select { case <-store.NewEcShardsChan: case <-store.NewVolumesChan: case <-store.DeletedVolumesChan: case <-store.DeletedEcShardsChan: case <-store.StateUpdateChan: case <-done: return } } }() t.Cleanup(func() { store.Close() close(done) }) return store } // TestFetchEcIndexFromPeers_CopiesIndexOverGrpc stands up a source volume server // that holds the .ecx/.ecj/.vif for a volume and verifies the receiver pulls // them over a real CopyFile gRPC stream into its own disk, the way #10104 // recovery does when the index lives only on a peer. func TestFetchEcIndexFromPeers_CopiesIndexOverGrpc(t *testing.T) { const collection = "video-recordings" vid := needle.VolumeId(6190) // Source server: has the index files on disk. srcDir := filepath.Join(t.TempDir(), "src") srcStore := newRecoverTestStore(t, srcDir) srcBase := erasure_coding.EcShardFileName(collection, srcDir, int(vid)) ecxBytes := make([]byte, types.NeedleMapEntrySize*3) for i := range ecxBytes { ecxBytes[i] = byte(i) } if err := os.WriteFile(srcBase+".ecx", ecxBytes, 0o644); err != nil { t.Fatalf("write source .ecx: %v", err) } if err := os.WriteFile(srcBase+".ecj", []byte("journal"), 0o644); err != nil { t.Fatalf("write source .ecj: %v", err) } // A real .vif: the receiver MOUNTS the volume from the copied files, and a // mount refuses a .vif it cannot parse rather than guessing the layout. if err := volume_info.SaveVolumeInfo(srcBase+".vif", &volume_server_pb.VolumeInfo{ Version: uint32(needle.Version3), EcShardConfig: &volume_server_pb.EcShardConfig{DataShards: 10, ParityShards: 4}, }); err != nil { t.Fatalf("write source .vif: %v", err) } // Serve the source over a real TCP gRPC listener. lis, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } grpcServer := grpc.NewServer() volume_server_pb.RegisterVolumeServerServer(grpcServer, &VolumeServer{store: srcStore}) go grpcServer.Serve(lis) t.Cleanup(grpcServer.Stop) grpcPort := lis.Addr().(*net.TCPAddr).Port peer := pb.NewServerAddress("127.0.0.1", grpcPort-10000, grpcPort) // Receiver server: empty disk, ready to receive the index. dstDir := filepath.Join(t.TempDir(), "dst") dstStore := newRecoverTestStore(t, dstDir) receiver := &VolumeServer{ store: dstStore, grpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()), } m := storage.EcVolumeMissingIndex{ Collection: collection, VolumeId: vid, IdxDir: dstDir, DataDir: dstDir, } if !receiver.fetchEcIndexFromPeers([]pb.ServerAddress{peer}, m) { t.Fatalf("fetchEcIndexFromPeers returned false; expected a successful copy") } dstBase := erasure_coding.EcShardFileName(collection, dstDir, int(vid)) got, err := os.ReadFile(dstBase + ".ecx") if err != nil { t.Fatalf("read copied .ecx: %v", err) } if len(got) != len(ecxBytes) { t.Errorf("copied .ecx size = %d, want %d", len(got), len(ecxBytes)) } if _, err := os.Stat(dstBase + ".ecj"); err != nil { t.Errorf("copied .ecj missing: %v", err) } if _, err := os.Stat(dstBase + ".vif"); err != nil { t.Errorf("copied .vif missing: %v", err) } } // TestFetchEcIndexFromPeers_SkipsPeerWithoutIndex verifies the receiver moves on // to the next peer when the first has no .ecx, and reports failure when no peer // can serve a usable index. func TestFetchEcIndexFromPeers_SkipsPeerWithoutIndex(t *testing.T) { const collection = "video-recordings" vid := needle.VolumeId(6191) // Source server with NO index files for this volume. srcDir := filepath.Join(t.TempDir(), "src") srcStore := newRecoverTestStore(t, srcDir) lis, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } grpcServer := grpc.NewServer() volume_server_pb.RegisterVolumeServerServer(grpcServer, &VolumeServer{store: srcStore}) go grpcServer.Serve(lis) t.Cleanup(grpcServer.Stop) grpcPort := lis.Addr().(*net.TCPAddr).Port peer := pb.NewServerAddress("127.0.0.1", grpcPort-10000, grpcPort) dstDir := filepath.Join(t.TempDir(), "dst") dstStore := newRecoverTestStore(t, dstDir) receiver := &VolumeServer{ store: dstStore, grpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()), } m := storage.EcVolumeMissingIndex{Collection: collection, VolumeId: vid, IdxDir: dstDir, DataDir: dstDir} if receiver.fetchEcIndexFromPeers([]pb.ServerAddress{peer}, m) { t.Fatalf("fetchEcIndexFromPeers should fail when no peer has the index") } // No stub .ecx must be left behind. dstBase := erasure_coding.EcShardFileName(collection, dstDir, int(vid)) if _, err := os.Stat(dstBase + ".ecx"); !os.IsNotExist(err) { t.Errorf("a .ecx stub was left behind; stat err = %v", err) } } // TestVolumeEcShardsMount_RecoverMissingIndex drives the full on-demand path: // VolumeEcShardsMount with recover_missing_index (volume_id 0 = recover every // orphan on this server, as ec.rebuild broadcasts it) looks up holders via a // (fake) master, fetches the missing .ecx/.ecj/.vif from the holding peer over // gRPC, and mounts the previously-orphaned on-disk shards (issue #10104). func TestVolumeEcShardsMount_RecoverMissingIndex(t *testing.T) { const collection = "video-recordings" vid := needle.VolumeId(6190) // Holder peer: serves the index files for the volume. srcDir := filepath.Join(t.TempDir(), "src") srcStore := newRecoverTestStore(t, srcDir) srcBase := erasure_coding.EcShardFileName(collection, srcDir, int(vid)) if err := os.WriteFile(srcBase+".ecx", make([]byte, types.NeedleMapEntrySize*4), 0o644); err != nil { t.Fatalf("write source .ecx: %v", err) } if err := os.WriteFile(srcBase+".ecj", nil, 0o644); err != nil { t.Fatalf("write source .ecj: %v", err) } // A real .vif: the receiver MOUNTS the volume from the copied files, and a // mount refuses a .vif it cannot parse rather than guessing the layout. if err := volume_info.SaveVolumeInfo(srcBase+".vif", &volume_server_pb.VolumeInfo{ Version: uint32(needle.Version3), EcShardConfig: &volume_server_pb.EcShardConfig{DataShards: 10, ParityShards: 4}, }); err != nil { t.Fatalf("write source .vif: %v", err) } srcGrpcPort := serveGrpc(t, func(s *grpc.Server) { volume_server_pb.RegisterVolumeServerServer(s, &VolumeServer{store: srcStore}) }) // Fake master points every shard at the holder peer. masterGrpcPort := serveGrpc(t, func(s *grpc.Server) { master_pb.RegisterSeaweedServer(s, &fakeMaster{ volumeId: uint32(vid), locations: []*master_pb.Location{{Url: "127.0.0.1:1", GrpcPort: uint32(srcGrpcPort)}}, }) }) // Receiver: holds orphan shard files on disk, no .ecx anywhere. dstDir := filepath.Join(t.TempDir(), "dst") dstStore := newRecoverTestStore(t, dstDir) const shardSize = 1 << 20 for _, sid := range []erasure_coding.ShardId{0, 5} { base := erasure_coding.EcShardFileName(collection, dstDir, int(vid)) f, err := os.Create(base + erasure_coding.ToExt(int(sid))) if err != nil { t.Fatalf("create shard %d: %v", sid, err) } if err := f.Truncate(shardSize); err != nil { f.Close() t.Fatalf("truncate shard %d: %v", sid, err) } f.Close() } receiver := &VolumeServer{ store: dstStore, grpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()), } receiver.setCurrentMaster(pb.NewServerAddress("127.0.0.1", masterGrpcPort-10000, masterGrpcPort)) if _, found := dstStore.FindEcVolume(vid); found { t.Fatalf("EC volume %d unexpectedly mounted before recovery", vid) } // volume_id 0: the receiver discovers the orphan (6190) on disk itself, even // though the request names no volume and the master never registered it here. if _, err := receiver.VolumeEcShardsMount(context.Background(), &volume_server_pb.VolumeEcShardsMountRequest{ RecoverMissingIndex: true, }); err != nil { t.Fatalf("VolumeEcShardsMount with recover_missing_index: %v", err) } ev, found := dstStore.FindEcVolume(vid) if !found { t.Fatalf("EC volume %d not mounted after recovery", vid) } for _, sid := range []erasure_coding.ShardId{0, 5} { if _, ok := ev.FindEcVolumeShard(sid); !ok { t.Errorf("shard %d.%d not registered after recovery", vid, sid) } } }