mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-10 16:40:46 +02:00
* 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
119 lines
4.1 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|