mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-24 06:50:40 +02:00
* ec: compare live idx NeedleId sets before encode Refuse encode when replicas' live .idx NeedleId sets differ. Walk only live rows (skip deleted and Size<=0) and compare NeedleId, ignoring Size so MIME/flags drift does not abort a matching id set. Mark the volume read-only for encode and reject writes after that. * fix(ec): compare all replicas then pick capacity source Log matching live idx sets with "replicas are consistent" for the shell CI check. After readonly, compare every replica's live NeedleId set, then require the encode source to have FreeVolumeCount>=2. Empty live-idx vs non-empty refuses encode instead of skipping superblock stubs. Drop the FileCount rebind after the idx gate. * volume: mark readonly under the write lock so checked appends finish first * ec: fix readonly deadlock and restore worker-host source preference MarkVolumeReadonly held dataFileAccessLock while calling reopenIdxForWrite, which re-acquires the same mutex and deadlocks TestMarkVolumeReadonlyCanDelete. Split out reopenIdxForWriteLocked for callers already holding the lock, matching the existing Locked pattern. The worker task lost its worker-host source preference when the idx gate replaced the replica union. Late-bind the source to a replica on the worker host using only the checked location set, with no FileCount rebinding. Drop the now-unused helpers, trim comments added by the earlier commits, and rename tests whose names still claimed convergence while asserting refusal. --------- Co-authored-by: Chepurnov Simon <195125934+CHEPURNOV-S-S@users.noreply.github.com> Co-authored-by: Devin <devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Chris Lu <chris.lu@gmail.com>
196 lines
5.7 KiB
Go
196 lines
5.7 KiB
Go
package volume_replica
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle_map"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
"github.com/seaweedfs/seaweedfs/weed/wdclient"
|
|
)
|
|
|
|
func TestEqualLiveNeedleSets_SameFileCountDifferentIds(t *testing.T) {
|
|
a := needle_map.NewMemDb()
|
|
b := needle_map.NewMemDb()
|
|
defer a.Close()
|
|
defer b.Close()
|
|
|
|
// Equal cardinality, different ids — FileCount would match, live sets must not.
|
|
a.Set(types.NeedleId(1), types.ToOffset(8), types.Size(100))
|
|
a.Set(types.NeedleId(2), types.ToOffset(16), types.Size(100))
|
|
a.Set(types.NeedleId(3), types.ToOffset(24), types.Size(100))
|
|
b.Set(types.NeedleId(1), types.ToOffset(8), types.Size(100))
|
|
b.Set(types.NeedleId(2), types.ToOffset(16), types.Size(100))
|
|
b.Set(types.NeedleId(4), types.ToOffset(24), types.Size(100))
|
|
|
|
la, err := LiveNeedlesFromIndex(a)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lb, err := LiveNeedlesFromIndex(b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if EqualLiveNeedleSets(la, lb) {
|
|
t.Fatalf("equal FileCount with different ids must not match: a=%v b=%v", la, lb)
|
|
}
|
|
}
|
|
|
|
func TestEqualLiveNeedleSets_VacuumOffsetsIgnored(t *testing.T) {
|
|
a := needle_map.NewMemDb()
|
|
b := needle_map.NewMemDb()
|
|
defer a.Close()
|
|
defer b.Close()
|
|
|
|
a.Set(types.NeedleId(10), types.ToOffset(8), types.Size(50))
|
|
a.Set(types.NeedleId(11), types.ToOffset(64), types.Size(80))
|
|
// Vacuum rewrote the same live needles to new offsets.
|
|
b.Set(types.NeedleId(10), types.ToOffset(8), types.Size(50))
|
|
b.Set(types.NeedleId(11), types.ToOffset(200), types.Size(80))
|
|
|
|
la, err := LiveNeedlesFromIndex(a)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lb, err := LiveNeedlesFromIndex(b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !EqualLiveNeedleSets(la, lb) {
|
|
t.Fatalf("same ids with different offsets must match: a=%v b=%v", la, lb)
|
|
}
|
|
}
|
|
|
|
func TestEqualLiveNeedleSets_SizeMismatchIgnored(t *testing.T) {
|
|
a := needle_map.NewMemDb()
|
|
b := needle_map.NewMemDb()
|
|
defer a.Close()
|
|
defer b.Close()
|
|
|
|
a.Set(types.NeedleId(10), types.ToOffset(8), types.Size(50))
|
|
b.Set(types.NeedleId(10), types.ToOffset(8), types.Size(74))
|
|
|
|
la, err := LiveNeedlesFromIndex(a)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lb, err := LiveNeedlesFromIndex(b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !EqualLiveNeedleSets(la, lb) {
|
|
t.Fatalf("same NeedleId with different Size must match: a=%v b=%v", la, lb)
|
|
}
|
|
}
|
|
|
|
func TestLiveNeedlesFromIndex_SkipsDeleted(t *testing.T) {
|
|
db := needle_map.NewMemDb()
|
|
defer db.Close()
|
|
db.Set(types.NeedleId(1), types.ToOffset(8), types.Size(40))
|
|
db.Set(types.NeedleId(2), types.ToOffset(16), types.TombstoneFileSize)
|
|
db.Set(types.NeedleId(3), types.ToOffset(24), types.Size(0))
|
|
|
|
live, err := LiveNeedlesFromIndex(db)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(live) != 1 || live[0] != 1 {
|
|
t.Fatalf("live=%v, want only id=1", live)
|
|
}
|
|
}
|
|
|
|
func TestSelectSourceIfIndexesMatch_SingleReplica(t *testing.T) {
|
|
loc := wdclient.Location{Url: "127.0.0.1:8080"}
|
|
got, err := SelectSourceIfIndexesMatch(nil, needle.VolumeId(7), "", []wdclient.Location{loc}, "other", io.Discard)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Url != loc.Url {
|
|
t.Fatalf("got %+v, want %+v", got, loc)
|
|
}
|
|
}
|
|
|
|
func TestSelectSourceIfIndexesMatch_NoReplicas(t *testing.T) {
|
|
_, err := SelectSourceIfIndexesMatch(nil, needle.VolumeId(7), "", nil, "", io.Discard)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty replica list")
|
|
}
|
|
}
|
|
|
|
func TestChooseSourceFromLiveNeedles_SuccessContainsConsistent(t *testing.T) {
|
|
a := wdclient.Location{Url: "127.0.0.1:8080"}
|
|
b := wdclient.Location{Url: "127.0.0.1:8081"}
|
|
live := []types.NeedleId{1, 2}
|
|
var buf bytes.Buffer
|
|
got, err := chooseSourceFromLiveNeedles(7, []replicaLiveIdx{
|
|
{loc: a, live: live},
|
|
{loc: b, live: append([]types.NeedleId(nil), live...)},
|
|
}, "", &buf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Url != a.Url {
|
|
t.Fatalf("got %+v, want %+v", got, a)
|
|
}
|
|
if !strings.Contains(buf.String(), "replicas are consistent") {
|
|
t.Fatalf("success log %q must contain %q", buf.String(), "replicas are consistent")
|
|
}
|
|
wantLine := "volume 7: all 2 data replicas are consistent (matching live idx sets, 2 needles)"
|
|
if !strings.Contains(buf.String(), wantLine) {
|
|
t.Fatalf("success log %q must contain %q", buf.String(), wantLine)
|
|
}
|
|
}
|
|
|
|
func TestChooseSourceFromLiveNeedles_EmptyVsLiveRefuses(t *testing.T) {
|
|
a := wdclient.Location{Url: "127.0.0.1:8080"}
|
|
b := wdclient.Location{Url: "127.0.0.1:8081"}
|
|
_, err := chooseSourceFromLiveNeedles(7, []replicaLiveIdx{
|
|
{loc: a, live: []types.NeedleId{1}},
|
|
{loc: b, live: nil}, // empty / superblock stub
|
|
}, "", io.Discard)
|
|
if err == nil {
|
|
t.Fatal("empty live-idx vs non-empty must error, not skip")
|
|
}
|
|
if strings.Contains(err.Error(), "skipping") {
|
|
t.Fatalf("must refuse, not skip: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestChooseSourceFromLiveNeedles_AllEmpty(t *testing.T) {
|
|
a := wdclient.Location{Url: "127.0.0.1:8080"}
|
|
b := wdclient.Location{Url: "127.0.0.1:8081"}
|
|
_, err := chooseSourceFromLiveNeedles(7, []replicaLiveIdx{
|
|
{loc: a, live: nil},
|
|
{loc: b, live: nil},
|
|
}, "", io.Discard)
|
|
if err == nil {
|
|
t.Fatal("all empty live-idx must error")
|
|
}
|
|
if !strings.Contains(err.Error(), "no data-bearing replicas") {
|
|
t.Fatalf("got %v, want no data-bearing replicas", err)
|
|
}
|
|
}
|
|
|
|
func TestRequireCapacitySource_ThreeLocationsSourceFromCapacity(t *testing.T) {
|
|
fat := wdclient.Location{Url: "fat:8080"}
|
|
cap1 := wdclient.Location{Url: "cap1:8080"}
|
|
cap2 := wdclient.Location{Url: "cap2:8080"}
|
|
filtered := []wdclient.Location{cap1, cap2}
|
|
|
|
got, err := RequireCapacitySource(1, cap1, filtered)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Url != cap1.Url {
|
|
t.Fatalf("got %+v, want capacity source %+v", got, cap1)
|
|
}
|
|
|
|
_, err = RequireCapacitySource(1, fat, filtered)
|
|
if err == nil {
|
|
t.Fatal("must refuse encode from first fat replica")
|
|
}
|
|
}
|