Files
seaweedfs/weed/storage/blockvol/testrunner/actions/cleanup.go
T
pingqiuandClaude Opus 4.6 785a7d7efd feat: wire real pinner into flusher retention + real WAL scan executor (Phase 07 P1)
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>
2026-03-30 20:01:46 -07:00

163 lines
5.7 KiB
Go

package actions
import (
"context"
"fmt"
"strings"
tr "github.com/seaweedfs/seaweedfs/weed/storage/blockvol/testrunner"
)
// RegisterCleanupActions registers environment cleanup and device discovery actions.
func RegisterCleanupActions(r *tr.Registry) {
r.RegisterFunc("pre_run_cleanup", tr.TierCore, preRunCleanup)
r.RegisterFunc("nvme_connect_direct", tr.TierBlock, nvmeConnectDirect)
r.RegisterFunc("nvme_disconnect_all", tr.TierBlock, nvmeDisconnectAll)
}
// preRunCleanup kills stale processes, unmounts filesystems, disconnects
// NVMe/iSCSI sessions, and verifies ports are free. Runs on a specified node.
//
// Params:
// - kill_patterns: comma-separated process names to kill (default: "weed,iscsi-target,postgres")
// - unmount: comma-separated mount points to unmount
// - nvme_disconnect: "true" to disconnect all NVMe sessions
// - iscsi_logout_prefix: IQN prefix to logout (e.g., "iqn.2024-01.com.seaweedfs")
// - check_ports: comma-separated ports that must be free after cleanup
//
// Always succeeds (ignore_error semantics built in) — logs warnings but doesn't fail the scenario.
func preRunCleanup(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
node, err := GetNode(actx, act.Node)
if err != nil {
return nil, fmt.Errorf("pre_run_cleanup: %w", err)
}
var cleaned []string
// Kill stale processes.
patterns := act.Params["kill_patterns"]
if patterns == "" {
patterns = "weed,iscsi-target,postgres"
}
for _, p := range strings.Split(patterns, ",") {
p = strings.TrimSpace(p)
if p == "" {
continue
}
node.RunRoot(ctx, fmt.Sprintf("pkill -9 %s 2>/dev/null || true", p))
cleaned = append(cleaned, "kill:"+p)
}
// Unmount filesystems.
if mounts := act.Params["unmount"]; mounts != "" {
for _, m := range strings.Split(mounts, ",") {
m = strings.TrimSpace(m)
if m == "" {
continue
}
node.RunRoot(ctx, fmt.Sprintf("umount -l %s 2>/dev/null || true", m))
cleaned = append(cleaned, "umount:"+m)
}
}
// Disconnect NVMe.
if act.Params["nvme_disconnect"] == "true" {
node.RunRoot(ctx, "nvme disconnect-all 2>/dev/null || true")
cleaned = append(cleaned, "nvme:disconnect-all")
}
// Logout iSCSI sessions.
if prefix := act.Params["iscsi_logout_prefix"]; prefix != "" {
node.RunRoot(ctx, fmt.Sprintf(
"iscsiadm -m session 2>/dev/null | grep '%s' | awk '{print $4}' | while read iqn; do "+
"iscsiadm -m node -T $iqn --logout 2>/dev/null; "+
"iscsiadm -m node -T $iqn -o delete 2>/dev/null; done || true", prefix))
cleaned = append(cleaned, "iscsi:"+prefix)
}
// Check ports are free.
if ports := act.Params["check_ports"]; ports != "" {
for _, p := range strings.Split(ports, ",") {
p = strings.TrimSpace(p)
stdout, _, _, _ := node.RunRoot(ctx, fmt.Sprintf("ss -tlnp | grep ':%s ' | head -1", p))
if strings.TrimSpace(stdout) != "" {
actx.Log(" WARNING: port %s still in use after cleanup: %s", p, strings.TrimSpace(stdout))
}
}
}
actx.Log(" cleanup: %s", strings.Join(cleaned, ", "))
return map[string]string{"value": strings.Join(cleaned, ",")}, nil
}
// nvmeConnect connects to an NVMe-oF target and returns the discovered device path.
// Handles modprobe, disconnect stale sessions, connect, and device discovery.
//
// Params:
// - target_addr: NVMe target IP (required)
// - target_port: NVMe target port (default: "4420")
// - nqn: NVMe subsystem NQN (required)
// - transport: "tcp" or "rdma" (default: "tcp")
// - expected_size: expected device size for discovery (e.g., "2G") (optional)
//
// Returns: value = device path (e.g., "/dev/nvme1n1")
func nvmeConnectDirect(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
node, err := GetNode(actx, act.Node)
if err != nil {
return nil, fmt.Errorf("nvme_connect: %w", err)
}
addr := act.Params["target_addr"]
if addr == "" {
return nil, fmt.Errorf("nvme_connect: target_addr required")
}
port := paramDefault(act.Params, "target_port", "4420")
nqn := act.Params["nqn"]
if nqn == "" {
return nil, fmt.Errorf("nvme_connect: nqn required")
}
transport := paramDefault(act.Params, "transport", "tcp")
// Ensure NVMe-TCP kernel module is loaded.
node.RunRoot(ctx, fmt.Sprintf("modprobe nvme_%s 2>/dev/null || true", transport))
// Connect.
cmd := fmt.Sprintf("nvme connect -t %s -a %s -s %s -n %s 2>&1", transport, addr, port, nqn)
stdout, stderr, code, err := node.RunRoot(ctx, cmd)
if err != nil || code != 0 {
return nil, fmt.Errorf("nvme_connect: code=%d stdout=%s stderr=%s err=%v", code, stdout, stderr, err)
}
// Wait for device to appear.
node.Run(ctx, "sleep 2")
// Discover the device. Strategy: find NVMe namespace matching expected size.
expectedSize := act.Params["expected_size"]
var devCmd string
if expectedSize != "" {
devCmd = fmt.Sprintf("lsblk -dpno NAME,SIZE | grep '%s' | head -1 | awk '{print $1}'", expectedSize)
} else {
// Fall back to newest NVMe device (not nvme0 which is the boot disk).
devCmd = "lsblk -dpno NAME | grep nvme | grep -v nvme0 | tail -1"
}
devOut, _, _, _ := node.RunRoot(ctx, devCmd)
device := strings.TrimSpace(devOut)
if device == "" {
return nil, fmt.Errorf("nvme_connect: connected but no device found (expected_size=%s)", expectedSize)
}
actx.Log(" nvme connected: %s → %s", nqn, device)
return map[string]string{"value": device}, nil
}
// nvmeDisconnectAll disconnects all NVMe-oF sessions on the node.
func nvmeDisconnectAll(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
node, err := GetNode(actx, act.Node)
if err != nil {
return nil, fmt.Errorf("nvme_disconnect_all: %w", err)
}
node.RunRoot(ctx, "nvme disconnect-all 2>/dev/null || true")
return nil, nil
}