mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-05 22:12:04 +02: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>
355 lines
10 KiB
Go
355 lines
10 KiB
Go
package actions
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
tr "github.com/seaweedfs/seaweedfs/weed/storage/blockvol/testrunner"
|
|
)
|
|
|
|
// RegisterIOActions registers IO-related actions.
|
|
func RegisterIOActions(r *tr.Registry) {
|
|
r.RegisterFunc("dd_write", tr.TierBlock, ddWrite)
|
|
r.RegisterFunc("dd_read_md5", tr.TierBlock, ddReadMD5)
|
|
r.RegisterFunc("fio", tr.TierBlock, fioAction)
|
|
r.RegisterFunc("fio_verify", tr.TierBlock, fioVerify)
|
|
r.RegisterFunc("mkfs", tr.TierBlock, mkfsAction)
|
|
r.RegisterFunc("mount", tr.TierBlock, mountAction)
|
|
r.RegisterFunc("umount", tr.TierBlock, umountAction)
|
|
r.RegisterFunc("write_loop_bg", tr.TierBlock, writeLoopBg)
|
|
r.RegisterFunc("stop_bg", tr.TierBlock, stopBg)
|
|
}
|
|
|
|
// ddWrite writes random data using dd, returns the md5 checksum.
|
|
func ddWrite(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
device := act.Params["device"]
|
|
if device == "" {
|
|
return nil, fmt.Errorf("dd_write: device param required")
|
|
}
|
|
bs := act.Params["bs"]
|
|
if bs == "" {
|
|
bs = "1M"
|
|
}
|
|
count := act.Params["count"]
|
|
if count == "" {
|
|
count = "1"
|
|
}
|
|
oflag := act.Params["oflag"]
|
|
if oflag == "" {
|
|
oflag = "direct"
|
|
}
|
|
|
|
node, err := GetNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Generate random data to temp file, write to device, compute md5.
|
|
tmpFile := tempPath(actx, "dd-data")
|
|
if err := ensureTempRoot(ctx, node, actx); err != nil {
|
|
return nil, fmt.Errorf("dd_write: %w", err)
|
|
}
|
|
genCmd := fmt.Sprintf("dd if=/dev/urandom of=%s bs=%s count=%s 2>/dev/null", tmpFile, bs, count)
|
|
_, stderr, code, err := node.RunRoot(ctx, genCmd)
|
|
if err != nil || code != 0 {
|
|
return nil, fmt.Errorf("dd_write gen: code=%d stderr=%s err=%v", code, stderr, err)
|
|
}
|
|
|
|
writeCmd := fmt.Sprintf("dd if=%s of=%s bs=%s oflag=%s conv=fsync", tmpFile, device, bs, oflag)
|
|
if seek := act.Params["seek"]; seek != "" {
|
|
writeCmd += fmt.Sprintf(" seek=%s", seek)
|
|
}
|
|
writeCmd += " 2>/dev/null"
|
|
_, stderr, code, err = node.RunRoot(ctx, writeCmd)
|
|
if err != nil || code != 0 {
|
|
return nil, fmt.Errorf("dd_write: code=%d stderr=%s err=%v", code, stderr, err)
|
|
}
|
|
|
|
md5Cmd := fmt.Sprintf("md5sum %s | cut -d' ' -f1", tmpFile)
|
|
stdout, stderr, code, err := node.RunRoot(ctx, md5Cmd)
|
|
if err != nil || code != 0 {
|
|
return nil, fmt.Errorf("dd_write md5: code=%d stderr=%s err=%v", code, stderr, err)
|
|
}
|
|
node.Run(ctx, fmt.Sprintf("rm -f %s", tmpFile))
|
|
|
|
md5 := strings.TrimSpace(stdout)
|
|
if md5 == "" {
|
|
return nil, fmt.Errorf("dd_write: empty md5")
|
|
}
|
|
|
|
return map[string]string{"value": md5}, nil
|
|
}
|
|
|
|
// ddReadMD5 reads from device using dd and returns md5.
|
|
func ddReadMD5(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
device := act.Params["device"]
|
|
if device == "" {
|
|
return nil, fmt.Errorf("dd_read_md5: device param required")
|
|
}
|
|
bs := act.Params["bs"]
|
|
if bs == "" {
|
|
bs = "1M"
|
|
}
|
|
count := act.Params["count"]
|
|
if count == "" {
|
|
count = "1"
|
|
}
|
|
|
|
node, err := GetNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tmpFile := tempPath(actx, "dd-read")
|
|
if err := ensureTempRoot(ctx, node, actx); err != nil {
|
|
return nil, fmt.Errorf("dd_read_md5: %w", err)
|
|
}
|
|
readCmd := fmt.Sprintf("dd if=%s of=%s bs=%s count=%s iflag=direct", device, tmpFile, bs, count)
|
|
if skip := act.Params["skip"]; skip != "" {
|
|
readCmd += fmt.Sprintf(" skip=%s", skip)
|
|
}
|
|
readCmd += " 2>/dev/null"
|
|
_, stderr, code, err := node.RunRoot(ctx, readCmd)
|
|
if err != nil || code != 0 {
|
|
return nil, fmt.Errorf("dd_read_md5 read: code=%d stderr=%s err=%v", code, stderr, err)
|
|
}
|
|
|
|
cmd := fmt.Sprintf("md5sum %s | cut -d' ' -f1", tmpFile)
|
|
stdout, stderr, code, err := node.RunRoot(ctx, cmd)
|
|
if err != nil || code != 0 {
|
|
return nil, fmt.Errorf("dd_read_md5: code=%d stderr=%s err=%v", code, stderr, err)
|
|
}
|
|
node.Run(ctx, fmt.Sprintf("rm -f %s", tmpFile))
|
|
|
|
md5 := strings.TrimSpace(stdout)
|
|
if md5 == "" {
|
|
return nil, fmt.Errorf("dd_read_md5: empty md5")
|
|
}
|
|
|
|
return map[string]string{"value": md5}, nil
|
|
}
|
|
|
|
func fioAction(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
device := act.Params["device"]
|
|
if device == "" {
|
|
return nil, fmt.Errorf("fio: device param required")
|
|
}
|
|
|
|
node, err := GetNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rw := act.Params["rw"]
|
|
if rw == "" {
|
|
rw = "randwrite"
|
|
}
|
|
bs := act.Params["bs"]
|
|
if bs == "" {
|
|
bs = "4k"
|
|
}
|
|
iodepth := act.Params["iodepth"]
|
|
if iodepth == "" {
|
|
iodepth = "32"
|
|
}
|
|
runtime := act.Params["runtime"]
|
|
if runtime == "" {
|
|
runtime = "10"
|
|
}
|
|
name := act.Params["name"]
|
|
if name == "" {
|
|
name = "fio_test"
|
|
}
|
|
|
|
cmd := fmt.Sprintf("fio --name=%s --filename=%s --rw=%s --bs=%s --iodepth=%s --direct=1 --runtime=%s --time_based --output-format=json",
|
|
name, device, rw, bs, iodepth, runtime)
|
|
if size := act.Params["size"]; size != "" {
|
|
cmd += fmt.Sprintf(" --size=%s", size)
|
|
}
|
|
stdout, stderr, code, err := node.RunRoot(ctx, cmd)
|
|
if err != nil || code != 0 {
|
|
return nil, fmt.Errorf("fio: code=%d stderr=%s err=%v", code, stderr, err)
|
|
}
|
|
|
|
return map[string]string{"value": stdout}, nil
|
|
}
|
|
|
|
func fioVerify(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
device := act.Params["device"]
|
|
if device == "" {
|
|
return nil, fmt.Errorf("fio_verify: device param required")
|
|
}
|
|
|
|
node, err := GetNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bs := act.Params["bs"]
|
|
if bs == "" {
|
|
bs = "4k"
|
|
}
|
|
size := act.Params["size"]
|
|
if size == "" {
|
|
size = "10M"
|
|
}
|
|
|
|
// Write with verify pattern, then read+verify.
|
|
cmd := fmt.Sprintf("fio --name=verify --filename=%s --rw=write --bs=%s --size=%s --direct=1 --verify=crc32c --do_verify=1 --output-format=json",
|
|
device, bs, size)
|
|
stdout, stderr, code, err := node.RunRoot(ctx, cmd)
|
|
if err != nil || code != 0 {
|
|
return nil, fmt.Errorf("fio_verify: code=%d stderr=%s err=%v", code, stderr, err)
|
|
}
|
|
|
|
return map[string]string{"value": stdout}, nil
|
|
}
|
|
|
|
func mkfsAction(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
device := act.Params["device"]
|
|
if device == "" {
|
|
return nil, fmt.Errorf("mkfs: device param required")
|
|
}
|
|
fstype := act.Params["fstype"]
|
|
if fstype == "" {
|
|
fstype = "ext4"
|
|
}
|
|
|
|
node, err := GetNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cmd := fmt.Sprintf("mkfs.%s -F %s", fstype, device)
|
|
_, stderr, code, err := node.RunRoot(ctx, cmd)
|
|
if err != nil || code != 0 {
|
|
return nil, fmt.Errorf("mkfs: code=%d stderr=%s err=%v", code, stderr, err)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func mountAction(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
device := act.Params["device"]
|
|
if device == "" {
|
|
return nil, fmt.Errorf("mount: device param required")
|
|
}
|
|
mountpoint := act.Params["mountpoint"]
|
|
if mountpoint == "" {
|
|
mountpoint = "/mnt/test"
|
|
}
|
|
|
|
node, err := GetNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
node.RunRoot(ctx, fmt.Sprintf("mkdir -p %s", mountpoint))
|
|
_, stderr, code, err := node.RunRoot(ctx, fmt.Sprintf("mount %s %s", device, mountpoint))
|
|
if err != nil || code != 0 {
|
|
return nil, fmt.Errorf("mount: code=%d stderr=%s err=%v", code, stderr, err)
|
|
}
|
|
return map[string]string{"value": mountpoint}, nil
|
|
}
|
|
|
|
func umountAction(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
mountpoint := act.Params["mountpoint"]
|
|
if mountpoint == "" {
|
|
mountpoint = "/mnt/test"
|
|
}
|
|
|
|
node, err := GetNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, stderr, code, err := node.RunRoot(ctx, fmt.Sprintf("umount %s", mountpoint))
|
|
if err != nil || code != 0 {
|
|
return nil, fmt.Errorf("umount: code=%d stderr=%s err=%v", code, stderr, err)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// writeLoopBg starts a background dd write loop. Returns PID.
|
|
// Params: device (required), bs (default: "4k"), oflag (default: "direct")
|
|
func writeLoopBg(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
device := act.Params["device"]
|
|
if device == "" {
|
|
return nil, fmt.Errorf("write_loop_bg: device param required")
|
|
}
|
|
bs := act.Params["bs"]
|
|
if bs == "" {
|
|
bs = "4k"
|
|
}
|
|
oflag := act.Params["oflag"]
|
|
if oflag == "" {
|
|
oflag = "direct"
|
|
}
|
|
|
|
node, err := GetNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bgLog := tempPath(actx, "bg.log")
|
|
if err := ensureTempRoot(ctx, node, actx); err != nil {
|
|
return nil, fmt.Errorf("write_loop_bg: %w", err)
|
|
}
|
|
cmd := fmt.Sprintf("setsid bash -c 'while true; do dd if=/dev/urandom of=%s bs=%s count=1 oflag=%s conv=notrunc 2>/dev/null; done' &>%s & echo $!",
|
|
device, bs, oflag, bgLog)
|
|
stdout, stderr, code, err := node.RunRoot(ctx, cmd)
|
|
if err != nil || code != 0 {
|
|
return nil, fmt.Errorf("write_loop_bg: code=%d stderr=%s err=%v", code, stderr, err)
|
|
}
|
|
|
|
pid := strings.TrimSpace(stdout)
|
|
if pid == "" {
|
|
return nil, fmt.Errorf("write_loop_bg: empty PID")
|
|
}
|
|
|
|
return map[string]string{"value": pid}, nil
|
|
}
|
|
|
|
// stopBg kills a background process by PID.
|
|
// Params: pid (required)
|
|
func stopBg(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
pid := act.Params["pid"]
|
|
if pid == "" {
|
|
return nil, fmt.Errorf("stop_bg: pid param required")
|
|
}
|
|
|
|
node, err := GetNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cmd := fmt.Sprintf("kill %s; wait %s 2>/dev/null || true", pid, pid)
|
|
node.RunRoot(ctx, cmd)
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// ensureTempRoot creates the per-run temp directory on the remote node.
|
|
// Uses RunRoot so the directory is created with root privileges, ensuring
|
|
// subsequent RunRoot commands can write into it.
|
|
func ensureTempRoot(ctx context.Context, node interface{ RunRoot(context.Context, string) (string, string, int, error) }, actx *tr.ActionContext) error {
|
|
if actx.TempRoot == "" {
|
|
return nil
|
|
}
|
|
_, stderr, code, err := node.RunRoot(ctx, fmt.Sprintf("mkdir -p %s", actx.TempRoot))
|
|
if err != nil || code != 0 {
|
|
return fmt.Errorf("mkdir TempRoot %s: code=%d stderr=%s err=%v", actx.TempRoot, code, stderr, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// tempPath returns a path under the per-run temp root for the given suffix.
|
|
// Falls back to /tmp if TempRoot is empty (backward compat).
|
|
func tempPath(actx *tr.ActionContext, suffix string) string {
|
|
root := actx.TempRoot
|
|
if root == "" {
|
|
root = "/tmp"
|
|
}
|
|
return root + "/sw-" + suffix
|
|
}
|