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 authored and GitHub committed 2026-05-21 00:41:27 -07:00
1 parent 9b9fdb5b76
commit 303c2be38d
3 files changed
+579 -7

No files matched your search

+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 {