mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-19 06:31:54 +00:00
Pinner wired to real retention: - NewPinner calls vol.SetV2RetentionFloor(p.MinWALRetentionFloor) - Flusher.RetentionFloorFn() / SetRetentionFloorFn() exposed - SetV2RetentionFloor chains with existing shipper retention floor - Holds actually prevent WAL reclaim (not just tracked state) Executor uses real WAL scan: - BlockVol.ScanWALEntries(fromLSN, callback) wraps wal.ScanFrom with real fd, walOffset, checkpointLSN - Executor.StreamWALEntries uses ScanWALEntries (not stub) - Reads real WAL entries, tracks highest LSN scanned CommittedLSN mapping: - Explicitly documented as interim V1 model (committed = checkpointed) - Will diverge when V2 distributed commit separates from local flush Carry-forward: - TransferSnapshot/TransferFullBase/TruncateWAL: stubs (need extent I/O) - Control intent from confirmed failover: deferred Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
234 lines
6.2 KiB
Go
234 lines
6.2 KiB
Go
package testrunner
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// mockNode implements NodeRunner for testing.
|
|
type mockNode struct {
|
|
commands []string
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func (m *mockNode) Run(ctx context.Context, cmd string) (string, string, int, error) {
|
|
m.mu.Lock()
|
|
m.commands = append(m.commands, cmd)
|
|
m.mu.Unlock()
|
|
// Simulate curl responses for cluster probing.
|
|
if strings.Contains(cmd, "/cluster/status") {
|
|
return `{"IsLeader":true}`, "", 0, nil
|
|
}
|
|
if strings.Contains(cmd, "/dir/status") {
|
|
return `{"Topology":{"DataCenters":[{"Racks":[{"DataNodes":[{},{}]}]}]}}`, "", 0, nil
|
|
}
|
|
return "", "", 0, nil
|
|
}
|
|
|
|
func (m *mockNode) RunRoot(ctx context.Context, cmd string) (string, string, int, error) {
|
|
m.mu.Lock()
|
|
m.commands = append(m.commands, "ROOT:"+cmd)
|
|
m.mu.Unlock()
|
|
if strings.Contains(cmd, "nohup") && strings.Contains(cmd, "weed master") {
|
|
return "12345", "", 0, nil
|
|
}
|
|
if strings.Contains(cmd, "nohup") && strings.Contains(cmd, "weed volume") {
|
|
return "12346", "", 0, nil
|
|
}
|
|
return "", "", 0, nil
|
|
}
|
|
|
|
func (m *mockNode) Upload(local, remote string) error { return nil }
|
|
func (m *mockNode) Close() {}
|
|
|
|
func (m *mockNode) hasCommand(substr string) bool {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
for _, c := range m.commands {
|
|
if strings.Contains(c, substr) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestClusterManager_NilSpec_Noop(t *testing.T) {
|
|
cm := NewClusterManager(nil, t.Logf)
|
|
actx := &ActionContext{Vars: map[string]string{}}
|
|
if err := cm.Setup(context.Background(), actx); err != nil {
|
|
t.Fatalf("setup: %v", err)
|
|
}
|
|
if cm.State().Mode != ClusterModeNone {
|
|
t.Fatalf("mode: got %s, want none", cm.State().Mode)
|
|
}
|
|
cm.Teardown(context.Background()) // no-op, no panic
|
|
}
|
|
|
|
func TestClusterManager_Fallback_Fail(t *testing.T) {
|
|
spec := &ClusterSpec{
|
|
Require: ClusterRequire{Servers: 1},
|
|
Fallback: "fail",
|
|
}
|
|
cm := NewClusterManager(spec, t.Logf)
|
|
actx := &ActionContext{
|
|
Scenario: &Scenario{Env: map[string]string{"master_url": "http://127.0.0.1:1"}},
|
|
Vars: map[string]string{},
|
|
Nodes: map[string]NodeRunner{},
|
|
}
|
|
err := cm.Setup(context.Background(), actx)
|
|
if err == nil {
|
|
t.Fatal("expected error for fallback=fail with no cluster")
|
|
}
|
|
if !strings.Contains(err.Error(), "fallback=fail") {
|
|
t.Fatalf("error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestClusterManager_Fallback_Skip(t *testing.T) {
|
|
spec := &ClusterSpec{
|
|
Require: ClusterRequire{Servers: 1},
|
|
Fallback: "skip",
|
|
}
|
|
cm := NewClusterManager(spec, t.Logf)
|
|
actx := &ActionContext{
|
|
Scenario: &Scenario{Env: map[string]string{"master_url": "http://127.0.0.1:1"}},
|
|
Vars: map[string]string{},
|
|
Nodes: map[string]NodeRunner{},
|
|
}
|
|
err := cm.Setup(context.Background(), actx)
|
|
if err != nil {
|
|
t.Fatalf("skip should not error: %v", err)
|
|
}
|
|
if !cm.Skipped() {
|
|
t.Fatal("expected Skipped()=true")
|
|
}
|
|
}
|
|
|
|
func TestClusterManager_SetVars(t *testing.T) {
|
|
cm := &ClusterManager{
|
|
logFunc: t.Logf,
|
|
state: ClusterState{
|
|
Mode: ClusterModeManaged,
|
|
MasterURL: "http://1.2.3.4:9333",
|
|
Servers: 2,
|
|
BlockCap: 1,
|
|
},
|
|
}
|
|
actx := &ActionContext{Vars: map[string]string{}}
|
|
cm.setVars(actx)
|
|
if actx.Vars["master_url"] != "http://1.2.3.4:9333" {
|
|
t.Fatalf("master_url: got %q", actx.Vars["master_url"])
|
|
}
|
|
if actx.Vars["cluster_mode"] != "managed" {
|
|
t.Fatalf("cluster_mode: got %q", actx.Vars["cluster_mode"])
|
|
}
|
|
if actx.Vars["cluster_servers"] != "2" {
|
|
t.Fatalf("cluster_servers: got %q", actx.Vars["cluster_servers"])
|
|
}
|
|
if actx.Vars["cluster_block_capable"] != "1" {
|
|
t.Fatalf("cluster_block_capable: got %q", actx.Vars["cluster_block_capable"])
|
|
}
|
|
}
|
|
|
|
func TestClusterManager_Teardown_AutoManaged_Kills(t *testing.T) {
|
|
node := &mockNode{}
|
|
cm := &ClusterManager{
|
|
spec: &ClusterSpec{Cleanup: "auto"},
|
|
logFunc: t.Logf,
|
|
node: node,
|
|
state: ClusterState{
|
|
Mode: ClusterModeManaged,
|
|
Pids: []string{"111", "222"},
|
|
Dirs: []string{"/tmp/test-master", "/tmp/test-vs"},
|
|
},
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
cm.Teardown(ctx)
|
|
|
|
if !node.hasCommand("kill -9 111") {
|
|
t.Fatal("expected kill for PID 111")
|
|
}
|
|
if !node.hasCommand("kill -9 222") {
|
|
t.Fatal("expected kill for PID 222")
|
|
}
|
|
if !node.hasCommand("rm -rf /tmp/test-master") {
|
|
t.Fatal("expected rm for master dir")
|
|
}
|
|
if !node.hasCommand("rm -rf /tmp/test-vs") {
|
|
t.Fatal("expected rm for vs dir")
|
|
}
|
|
}
|
|
|
|
func TestClusterManager_Teardown_AutoAttached_NoKill(t *testing.T) {
|
|
node := &mockNode{}
|
|
cm := &ClusterManager{
|
|
spec: &ClusterSpec{Cleanup: "auto"},
|
|
logFunc: t.Logf,
|
|
state: ClusterState{Mode: ClusterModeAttached},
|
|
attachedNodes: []NodeRunner{node},
|
|
}
|
|
cm.Teardown(context.Background())
|
|
if node.hasCommand("kill") {
|
|
t.Fatal("auto cleanup should NOT kill attached cluster")
|
|
}
|
|
}
|
|
|
|
func TestClusterManager_Teardown_DestroyAttached_Kills(t *testing.T) {
|
|
node := &mockNode{}
|
|
cm := &ClusterManager{
|
|
spec: &ClusterSpec{Cleanup: "destroy"},
|
|
logFunc: t.Logf,
|
|
state: ClusterState{Mode: ClusterModeAttached},
|
|
attachedNodes: []NodeRunner{node},
|
|
}
|
|
cm.Teardown(context.Background())
|
|
if !node.hasCommand("killall -9 weed") {
|
|
t.Fatal("destroy cleanup should kill attached cluster processes")
|
|
}
|
|
}
|
|
|
|
func TestClusterManager_Teardown_Keep_NoAction(t *testing.T) {
|
|
node := &mockNode{}
|
|
cm := &ClusterManager{
|
|
spec: &ClusterSpec{Cleanup: "keep"},
|
|
logFunc: t.Logf,
|
|
node: node,
|
|
state: ClusterState{
|
|
Mode: ClusterModeManaged,
|
|
Pids: []string{"111"},
|
|
},
|
|
}
|
|
cm.Teardown(context.Background())
|
|
if node.hasCommand("kill") {
|
|
t.Fatal("keep cleanup should NOT kill anything")
|
|
}
|
|
}
|
|
|
|
func TestClusterManager_MeetsRequirements(t *testing.T) {
|
|
cm := &ClusterManager{
|
|
spec: &ClusterSpec{
|
|
Require: ClusterRequire{Servers: 2, BlockCapable: 1},
|
|
},
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
state ClusterState
|
|
expect bool
|
|
}{
|
|
{"meets both", ClusterState{Servers: 3, BlockCap: 2}, true},
|
|
{"meets exact", ClusterState{Servers: 2, BlockCap: 1}, true},
|
|
{"servers short", ClusterState{Servers: 1, BlockCap: 1}, false},
|
|
{"block short", ClusterState{Servers: 3, BlockCap: 0}, false},
|
|
{"both short", ClusterState{Servers: 0, BlockCap: 0}, false},
|
|
}
|
|
for _, tt := range tests {
|
|
if got := cm.meetsRequirements(tt.state); got != tt.expect {
|
|
t.Errorf("%s: got %v, want %v", tt.name, got, tt.expect)
|
|
}
|
|
}
|
|
}
|