feat(fix): rebuild lost EC index (.ecx) and .vif from local shards (#9596)

weed fix -ecx reconstructs the .dat from the local data shards, scans the
needles, and writes a fresh ascending-sorted .ecx containing only live
entries — the same on-disk index WriteSortedFileFromIdx emits at encode
time. When the .vif is also missing it is regenerated from the inferred
EC ratio (flags > .vif > shard-count inference / 10+4) and the .dat size
recovered from the scan.

When some data shards are missing but at least dataShards shards survive,
the missing shards are first reconstructed from the survivors via
Reed-Solomon, so a partial shard set is repaired too.

Also makes erasure_coding.WriteDatFile de-stripe using len(shardFileNames)
instead of the DataShardsCount constant, so the caller's actual data-shard
count is honored (behavior-preserving for the default 10, and fixing the
existing caller that already passes ECContext.DataShards).

This recovers an EC volume whose sealed index was lost from every node
while enough shards survive, a state neither ec.rebuild nor ec.decode can
repair because both require an existing .ecx.

Flags: -ecx, -ecDataShards, -ecParityShards. Run with the volume server
stopped.
This commit is contained in:
Chris Lu
2026-05-21 00:41:27 -07:00
committed by GitHub
parent 9b9fdb5b76
commit 303c2be38d
3 changed files with 579 additions and 7 deletions
+296
View File
@@ -2,6 +2,7 @@ package command
import (
"fmt"
"io"
"io/fs"
"os"
"path"
@@ -9,12 +10,15 @@ import (
"strings"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
"github.com/seaweedfs/seaweedfs/weed/storage"
"github.com/seaweedfs/seaweedfs/weed/storage/backend"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/needle_map"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
"github.com/seaweedfs/seaweedfs/weed/storage/volume_info"
"github.com/seaweedfs/seaweedfs/weed/util"
)
@@ -27,6 +31,7 @@ var cmdFix = &Command{
Short: "run weed tool fix on files or whole folders to recreate index file(s) if corrupted",
Long: `Fix runs the SeaweedFS fix command on local dat files ( or remote files) or whole folders to re-create the index .idx file. If fixing remote files, you need to synchronize master.toml to the same directory on the current node as on the master node.
You Need to stop the volume server when running this command.
Use -ecx to rebuild a lost EC index (.ecx) — and the .vif when missing — from the local .ec## shards.
`,
}
@@ -36,6 +41,9 @@ var (
fixIncludeDeleted = cmdFix.Flag.Bool("includeDeleted", true, "include deleted entries in the index file")
fixIgnoreError = cmdFix.Flag.Bool("ignoreError", false, "an optional, if true will be processed despite errors")
fixRemoteFile = cmdFix.Flag.Bool("remoteFile", false, "an optional, if true will not try to load the local .dat file, but only the remote file")
fixGenerateEcx = cmdFix.Flag.Bool("ecx", false, "regenerate a lost EC index (.ecx) — and the .vif when missing — from the local .ec## shards (missing shards are reconstructed from parity when enough survive). Run with the volume server stopped.")
fixEcDataShards = cmdFix.Flag.Int("ecDataShards", 0, "EC data shard count for -ecx (0 = read from .vif, otherwise default 10)")
fixEcParityShards = cmdFix.Flag.Int("ecParityShards", 0, "EC parity shard count for -ecx (0 = read from .vif, infer from shard count, otherwise default 4)")
)
type VolumeFileScanner4Fix struct {
@@ -130,6 +138,45 @@ func runFix(cmd *Command, args []string) bool {
}
doFixOneVolume(basePath, baseFileName, collection, volumeId, *fixIncludeDeleted)
}
if *fixGenerateEcx {
if !fixEcxFromShardsInDir(basePath, files) {
return false
}
}
}
return true
}
// fixEcxFromShardsInDir finds EC volumes in files (identified by their .ec00
// data shard) and regenerates the .ecx (and .vif when missing) for each,
// honoring the -collection and -volumeId filters.
func fixEcxFromShardsInDir(basePath string, files []fs.DirEntry) bool {
const shard0Ext = ".ec00"
for _, file := range files {
if !strings.HasSuffix(file.Name(), shard0Ext) {
continue
}
if *fixVolumeCollection != "" {
if !strings.HasPrefix(file.Name(), *fixVolumeCollection+"_") {
continue
}
}
baseFileName := file.Name()[:len(file.Name())-len(shard0Ext)]
collection, volumeIdStr := "", baseFileName
if sepIndex := strings.LastIndex(baseFileName, "_"); sepIndex > 0 {
collection = baseFileName[:sepIndex]
volumeIdStr = baseFileName[sepIndex+1:]
}
volumeId, parseErr := strconv.ParseInt(volumeIdStr, 10, 64)
if parseErr != nil {
fmt.Printf("Failed to parse volume id from %s: %v\n", baseFileName, parseErr)
return false
}
if *fixVolumeId != 0 && *fixVolumeId != volumeId {
continue
}
doFixEcxFromShards(basePath, baseFileName, collection, volumeId)
}
return true
}
@@ -202,3 +249,252 @@ func doFixOneVolume(basepath string, baseFileName string, collection string, vol
}
}
}
// doFixEcxFromShards rebuilds the sealed EC index (.ecx) for one EC volume
// directly from its local shards when both the .ecx and the original .dat are
// gone but the shards survive. When some data shards are missing but at least
// dataShards shards survive in total, the missing shards are first reconstructed
// from the survivors via Reed-Solomon. It then de-stripes the data shards into a
// temporary .dat, scans the needles, and writes a fresh ascending-sorted .ecx
// that matches what WriteSortedFileFromIdx emits at encode time (live entries
// only). When the .vif is also missing it is regenerated from the inferred EC
// ratio and the .dat size discovered during the scan.
func doFixEcxFromShards(basePath, baseFileName, collection string, volumeId int64) {
base := path.Join(basePath, baseFileName)
fail := func(err error) {
if *fixIgnoreError {
glog.Error(err)
} else {
glog.Fatal(err)
}
}
ecxName := base + ".ecx"
if info, err := os.Stat(ecxName); err == nil && info.Size() > 0 {
glog.Infof("volume %d: %s already exists (%d bytes), skipping; remove it first to force regeneration", volumeId, ecxName, info.Size())
return
}
// Discover which shards are present and their common size. Reed-Solomon
// requires every shard to be the same size.
present := make([]bool, erasure_coding.MaxShardCount)
presentCount := 0
maxPresentIdx := -1
var shardSize int64
for i := 0; i < erasure_coding.MaxShardCount; i++ {
info, statErr := os.Stat(base + erasure_coding.ToExt(i))
if statErr != nil || info.Size() == 0 {
continue
}
if shardSize == 0 {
shardSize = info.Size()
} else if info.Size() != shardSize {
fail(fmt.Errorf("volume %d: shard %s size %d does not match %d", volumeId, base+erasure_coding.ToExt(i), info.Size(), shardSize))
return
}
present[i] = true
presentCount++
maxPresentIdx = i
}
if presentCount == 0 {
fail(fmt.Errorf("volume %d: no EC shards found under %s", volumeId, base))
return
}
// Resolve the EC ratio and the original .dat size.
// Priority: explicit flags > existing .vif > defaults (10+4).
vifName := base + ".vif"
vifExists := util.FileExists(vifName)
dataShards := erasure_coding.DataShardsCount
parityShards := erasure_coding.ParityShardsCount
var datFileSize int64
if vifExists {
// MaybeLoadVolumeInfo returns a non-nil error when the .vif exists but
// cannot be read or unmarshalled; fail loudly rather than silently
// falling back to defaults (which would be wrong for a custom ratio).
if vi, _, found, loadErr := volume_info.MaybeLoadVolumeInfo(vifName); loadErr != nil {
fail(fmt.Errorf("volume %d: read %s: %w", volumeId, vifName, loadErr))
return
} else if found && vi != nil {
if cfg := vi.GetEcShardConfig(); cfg != nil && cfg.GetDataShards() > 0 {
dataShards = int(cfg.GetDataShards())
parityShards = int(cfg.GetParityShards())
}
datFileSize = vi.GetDatFileSize()
}
}
if *fixEcDataShards > 0 {
dataShards = *fixEcDataShards
}
if *fixEcParityShards > 0 {
parityShards = *fixEcParityShards
}
// Ensure the configured total covers every shard index actually present
// (a custom-ratio volume with more than the default 14 shards and no .vif).
// This never lowers parity below the default, so the common 10+4 case stays
// correct for any subset of missing shards.
if maxPresentIdx+1 > dataShards+parityShards {
parityShards = maxPresentIdx + 1 - dataShards
}
if dataShards <= 0 || parityShards <= 0 || dataShards+parityShards > erasure_coding.MaxShardCount {
fail(fmt.Errorf("volume %d: cannot determine EC ratio (data=%d parity=%d); set -ecDataShards/-ecParityShards", volumeId, dataShards, parityShards))
return
}
// Need at least dataShards shards (any data+parity mix) to recover anything.
if presentCount < dataShards {
fail(fmt.Errorf("volume %d: only %d shards present, need at least %d (data shards) to recover", volumeId, presentCount, dataShards))
return
}
// If any data shard is missing, reconstruct the missing shards from the
// survivors via Reed-Solomon before de-striping. This writes the rebuilt
// shard files back to disk, fully repairing the volume locally.
dataComplete := true
for i := 0; i < dataShards; i++ {
if !present[i] {
dataComplete = false
break
}
}
if !dataComplete {
ctx := &erasure_coding.ECContext{DataShards: dataShards, ParityShards: parityShards}
glog.Infof("volume %d: %d/%d shards present; reconstructing missing shards (%s) before index rebuild", volumeId, presentCount, dataShards+parityShards, ctx.String())
if _, err := erasure_coding.RebuildEcFilesWithContext(base, ctx); err != nil {
fail(fmt.Errorf("volume %d: reconstruct missing shards from %d survivors: %w", volumeId, presentCount, err))
return
}
}
// Collect the data shards (now all present).
shardFileNames := make([]string, dataShards)
for i := 0; i < dataShards; i++ {
shardPath := base + erasure_coding.ToExt(i)
if !util.FileExists(shardPath) {
fail(fmt.Errorf("volume %d: data shard %s still missing after reconstruction", volumeId, shardPath))
return
}
shardFileNames[i] = shardPath
}
// Without a recorded original size, reconstruct the fully padded layout; the
// scan below detects the trailing zero padding and recovers the true size.
reconstructSize := datFileSize
if reconstructSize <= 0 {
reconstructSize = int64(dataShards) * shardSize
glog.V(0).Infof("volume %d: no .dat size in .vif; reconstructing padded .dat (%d bytes) from %d data shards", volumeId, reconstructSize, dataShards)
}
// De-stripe the data shards into a temporary .dat next to the shards.
tmpBase := base + ".ecxrecover"
tmpDat := tmpBase + ".dat"
if err := erasure_coding.WriteDatFile(tmpBase, reconstructSize, shardFileNames); err != nil {
os.Remove(tmpDat)
fail(fmt.Errorf("volume %d: reconstruct .dat from data shards: %w", volumeId, err))
return
}
defer os.Remove(tmpDat)
realDatSize, version, err := writeEcxFromDat(tmpDat, ecxName)
if err != nil {
os.Remove(ecxName)
fail(fmt.Errorf("volume %d: build .ecx from reconstructed .dat: %w", volumeId, err))
return
}
glog.Infof("volume %d: wrote %s from %d data shards", volumeId, ecxName, dataShards)
// Regenerate the .vif when missing so the volume can mount and future
// rebuilds know the EC ratio and original .dat size.
if !vifExists {
size := datFileSize
if size <= 0 {
size = realDatSize
}
volumeInfo := &volume_server_pb.VolumeInfo{
Version: uint32(version),
DatFileSize: size,
EcShardConfig: &volume_server_pb.EcShardConfig{
DataShards: uint32(dataShards),
ParityShards: uint32(parityShards),
},
}
if err := volume_info.SaveVolumeInfo(vifName, volumeInfo); err != nil {
fail(fmt.Errorf("volume %d: write %s: %w", volumeId, vifName, err))
return
}
glog.Infof("volume %d: wrote %s (version %d, datFileSize %d, ec %d+%d)", volumeId, vifName, version, size, dataShards, parityShards)
}
}
// writeEcxFromDat scans a (reconstructed) .dat and writes an ascending-sorted
// .ecx containing only live needles — the same on-disk shape
// WriteSortedFileFromIdx produces when an EC volume is first encoded. It returns
// the physical .dat size (the offset where the EC zero padding begins) and the
// volume version read from the superblock.
func writeEcxFromDat(datPath, ecxPath string) (datFileSize int64, version needle.Version, err error) {
f, err := os.OpenFile(datPath, os.O_RDONLY, 0644)
if err != nil {
return 0, 0, fmt.Errorf("open %s: %w", datPath, err)
}
datBackend := backend.NewDiskFile(f)
defer datBackend.Close()
superBlock, err := super_block.ReadSuperBlock(datBackend)
if err != nil {
return 0, 0, fmt.Errorf("read superblock: %w", err)
}
version = superBlock.Version
fileSize, _, err := datBackend.GetStat()
if err != nil {
return 0, version, fmt.Errorf("stat %s: %w", datPath, err)
}
nm := needle_map.NewMemDb()
defer nm.Close()
offset := int64(superBlock.BlockSize())
for offset < fileSize {
n, _, rest, readErr := needle.ReadNeedleHeader(datBackend, version, offset)
if readErr != nil {
if readErr == io.EOF {
break
}
return 0, version, fmt.Errorf("read needle header at offset %d: %w", offset, readErr)
}
// EC encoding zero-pads the tail of the last block row. An all-zero
// header marks the start of that padding, i.e. the end of real needles.
if n.Cookie == 0 && n.Id == 0 && n.Size == 0 {
break
}
if n.Size.IsValid() {
if pe := nm.Set(n.Id, types.ToOffset(offset), n.Size); pe != nil {
return 0, version, fmt.Errorf("set needle %d: %w", n.Id, pe)
}
} else {
// Deleted/invalid: drop it so the .ecx carries only live entries,
// matching the encode-time WriteSortedFileFromIdx behavior.
if pe := nm.Delete(n.Id); pe != nil {
return 0, version, fmt.Errorf("delete needle %d: %w", n.Id, pe)
}
}
offset += types.NeedleHeaderSize + rest
}
datFileSize = offset
ecxFile, err := os.OpenFile(ecxPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return 0, version, fmt.Errorf("open %s: %w", ecxPath, err)
}
defer ecxFile.Close()
if err := nm.AscendingVisit(func(value needle_map.NeedleValue) error {
_, writeErr := ecxFile.Write(value.ToBytes())
return writeErr
}); err != nil {
return 0, version, fmt.Errorf("write %s: %w", ecxPath, err)
}
return datFileSize, version, nil
}
+273
View File
@@ -0,0 +1,273 @@
package command
import (
"bytes"
"math/rand"
"os"
"path/filepath"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/backend"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/needle_map"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
"github.com/seaweedfs/seaweedfs/weed/storage/volume_info"
)
// buildAndEncodeTestEcVolume writes a small volume (.dat + .idx), EC-encodes it
// into .ec00..ec13, and produces the canonical sorted .ecx. It returns the base
// path, the canonical .ecx bytes, and the original .dat size. A couple of
// needles are deleted so the .ecx must exclude them (live entries only).
func buildAndEncodeTestEcVolume(t *testing.T, dir, baseName string) (base string, canonicalEcx []byte, origDatSize int64) {
t.Helper()
base = filepath.Join(dir, baseName)
version := needle.GetCurrentVersion()
df, err := os.OpenFile(base+".dat", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
t.Fatal(err)
}
datBackend := backend.NewDiskFile(df)
sb := super_block.SuperBlock{
Version: version,
ReplicaPlacement: &super_block.ReplicaPlacement{},
Ttl: &needle.TTL{},
}
if _, err := datBackend.WriteAt(sb.Bytes(), 0); err != nil {
t.Fatal(err)
}
nm := needle_map.NewMemDb()
for i := uint64(1); i <= 12; i++ {
n := new(needle.Needle)
n.Id = types.Uint64ToNeedleId(i)
n.Data = make([]byte, 200+int(i))
rand.Read(n.Data)
n.Checksum = needle.NewCRC(n.Data)
offset, _, _, err := n.Append(datBackend, version)
if err != nil {
t.Fatalf("append needle %d: %v", i, err)
}
// Store n.Size (the on-disk header size), exactly what the volume
// server records in its .idx (volume_write.go: nm.Put(..., n.Size)).
if err := nm.Set(n.Id, types.ToOffset(int64(offset)), n.Size); err != nil {
t.Fatal(err)
}
}
// Delete ids 3 and 8: append an empty needle (delete record) and drop them
// from the index, exactly as the encode-time .ecx would reflect.
for _, id := range []uint64{3, 8} {
n := new(needle.Needle)
n.Id = types.Uint64ToNeedleId(id)
if _, _, _, err := n.Append(datBackend, version); err != nil {
t.Fatalf("append delete record %d: %v", id, err)
}
if err := nm.Delete(types.Uint64ToNeedleId(id)); err != nil {
t.Fatal(err)
}
}
if err := datBackend.Sync(); err != nil {
t.Fatal(err)
}
datInfo, err := df.Stat()
if err != nil {
t.Fatal(err)
}
origDatSize = datInfo.Size()
datBackend.Close()
idxFile, err := os.OpenFile(base+".idx", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
t.Fatal(err)
}
if err := nm.AscendingVisit(func(v needle_map.NeedleValue) error {
_, e := idxFile.Write(v.ToBytes())
return e
}); err != nil {
t.Fatal(err)
}
idxFile.Close()
nm.Close()
if err := erasure_coding.WriteEcFiles(base); err != nil {
t.Fatalf("WriteEcFiles: %v", err)
}
if err := erasure_coding.WriteSortedFileFromIdx(base, ".ecx"); err != nil {
t.Fatalf("WriteSortedFileFromIdx: %v", err)
}
canonicalEcx, err = os.ReadFile(base + ".ecx")
if err != nil {
t.Fatal(err)
}
if len(canonicalEcx) == 0 {
t.Fatal("canonical .ecx is empty")
}
return base, canonicalEcx, origDatSize
}
// TestFixEcxFromShards verifies the .ecx and .vif are rebuilt purely from the
// data shards when every index/metadata file has been lost.
func TestFixEcxFromShards(t *testing.T) {
oldData, oldParity := *fixEcDataShards, *fixEcParityShards
*fixEcDataShards, *fixEcParityShards = 0, 0
*fixIgnoreError = true
t.Cleanup(func() {
*fixIgnoreError = false
*fixEcDataShards, *fixEcParityShards = oldData, oldParity
})
dir := t.TempDir()
const volumeId = 7
base, canonical, origDatSize := buildAndEncodeTestEcVolume(t, dir, "7")
// Disaster: keep only the shards.
for _, ext := range []string{".ecx", ".ecj", ".idx", ".dat", ".vif"} {
if err := os.Remove(base + ext); err != nil && !os.IsNotExist(err) {
t.Fatalf("remove %s: %v", base+ext, err)
}
}
doFixEcxFromShards(dir, "7", "", volumeId)
recovered, err := os.ReadFile(base + ".ecx")
if err != nil {
t.Fatalf("recovered .ecx not written: %v", err)
}
if !bytes.Equal(canonical, recovered) {
t.Fatalf(".ecx mismatch: canonical %d bytes, recovered %d bytes", len(canonical), len(recovered))
}
// The reconstructed temporary .dat must not be left behind.
if _, err := os.Stat(base + ".ecxrecover.dat"); !os.IsNotExist(err) {
t.Fatalf("temporary reconstructed .dat was not cleaned up")
}
// .vif must be regenerated with the default ratio and the original .dat size.
vi, _, found, err := volume_info.MaybeLoadVolumeInfo(base + ".vif")
if err != nil || !found {
t.Fatalf(".vif not regenerated: found=%v err=%v", found, err)
}
if got := int(vi.GetEcShardConfig().GetDataShards()); got != erasure_coding.DataShardsCount {
t.Fatalf("data shards = %d, want %d", got, erasure_coding.DataShardsCount)
}
if got := int(vi.GetEcShardConfig().GetParityShards()); got != erasure_coding.ParityShardsCount {
t.Fatalf("parity shards = %d, want %d", got, erasure_coding.ParityShardsCount)
}
if vi.GetDatFileSize() != origDatSize {
t.Fatalf("dat size = %d, want %d", vi.GetDatFileSize(), origDatSize)
}
}
// TestFixEcxFromShardsWithVif verifies that when the .vif survives (recording
// the exact .dat size and EC ratio) the .ecx is rebuilt from it and the .vif is
// left untouched.
func TestFixEcxFromShardsWithVif(t *testing.T) {
oldData, oldParity := *fixEcDataShards, *fixEcParityShards
*fixEcDataShards, *fixEcParityShards = 0, 0
*fixIgnoreError = true
t.Cleanup(func() {
*fixIgnoreError = false
*fixEcDataShards, *fixEcParityShards = oldData, oldParity
})
dir := t.TempDir()
const volumeId = 9
base, canonical, origDatSize := buildAndEncodeTestEcVolume(t, dir, "9")
// Write a .vif as the volume server would after encoding.
if err := volume_info.SaveVolumeInfo(base+".vif", &volume_server_pb.VolumeInfo{
Version: uint32(needle.GetCurrentVersion()),
DatFileSize: origDatSize,
EcShardConfig: &volume_server_pb.EcShardConfig{
DataShards: uint32(erasure_coding.DataShardsCount),
ParityShards: uint32(erasure_coding.ParityShardsCount),
},
}); err != nil {
t.Fatal(err)
}
vifBefore, err := os.ReadFile(base + ".vif")
if err != nil {
t.Fatal(err)
}
// Lose the index but keep the surviving .vif and shards.
for _, ext := range []string{".ecx", ".ecj", ".idx", ".dat"} {
if err := os.Remove(base + ext); err != nil && !os.IsNotExist(err) {
t.Fatalf("remove %s: %v", base+ext, err)
}
}
doFixEcxFromShards(dir, "9", "", volumeId)
recovered, err := os.ReadFile(base + ".ecx")
if err != nil {
t.Fatalf("recovered .ecx not written: %v", err)
}
if !bytes.Equal(canonical, recovered) {
t.Fatalf(".ecx mismatch: canonical %d bytes, recovered %d bytes", len(canonical), len(recovered))
}
// An existing .vif must be left untouched.
vifAfter, err := os.ReadFile(base + ".vif")
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(vifBefore, vifAfter) {
t.Fatalf("existing .vif was modified")
}
}
// TestFixEcxFromShardsMissingShards verifies that when some shards (including a
// couple of data shards) are lost but at least dataShards survive, the missing
// shards are reconstructed from parity and the .ecx is still rebuilt correctly.
func TestFixEcxFromShardsMissingShards(t *testing.T) {
oldData, oldParity := *fixEcDataShards, *fixEcParityShards
*fixEcDataShards, *fixEcParityShards = 0, 0
*fixIgnoreError = true
t.Cleanup(func() {
*fixIgnoreError = false
*fixEcDataShards, *fixEcParityShards = oldData, oldParity
})
dir := t.TempDir()
const volumeId = 11
base, canonical, origDatSize := buildAndEncodeTestEcVolume(t, dir, "11")
// Lose every index/metadata file plus three shards (two data: .ec02, .ec05;
// one parity: .ec11), keeping 11 of 14 — enough to reconstruct. The highest
// shard (.ec13) is kept so the default 10+4 ratio is inferred without a .vif.
for _, ext := range []string{".ecx", ".ecj", ".idx", ".dat", ".vif",
erasure_coding.ToExt(2), erasure_coding.ToExt(5), erasure_coding.ToExt(11)} {
if err := os.Remove(base + ext); err != nil && !os.IsNotExist(err) {
t.Fatalf("remove %s: %v", base+ext, err)
}
}
doFixEcxFromShards(dir, "11", "", volumeId)
recovered, err := os.ReadFile(base + ".ecx")
if err != nil {
t.Fatalf("recovered .ecx not written: %v", err)
}
if !bytes.Equal(canonical, recovered) {
t.Fatalf(".ecx mismatch: canonical %d bytes, recovered %d bytes", len(canonical), len(recovered))
}
// The missing shards must have been reconstructed on disk.
for _, idx := range []int{2, 5, 11} {
if info, err := os.Stat(base + erasure_coding.ToExt(idx)); err != nil || info.Size() == 0 {
t.Fatalf("missing shard %d was not reconstructed: err=%v", idx, err)
}
}
vi, _, found, err := volume_info.MaybeLoadVolumeInfo(base + ".vif")
if err != nil || !found {
t.Fatalf(".vif not regenerated: found=%v err=%v", found, err)
}
if vi.GetDatFileSize() != origDatSize {
t.Fatalf("dat size = %d, want %d", vi.GetDatFileSize(), origDatSize)
}
}
+10 -7
View File
@@ -172,7 +172,7 @@ func iterateEcjFile(baseFileName string, processNeedleFn func(key types.NeedleId
}
// WriteDatFile generates .dat from .ec00 ~ .ec09 files
// WriteDatFile generates .dat from EC shard files (e.g., .ec00 ~ .ec09 for 10+4)
func WriteDatFile(baseFileName string, datFileSize int64, shardFileNames []string) error {
datFile, openErr := os.OpenFile(baseFileName+".dat", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
@@ -181,25 +181,28 @@ func WriteDatFile(baseFileName string, datFileSize int64, shardFileNames []strin
}
defer datFile.Close()
inputFiles := make([]*os.File, DataShardsCount)
// Use the actual number of data shards passed in rather than the global
// constant, so the de-striping matches the caller's shard set.
dataShards := len(shardFileNames)
inputFiles := make([]*os.File, dataShards)
defer func() {
for shardId := 0; shardId < DataShardsCount; shardId++ {
for shardId := 0; shardId < dataShards; shardId++ {
if inputFiles[shardId] != nil {
inputFiles[shardId].Close()
}
}
}()
for shardId := 0; shardId < DataShardsCount; shardId++ {
for shardId := 0; shardId < dataShards; shardId++ {
inputFiles[shardId], openErr = os.OpenFile(shardFileNames[shardId], os.O_RDONLY, 0)
if openErr != nil {
return openErr
}
}
for datFileSize >= DataShardsCount*ErasureCodingLargeBlockSize {
for shardId := 0; shardId < DataShardsCount; shardId++ {
for datFileSize >= int64(dataShards)*ErasureCodingLargeBlockSize {
for shardId := 0; shardId < dataShards; shardId++ {
w, err := io.CopyN(datFile, inputFiles[shardId], ErasureCodingLargeBlockSize)
if w != ErasureCodingLargeBlockSize {
return fmt.Errorf("copy %s large block on shardId %d: %v", baseFileName, shardId, err)
@@ -209,7 +212,7 @@ func WriteDatFile(baseFileName string, datFileSize int64, shardFileNames []strin
}
for datFileSize > 0 {
for shardId := 0; shardId < DataShardsCount; shardId++ {
for shardId := 0; shardId < dataShards; shardId++ {
toRead := min(datFileSize, ErasureCodingSmallBlockSize)
w, err := io.CopyN(datFile, inputFiles[shardId], toRead)
if w != toRead {