Files
seaweedfs/weed/storage/blockvol/shipper_group_test.go
T
Ping QiuandClaude Opus 4.6 8d6379f841 feat: replica state machine + barrier eligibility gating (CP13-4)
Replaces binary degraded flag with ReplicaState type:
Disconnected, Connecting, CatchingUp, InSync, Degraded, NeedsRebuild.

Ship() allowed from Disconnected (bootstrap: data must flow before
first barrier) and InSync (steady state). Ship does NOT change state.

Barrier() gating:
- InSync: proceed normally
- Disconnected: bootstrap path (connect + barrier)
- Degraded: reconnect both data+ctrl connections, then barrier
- Connecting/CatchingUp/NeedsRebuild: rejected immediately

Only barrier success grants InSync. Reconnect alone does not.

IsDegraded() now means "not sync-eligible" (any non-InSync state).
InSyncCount() added to ShipperGroup.

dist_group_commit.go: removed AllDegraded short-circuit that
prevented bootstrap. Barrier attempts always run — individual
shippers handle their own state-based gating.

8 CP13-4 tests + TestBarrier_RejectsReplicaNotInSync flips FAIL→PASS.
All previously-passing baseline tests remain green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 02:39:32 -07:00

119 lines
3.2 KiB
Go

package blockvol
import (
"sync/atomic"
"testing"
)
func TestShipperGroup_ShipAll_Single(t *testing.T) {
s := newTestShipper()
sg := NewShipperGroup([]*WALShipper{s})
entry := &WALEntry{LSN: 1, Epoch: 1, Type: EntryTypeWrite}
sg.ShipAll(entry)
// Ship on a non-connected shipper degrades it but doesn't panic.
if sg.Len() != 1 {
t.Fatalf("Len: got %d, want 1", sg.Len())
}
}
func TestShipperGroup_ShipAll_Two(t *testing.T) {
s1 := newTestShipper()
s2 := newTestShipper()
sg := NewShipperGroup([]*WALShipper{s1, s2})
entry := &WALEntry{LSN: 1, Epoch: 1, Type: EntryTypeWrite}
sg.ShipAll(entry)
if sg.Len() != 2 {
t.Fatalf("Len: got %d, want 2", sg.Len())
}
}
func TestShipperGroup_BarrierAll_AllSucceed(t *testing.T) {
// Create shippers that are stopped (Barrier returns ErrShipperStopped).
// We test the fan-out logic: all return errors, which is fine.
s1 := newTestShipper()
s2 := newTestShipper()
sg := NewShipperGroup([]*WALShipper{s1, s2})
errs := sg.BarrierAll(10)
if len(errs) != 2 {
t.Fatalf("BarrierAll: got %d errors, want 2", len(errs))
}
}
func TestShipperGroup_BarrierAll_OneFail(t *testing.T) {
s1 := newTestShipper()
s1.state.Store(uint32(ReplicaDegraded))
s2 := newTestShipper()
sg := NewShipperGroup([]*WALShipper{s1, s2})
errs := sg.BarrierAll(10)
if errs[0] == nil {
t.Fatalf("expected error for degraded shipper[0]")
}
}
func TestShipperGroup_BarrierAll_AllFail(t *testing.T) {
s1 := newTestShipper()
s1.state.Store(uint32(ReplicaDegraded))
s2 := newTestShipper()
s2.state.Store(uint32(ReplicaDegraded))
sg := NewShipperGroup([]*WALShipper{s1, s2})
errs := sg.BarrierAll(10)
for i, e := range errs {
if e == nil {
t.Fatalf("expected error for shipper[%d]", i)
}
}
}
func TestShipperGroup_AllDegraded_Empty(t *testing.T) {
sg := NewShipperGroup(nil)
if sg.AllDegraded() {
t.Fatal("empty group should not be AllDegraded")
}
}
func TestShipperGroup_AllDegraded_Mixed(t *testing.T) {
s1 := newTestShipper()
s1.state.Store(uint32(ReplicaDegraded))
s2 := newTestShipper()
s2.state.Store(uint32(ReplicaInSync)) // one healthy, one degraded
sg := NewShipperGroup([]*WALShipper{s1, s2})
if sg.AllDegraded() {
t.Fatal("mixed group should not be AllDegraded")
}
if !sg.AnyDegraded() {
t.Fatal("mixed group should be AnyDegraded")
}
}
func TestShipperGroup_StopAll(t *testing.T) {
s1 := newTestShipper()
s2 := newTestShipper()
sg := NewShipperGroup([]*WALShipper{s1, s2})
sg.StopAll()
if !s1.stopped.Load() || !s2.stopped.Load() {
t.Fatal("StopAll should stop all shippers")
}
}
func TestShipperGroup_DegradedCount(t *testing.T) {
s1 := newTestShipper()
s2 := newTestShipper()
s1.state.Store(uint32(ReplicaDegraded))
s2.state.Store(uint32(ReplicaInSync)) // one healthy, one degraded
sg := NewShipperGroup([]*WALShipper{s1, s2})
if got := sg.DegradedCount(); got != 1 {
t.Fatalf("DegradedCount: got %d, want 1", got)
}
}
// newTestShipper creates a WALShipper with a fixed epoch, not connected to anything.
func newTestShipper() *WALShipper {
var epoch atomic.Uint64
epoch.Store(1)
return &WALShipper{
dataAddr: "127.0.0.1:0",
controlAddr: "127.0.0.1:0",
epochFn: func() uint64 { return epoch.Load() },
}
}