Files
seaweedfs/weed/storage/store_ec_reconstruct_test.go
T
Chris Lu c858e01a09 ec: split the shard-interval recovery into a gather and a rebuild (#11005)
* ec: split the shard-interval recovery into a gather and a rebuild

Recovering an interval is now one function doing the local seeding, the waved
peer fetch, the shard accounting and the Reed-Solomon rebuild, under a memory
budget. Splitting the gather from the rebuild makes the rebuild a plain
function over a set of intervals, which is testable on its own and reusable by
the parity checks a full scrub wants.

The rebuild refuses a parity target, and the caller checks that before the
gather so a doomed target costs no fan-out. ReconstructData rebuilds data
shards only, so asking it for a parity shard returned no error and left the
slot nil, and the caller copied that out as a successful read of zeroes. Only
data shard ids reach here today, so this is a guard, not a live fix.

Claude-Session: https://claude.ai/code/session_014yMNebkUjSbx9sfUCWJJtq

* ec: rebuild only the EC shard the read asked for

ReconstructData rebuilds every missing data shard. The gather stops as soon as
DataShards intervals are in hand, so on a distributed volume it routinely
finishes holding parity where data is missing -- and each of those data shards
is then rebuilt into an interval-sized buffer, decoded, and never read. Ask for
the one shard the read needs.

The budget covers it now too: DataShards gathered plus the one the rebuild
allocates. It never covered the rebuild's output, and with ReconstructData that
output was up to ParityShards buffers.

The required mask is Total() long rather than DataShards. reedsolomon documents
both lengths, but its presence scan walks every shard and indexes the short
mask past its end, so the documented short form panics whenever a parity shard
is absent - which here it usually is.

Claude-Session: https://claude.ai/code/session_014yMNebkUjSbx9sfUCWJJtq
2026-08-28 15:55:12 -07:00

119 lines
4.1 KiB
Go

package storage
import (
"bytes"
"math/rand"
"strings"
"testing"
"github.com/klauspost/reedsolomon"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)
// encodedInterval returns one interval's worth of every shard, Reed-Solomon encoded
// from random data, along with the context describing the ratio.
func encodedInterval(t *testing.T, intervalSize int) ([][]byte, *erasure_coding.ECContext) {
t.Helper()
ecCtx := erasure_coding.NewDefaultECContext("", 0)
shardIntervals := make([][]byte, ecCtx.Total())
r := rand.New(rand.NewSource(1))
for i := range shardIntervals {
shardIntervals[i] = make([]byte, intervalSize)
}
for i := 0; i < ecCtx.DataShards; i++ {
r.Read(shardIntervals[i])
}
enc, err := reedsolomon.New(ecCtx.DataShards, ecCtx.ParityShards)
if err != nil {
t.Fatalf("new encoder: %v", err)
}
if err := enc.Encode(shardIntervals); err != nil {
t.Fatalf("encode: %v", err)
}
return shardIntervals, ecCtx
}
func TestReconstructEcShardIntervalRebuildsDataShard(t *testing.T) {
shardIntervals, ecCtx := encodedInterval(t, 1024)
ecVolume := &erasure_coding.EcVolume{VolumeId: needle.VolumeId(1), ECContext: ecCtx}
const lost = erasure_coding.ShardId(3)
want := bytes.Clone(shardIntervals[lost])
shardIntervals[lost] = nil
// drop as many others as parity allows, so the rebuild really goes through parity
for i := ecCtx.Total() - ecCtx.ParityShards + 1; i < ecCtx.Total(); i++ {
shardIntervals[i] = nil
}
if err := reconstructEcShardInterval(ecVolume, ecCtx, shardIntervals, lost); err != nil {
t.Fatalf("reconstruct: %v", err)
}
if !bytes.Equal(shardIntervals[lost], want) {
t.Fatalf("rebuilt shard %d does not match the encoded bytes", lost)
}
}
// ReconstructData rebuilds data shards only, leaving a parity slot nil. Reporting that
// as a success would hand the caller a zero-filled buffer as if it had been read.
func TestReconstructEcShardIntervalRejectsParityShard(t *testing.T) {
shardIntervals, ecCtx := encodedInterval(t, 1024)
ecVolume := &erasure_coding.EcVolume{VolumeId: needle.VolumeId(1), ECContext: ecCtx}
parity := erasure_coding.ShardId(ecCtx.DataShards)
shardIntervals[parity] = nil
err := reconstructEcShardInterval(ecVolume, ecCtx, shardIntervals, parity)
if err == nil {
t.Fatalf("rebuilding parity shard %d reported success, buffer is %v", parity, shardIntervals[parity])
}
if !strings.Contains(err.Error(), "only data shards can be rebuilt") {
t.Fatalf("error %q, want it to say parity cannot be rebuilt", err)
}
}
func TestReconstructEcShardIntervalNeedsDataShardCount(t *testing.T) {
shardIntervals, ecCtx := encodedInterval(t, 1024)
ecVolume := &erasure_coding.EcVolume{VolumeId: needle.VolumeId(1), ECContext: ecCtx}
// one shard short of what the ratio needs
for i := 0; i <= ecCtx.ParityShards; i++ {
shardIntervals[i] = nil
}
err := reconstructEcShardInterval(ecVolume, ecCtx, shardIntervals, 0)
if err == nil || !strings.Contains(err.Error(), "need at least") {
t.Fatalf("error %v, want it to report too few shards", err)
}
}
// A gather that filled up on parity leaves several data shards missing, and only one
// of them is the shard anybody asked for.
func TestReconstructEcShardIntervalRebuildsOnlyTheTarget(t *testing.T) {
shardIntervals, ecCtx := encodedInterval(t, 1024)
ecVolume := &erasure_coding.EcVolume{VolumeId: needle.VolumeId(1), ECContext: ecCtx}
const lost = erasure_coding.ShardId(0)
want := bytes.Clone(shardIntervals[lost])
// exactly DataShards left in hand, ParityShards of the data shards missing
spare := []erasure_coding.ShardId{4, 6, 8}
shardIntervals[lost] = nil
for _, sid := range spare {
shardIntervals[sid] = nil
}
if err := reconstructEcShardInterval(ecVolume, ecCtx, shardIntervals, lost); err != nil {
t.Fatalf("reconstruct: %v", err)
}
if !bytes.Equal(shardIntervals[lost], want) {
t.Fatalf("rebuilt shard %d does not match the encoded bytes", lost)
}
for _, sid := range spare {
if shardIntervals[sid] != nil {
t.Errorf("shard %d was rebuilt too, only shard %d was asked for", sid, lost)
}
}
}