mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-07-27 18:43:43 +00:00
* fix(storage): never let an empty .dat delete healthy distributed EC shards A leftover empty .dat stub (a phantom from the pre-fix loader; zero needles) next to a distributed EC volume's local shards made startup classify the volume as an interrupted local encode: validateEcVolume requires >= dataShards local shards when a .dat is present, fails with the 1-2 shards a distributed volume keeps per disk, and the cleanup deletes those shards -- the only copies of that part of the volume. Repeated across restart waves this destroys enough shards cluster-wide to make the volume unrecoverable. Go: - loadExistingVolume: hoist the empty-stub sweep above the EC presence checks. Previously the .vif-next-to-.ecx guard returned before the sweep ever ran, so exactly the dangerous layout (stub + .ecx + local shards) kept its stub and then lost its shards in loadAllEcShards. - validateEcVolume / checkDatFileExists: treat a .dat <= a superblock (zero needles) as absent. An empty .dat cannot be the encode source, so it must never gate shard deletion; this also covers stubs without a .vif, which the sweep cannot prove are EC leftovers. Rust mirror (seaweed-volume): the same gate in validate_ec_volume and check_dat_file_exists (the Rust sweep already ran before validation); the volume-load skip keeps a plain existence check so fresh, needle-less volumes still load. Regression tests in Go and Rust reproduce the production layout (a zero-byte .dat beside .ecx/.ecj and two shards of a 10+4 volume, with and without a .vif) and fail without the fix with the shards deleted. * fix(ec): gate source volume deletion on a recoverable shard set After EC encode, the shell command and the (plugin) worker task refused to delete the source volume unless every shard was present, and aborted otherwise -- leaving the source .dat next to live shards, exactly the mixed state the startup cleanup mishandles. Replace the full-set requirement with a recoverability gate shared by both callers (RequireRecoverableShardSet): deleting a non-empty source .dat requires at least dataShards distinct shards cluster-wide. Below that the source is kept and the encode fails as before. A degraded but recoverable set (>= dataShards, < total) now proceeds with a warning instead of aborting: the missing shards can be rebuilt from the survivors, while keeping the source would preserve the dangerous mixed state. Empty stub replicas are still swept unguarded (OnlyEmpty) -- an empty .dat has nothing to lose. dataShards/totalShards stay parameters so enterprise custom EC ratios share the helper verbatim. * test(ec): use recoverable shard verification gate
143 lines
4.5 KiB
Go
143 lines
4.5 KiB
Go
package erasure_coding
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRequireRecoverableShardSet_AllPresent(t *testing.T) {
|
|
var bits ShardBits
|
|
for id := 0; id < TotalShardsCount; id++ {
|
|
bits = bits.Set(ShardId(id))
|
|
}
|
|
degraded, err := RequireRecoverableShardSet(42, bits, DataShardsCount, TotalShardsCount)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error for full set: %v", err)
|
|
}
|
|
if degraded {
|
|
t.Error("full set must not be reported as degraded")
|
|
}
|
|
}
|
|
|
|
func TestRequireRecoverableShardSet_DegradedButRecoverable(t *testing.T) {
|
|
// 12 of 14 shards: enough to reconstruct (>= 10), so the source may be
|
|
// deleted, but the caller is told to warn and schedule a rebuild.
|
|
var bits ShardBits
|
|
for id := 0; id < TotalShardsCount; id++ {
|
|
if id == 3 || id == 7 {
|
|
continue
|
|
}
|
|
bits = bits.Set(ShardId(id))
|
|
}
|
|
degraded, err := RequireRecoverableShardSet(42, bits, DataShardsCount, TotalShardsCount)
|
|
if err != nil {
|
|
t.Fatalf("recoverable set must not error: %v", err)
|
|
}
|
|
if !degraded {
|
|
t.Error("missing shards must be reported as degraded")
|
|
}
|
|
}
|
|
|
|
func TestRequireRecoverableShardSet_BelowDataShards(t *testing.T) {
|
|
// 9 of 14 shards: one short of reconstructable; the source must be kept.
|
|
var bits ShardBits
|
|
for id := 0; id < DataShardsCount-1; id++ {
|
|
bits = bits.Set(ShardId(id))
|
|
}
|
|
_, err := RequireRecoverableShardSet(42, bits, DataShardsCount, TotalShardsCount)
|
|
if err == nil {
|
|
t.Fatal("expected error for unrecoverable set, got nil")
|
|
}
|
|
msg := err.Error()
|
|
if !strings.Contains(msg, "volume 42") {
|
|
t.Errorf("error should name the volume id: %s", msg)
|
|
}
|
|
if !strings.Contains(msg, "9/14") {
|
|
t.Errorf("error should report 9/14 shards present: %s", msg)
|
|
}
|
|
if !strings.Contains(msg, "[9 10 11 12 13]") {
|
|
t.Errorf("error should list the missing ids: %s", msg)
|
|
}
|
|
}
|
|
|
|
func TestRequireRecoverableShardSet_EmptyBitmap(t *testing.T) {
|
|
_, err := RequireRecoverableShardSet(1, 0, DataShardsCount, TotalShardsCount)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty bitmap")
|
|
}
|
|
if !strings.Contains(err.Error(), "0/14") {
|
|
t.Errorf("error should report 0/14 shards: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestRequireRecoverableShardSet_CustomRatio(t *testing.T) {
|
|
// 6+3 ratio: total=9, all present
|
|
var bits ShardBits
|
|
for id := 0; id < 9; id++ {
|
|
bits = bits.Set(ShardId(id))
|
|
}
|
|
if degraded, err := RequireRecoverableShardSet(7, bits, 6, 9); err != nil || degraded {
|
|
t.Fatalf("full 6+3 set: degraded=%v err=%v", degraded, err)
|
|
}
|
|
|
|
// 6+3, missing shard 5: 8 >= 6 remain, recoverable but degraded
|
|
bits = bits.Clear(5)
|
|
if degraded, err := RequireRecoverableShardSet(7, bits, 6, 9); err != nil || !degraded {
|
|
t.Fatalf("8/9 shards of 6+3: degraded=%v err=%v", degraded, err)
|
|
}
|
|
|
|
// 6+3, only 5 shards left: below dataShards, must error
|
|
bits = ShardBits(0)
|
|
for id := 0; id < 5; id++ {
|
|
bits = bits.Set(ShardId(id))
|
|
}
|
|
_, err := RequireRecoverableShardSet(7, bits, 6, 9)
|
|
if err == nil {
|
|
t.Fatal("expected error with 5/9 shards in 6+3 ratio")
|
|
}
|
|
if !strings.Contains(err.Error(), "5/9") {
|
|
t.Errorf("error should report 5/9: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestRequireRecoverableShardSet_RejectsInvalidParams(t *testing.T) {
|
|
if _, err := RequireRecoverableShardSet(1, 0, DataShardsCount, 0); err == nil {
|
|
t.Error("expected error for totalShards=0")
|
|
}
|
|
if _, err := RequireRecoverableShardSet(1, 0, DataShardsCount, MaxShardCount+1); err == nil {
|
|
t.Errorf("expected error for totalShards > MaxShardCount")
|
|
}
|
|
if _, err := RequireRecoverableShardSet(1, 0, 0, TotalShardsCount); err == nil {
|
|
t.Error("expected error for dataShards=0")
|
|
}
|
|
if _, err := RequireRecoverableShardSet(1, 0, TotalShardsCount+1, TotalShardsCount); err == nil {
|
|
t.Error("expected error for dataShards > totalShards")
|
|
}
|
|
}
|
|
|
|
func TestSummarizeShardInventory_Deterministic(t *testing.T) {
|
|
perServer := map[string]ServerShardInventory{
|
|
"10.0.0.2:8080": {Bits: ShardBits(0).Set(4).Set(5).Set(6)},
|
|
"10.0.0.1:8080": {Bits: ShardBits(0).Set(0).Set(1).Set(2).Set(3)},
|
|
}
|
|
got := SummarizeShardInventory(perServer)
|
|
want := "10.0.0.1:8080=[0 1 2 3] 10.0.0.2:8080=[4 5 6]"
|
|
if got != want {
|
|
t.Errorf("summary mismatch\n got: %q\n want: %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestSummarizeShardInventory_IncludesError(t *testing.T) {
|
|
perServer := map[string]ServerShardInventory{
|
|
"10.0.0.1:8080": {Bits: ShardBits(0).Set(0).Set(1), QueryError: errStr("dial timeout")},
|
|
}
|
|
got := SummarizeShardInventory(perServer)
|
|
if !strings.Contains(got, "ERR:dial timeout") {
|
|
t.Errorf("expected error tag in summary, got %q", got)
|
|
}
|
|
}
|
|
|
|
type errStr string
|
|
|
|
func (e errStr) Error() string { return string(e) }
|