mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-01 21:59:44 +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>
327 lines
10 KiB
Go
327 lines
10 KiB
Go
package actions
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
tr "github.com/seaweedfs/seaweedfs/weed/storage/blockvol/testrunner"
|
|
)
|
|
|
|
// RegisterDatabaseActions registers SQLite and PostgreSQL database actions.
|
|
func RegisterDatabaseActions(r *tr.Registry) {
|
|
r.RegisterFunc("sqlite_create_db", tr.TierBlock, sqliteCreateDB)
|
|
r.RegisterFunc("sqlite_insert_rows", tr.TierBlock, sqliteInsertRows)
|
|
r.RegisterFunc("sqlite_count_rows", tr.TierBlock, sqliteCountRows)
|
|
r.RegisterFunc("sqlite_integrity_check", tr.TierBlock, sqliteIntegrityCheck)
|
|
r.RegisterFunc("pgbench_init", tr.TierBlock, pgbenchInit)
|
|
r.RegisterFunc("pgbench_run", tr.TierBlock, pgbenchRun)
|
|
r.RegisterFunc("pgbench_cleanup", tr.TierBlock, pgbenchCleanup)
|
|
}
|
|
|
|
// sqliteCreateDB creates a SQLite database with WAL mode and a test table.
|
|
// Params: path (required), table (default: "rows")
|
|
func sqliteCreateDB(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
path := act.Params["path"]
|
|
if path == "" {
|
|
return nil, fmt.Errorf("sqlite_create_db: path param required")
|
|
}
|
|
table := act.Params["table"]
|
|
if table == "" {
|
|
table = "rows"
|
|
}
|
|
|
|
node, err := GetNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sql := fmt.Sprintf("PRAGMA journal_mode=WAL; CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY, data TEXT, ts DATETIME DEFAULT CURRENT_TIMESTAMP);", table)
|
|
cmd := fmt.Sprintf("sqlite3 %s %q", path, sql)
|
|
_, stderr, code, err := node.RunRoot(ctx, cmd)
|
|
if err != nil || code != 0 {
|
|
return nil, fmt.Errorf("sqlite_create_db: code=%d stderr=%s err=%v", code, stderr, err)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// sqliteInsertRows inserts rows into a SQLite database.
|
|
// Params: path (required), count (default: "100"), table (default: "rows")
|
|
func sqliteInsertRows(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
path := act.Params["path"]
|
|
if path == "" {
|
|
return nil, fmt.Errorf("sqlite_insert_rows: path param required")
|
|
}
|
|
count := act.Params["count"]
|
|
if count == "" {
|
|
count = "100"
|
|
}
|
|
table := act.Params["table"]
|
|
if table == "" {
|
|
table = "rows"
|
|
}
|
|
|
|
node, err := GetNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Generate SQL in a temp file with BEGIN/COMMIT, then pipe to sqlite3.
|
|
// Use bash -c with \x27 for single quotes to avoid quoting issues with sudo.
|
|
tmpFile := tempPath(actx, "sqlite_insert.sql")
|
|
cmd := fmt.Sprintf(
|
|
`bash -c 'mkdir -p %s; printf "BEGIN;\n" > %s; for i in $(seq 1 %s); do printf "INSERT INTO %s (data) VALUES (\x27row-%%d\x27);\n" $i; done >> %s; printf "COMMIT;\n" >> %s; sqlite3 %s < %s; rm -f %s'`,
|
|
actx.TempRoot, tmpFile, count, table, tmpFile, tmpFile, path, tmpFile, tmpFile)
|
|
_, stderr, code, err := node.RunRoot(ctx, cmd)
|
|
if err != nil || code != 0 {
|
|
return nil, fmt.Errorf("sqlite_insert_rows: code=%d stderr=%s err=%v", code, stderr, err)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// sqliteCountRows returns the row count from a SQLite table.
|
|
// Params: path (required), table (default: "rows")
|
|
func sqliteCountRows(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
path := act.Params["path"]
|
|
if path == "" {
|
|
return nil, fmt.Errorf("sqlite_count_rows: path param required")
|
|
}
|
|
table := act.Params["table"]
|
|
if table == "" {
|
|
table = "rows"
|
|
}
|
|
|
|
node, err := GetNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cmd := fmt.Sprintf("sqlite3 %s \"SELECT COUNT(*) FROM %s;\"", path, table)
|
|
stdout, stderr, code, err := node.RunRoot(ctx, cmd)
|
|
if err != nil || code != 0 {
|
|
return nil, fmt.Errorf("sqlite_count_rows: code=%d stderr=%s err=%v", code, stderr, err)
|
|
}
|
|
|
|
return map[string]string{"value": strings.TrimSpace(stdout)}, nil
|
|
}
|
|
|
|
// sqliteIntegrityCheck runs PRAGMA integrity_check and fails if result != "ok".
|
|
// Params: path (required)
|
|
func sqliteIntegrityCheck(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
path := act.Params["path"]
|
|
if path == "" {
|
|
return nil, fmt.Errorf("sqlite_integrity_check: path param required")
|
|
}
|
|
|
|
node, err := GetNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cmd := fmt.Sprintf("sqlite3 %s \"PRAGMA integrity_check;\"", path)
|
|
stdout, stderr, code, err := node.RunRoot(ctx, cmd)
|
|
if err != nil || code != 0 {
|
|
return nil, fmt.Errorf("sqlite_integrity_check: code=%d stderr=%s err=%v", code, stderr, err)
|
|
}
|
|
|
|
result := strings.TrimSpace(stdout)
|
|
if result != "ok" {
|
|
return nil, fmt.Errorf("sqlite_integrity_check: result=%q (expected 'ok')", result)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// pgbenchInit initializes a PostgreSQL instance on a block device for benchmarking.
|
|
// Params:
|
|
// - device (required): block device to format and mount
|
|
// - mount (default: "/mnt/pgbench"): mount point
|
|
// - port (default: "5434"): PostgreSQL port
|
|
// - scale (default: "10"): pgbench scale factor
|
|
// - fstype (default: "ext4"): filesystem type
|
|
// - pg_bin (default: "/usr/lib/postgresql/16/bin"): PostgreSQL binary directory
|
|
//
|
|
// Returns: value = "ready"
|
|
func pgbenchInit(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
device := act.Params["device"]
|
|
if device == "" {
|
|
return nil, fmt.Errorf("pgbench_init: device param required")
|
|
}
|
|
|
|
mount := paramDefault(act.Params, "mount", "/mnt/pgbench")
|
|
port := paramDefault(act.Params, "port", "5434")
|
|
scale := paramDefault(act.Params, "scale", "10")
|
|
fstype := paramDefault(act.Params, "fstype", "ext4")
|
|
pgBin := paramDefault(act.Params, "pg_bin", "/usr/lib/postgresql/16/bin")
|
|
|
|
node, err := GetNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pgdata := mount + "/pgdata"
|
|
|
|
// Format, mount, init PostgreSQL, start, create bench DB, run pgbench -i.
|
|
script := fmt.Sprintf(`set -e
|
|
# Stop any previous instance
|
|
sudo -u postgres %s/pg_ctl -D %s stop 2>/dev/null || true
|
|
sleep 1
|
|
# Format and mount
|
|
mkfs.%s -F %s > /dev/null 2>&1
|
|
mkdir -p %s
|
|
mount %s %s
|
|
# Init PostgreSQL
|
|
mkdir -p %s
|
|
chown postgres:postgres %s
|
|
sudo -u postgres %s/initdb -D %s > /dev/null 2>&1
|
|
echo "listen_addresses = '127.0.0.1'" >> %s/postgresql.conf
|
|
echo "port = %s" >> %s/postgresql.conf
|
|
echo "unix_socket_directories = '/tmp'" >> %s/postgresql.conf
|
|
echo "shared_buffers = 256MB" >> %s/postgresql.conf
|
|
echo "effective_cache_size = 512MB" >> %s/postgresql.conf
|
|
echo "work_mem = 4MB" >> %s/postgresql.conf
|
|
echo "wal_buffers = 16MB" >> %s/postgresql.conf
|
|
echo "max_connections = 200" >> %s/postgresql.conf
|
|
chown -R postgres:postgres %s
|
|
# Start
|
|
sudo -u postgres %s/pg_ctl -D %s -l %s/logfile start
|
|
sleep 3
|
|
# Create DB and init pgbench
|
|
sudo -u postgres %s/createdb -h /tmp -p %s benchdb 2>/dev/null || true
|
|
sudo -u postgres pgbench -h /tmp -i -s %s -p %s benchdb 2>&1 | tail -3
|
|
echo PGBENCH_INIT_OK`,
|
|
pgBin, pgdata,
|
|
fstype, device,
|
|
mount,
|
|
device, mount,
|
|
pgdata,
|
|
pgdata,
|
|
pgBin, pgdata,
|
|
pgdata, port, pgdata, pgdata,
|
|
pgdata, pgdata, pgdata, pgdata, pgdata,
|
|
pgdata,
|
|
pgBin, pgdata, pgdata,
|
|
pgBin, port,
|
|
scale, port,
|
|
)
|
|
|
|
actx.Log(" pgbench_init: %s on %s port=%s scale=%s", fstype, device, port, scale)
|
|
stdout, stderr, code, err := node.RunRoot(ctx, fmt.Sprintf("bash -c '%s'", strings.ReplaceAll(script, "'", "'\\''")))
|
|
if err != nil || code != 0 {
|
|
return nil, fmt.Errorf("pgbench_init: code=%d stderr=%s err=%v stdout=%s", code, stderr, err, stdout)
|
|
}
|
|
if !strings.Contains(stdout, "PGBENCH_INIT_OK") {
|
|
return nil, fmt.Errorf("pgbench_init: init did not complete: %s", stdout)
|
|
}
|
|
|
|
// Save state for pgbench_run and pgbench_cleanup.
|
|
actx.Vars["__pgbench_mount"] = mount
|
|
actx.Vars["__pgbench_port"] = port
|
|
actx.Vars["__pgbench_pgbin"] = pgBin
|
|
actx.Vars["__pgbench_pgdata"] = pgdata
|
|
|
|
return map[string]string{"value": "ready"}, nil
|
|
}
|
|
|
|
// pgbenchRun executes a pgbench workload and returns the TPS.
|
|
// Params:
|
|
// - clients (default: "1"): number of concurrent clients
|
|
// - duration (default: "30"): run time in seconds
|
|
// - select_only (default: "false"): if "true", run SELECT-only workload (-S)
|
|
// - port: override port (default: uses __pgbench_port from pgbench_init)
|
|
//
|
|
// Returns: value = TPS (numeric string, e.g. "1234.56")
|
|
func pgbenchRun(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
port := act.Params["port"]
|
|
if port == "" {
|
|
port = actx.Vars["__pgbench_port"]
|
|
}
|
|
if port == "" {
|
|
port = "5434"
|
|
}
|
|
|
|
clients := paramDefault(act.Params, "clients", "1")
|
|
duration := paramDefault(act.Params, "duration", "30")
|
|
selectOnly := act.Params["select_only"] == "true"
|
|
|
|
node, err := GetNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cmd := fmt.Sprintf("sudo -u postgres pgbench -h /tmp -c %s -j %s -T %s -p %s",
|
|
clients, clients, duration, port)
|
|
if selectOnly {
|
|
cmd += " -S"
|
|
}
|
|
cmd += " benchdb"
|
|
|
|
mode := "TPC-B"
|
|
if selectOnly {
|
|
mode = "SELECT-only"
|
|
}
|
|
actx.Log(" pgbench %s c=%s %ss", mode, clients, duration)
|
|
stdout, stderr, code, err := node.RunRoot(ctx, cmd)
|
|
if err != nil || code != 0 {
|
|
return nil, fmt.Errorf("pgbench_run: code=%d stderr=%s stdout=%s err=%v", code, stderr, stdout, err)
|
|
}
|
|
|
|
// Parse TPS from pgbench output. Look for "tps = NNNN.NN" (excluding initial connection).
|
|
tps := parsePgbenchTPS(stdout)
|
|
if tps == "" {
|
|
return nil, fmt.Errorf("pgbench_run: could not parse TPS from output: %s", stdout)
|
|
}
|
|
|
|
actx.Log(" pgbench %s c=%s: %s TPS", mode, clients, tps)
|
|
return map[string]string{"value": tps}, nil
|
|
}
|
|
|
|
// pgbenchCleanup stops PostgreSQL and unmounts the device.
|
|
// Uses state saved by pgbench_init (__pgbench_mount, __pgbench_pgbin, __pgbench_pgdata).
|
|
func pgbenchCleanup(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
mount := actx.Vars["__pgbench_mount"]
|
|
pgBin := actx.Vars["__pgbench_pgbin"]
|
|
pgdata := actx.Vars["__pgbench_pgdata"]
|
|
|
|
if mount == "" {
|
|
mount = "/mnt/pgbench"
|
|
}
|
|
if pgBin == "" {
|
|
pgBin = "/usr/lib/postgresql/16/bin"
|
|
}
|
|
if pgdata == "" {
|
|
pgdata = mount + "/pgdata"
|
|
}
|
|
|
|
node, err := GetNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cmd := fmt.Sprintf("sudo -u postgres %s/pg_ctl -D %s stop 2>/dev/null; sleep 1; umount %s 2>/dev/null; true",
|
|
pgBin, pgdata, mount)
|
|
node.RunRoot(ctx, cmd)
|
|
return nil, nil
|
|
}
|
|
|
|
// parsePgbenchTPS extracts TPS from pgbench output.
|
|
// Matches "tps = 1234.567890" (excluding "initial connection time" lines).
|
|
var pgbenchTPSPattern = regexp.MustCompile(`tps = ([\d.]+)\s+\(`)
|
|
|
|
func parsePgbenchTPS(output string) string {
|
|
lines := strings.Split(output, "\n")
|
|
for _, line := range lines {
|
|
// Skip "initial connection time = X.XX ms" lines (no TPS).
|
|
if strings.Contains(line, "initial connection time") && !strings.Contains(line, "tps") {
|
|
continue
|
|
}
|
|
if m := pgbenchTPSPattern.FindStringSubmatch(line); len(m) > 1 {
|
|
return m[1]
|
|
}
|
|
}
|
|
return ""
|
|
}
|