Files
seaweedfs/weed/storage/blockvol/test/pg_helper.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

186 lines
5.6 KiB
Go

//go:build integration
package test
import (
"context"
"fmt"
"strconv"
"strings"
"time"
)
// pgHelper manages a Postgres instance lifecycle on a remote/WSL2 node.
type pgHelper struct {
node *Node
dev string // iSCSI block device (e.g. /dev/sdb)
mnt string // mount point
pgdata string // PGDATA directory
pgPort int // Postgres port (avoid conflicts)
}
// newPgHelper creates a pgHelper. dev must be a valid block device path.
func newPgHelper(node *Node, dev string, pgPort int) *pgHelper {
mnt := "/tmp/blockvol-pgcrash"
return &pgHelper{
node: node,
dev: dev,
mnt: mnt,
pgdata: mnt + "/pgdata",
pgPort: pgPort,
}
}
// InitFS formats the device and initializes Postgres.
func (p *pgHelper) InitFS(ctx context.Context) error {
// mkfs
_, stderr, code, _ := p.node.RunRoot(ctx, fmt.Sprintf("mkfs.ext4 -F %s", p.dev))
if code != 0 {
return fmt.Errorf("mkfs: code=%d stderr=%s", code, stderr)
}
// mount
if err := p.Mount(ctx); err != nil {
return err
}
// Prepare pgdata
p.node.RunRoot(ctx, fmt.Sprintf("chown postgres:postgres %s", p.mnt))
p.node.RunRoot(ctx, fmt.Sprintf("mkdir -p %s", p.pgdata))
p.node.RunRoot(ctx, fmt.Sprintf("chown postgres:postgres %s", p.pgdata))
p.node.RunRoot(ctx, fmt.Sprintf("chmod 700 %s", p.pgdata))
return p.InitDB(ctx)
}
// InitDB runs initdb in pgdata.
func (p *pgHelper) InitDB(ctx context.Context) error {
_, stderr, code, _ := p.node.RunRoot(ctx,
fmt.Sprintf("sudo -u postgres /usr/lib/postgresql/*/bin/initdb -D %s", p.pgdata))
if code != 0 {
return fmt.Errorf("initdb: code=%d stderr=%s", code, stderr)
}
return nil
}
// Start starts Postgres.
func (p *pgHelper) Start(ctx context.Context) error {
_, stderr, code, _ := p.node.RunRoot(ctx,
fmt.Sprintf("sudo -u postgres /usr/lib/postgresql/*/bin/pg_ctl -D %s -l %s/pg.log -o '-p %d' start",
p.pgdata, p.mnt, p.pgPort))
if code != 0 {
return fmt.Errorf("pg_ctl start: code=%d stderr=%s", code, stderr)
}
return nil
}
// Stop stops Postgres with fast shutdown.
func (p *pgHelper) Stop(ctx context.Context) error {
_, _, code, _ := p.node.RunRoot(ctx,
fmt.Sprintf("sudo -u postgres /usr/lib/postgresql/*/bin/pg_ctl -D %s stop -m fast 2>/dev/null", p.pgdata))
if code != 0 {
return fmt.Errorf("pg_ctl stop: code=%d", code)
}
return nil
}
// IsReady waits up to timeout for pg_isready to succeed.
func (p *pgHelper) IsReady(ctx context.Context, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
_, _, code, _ := p.node.RunRoot(ctx, fmt.Sprintf("pg_isready -p %d", p.pgPort))
if code == 0 {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
default:
time.Sleep(1 * time.Second)
}
}
return fmt.Errorf("pg_isready timeout after %v", timeout)
}
// PgBench runs pgbench for the given duration. Returns transaction count.
func (p *pgHelper) PgBench(ctx context.Context, seconds int) (int, error) {
stdout, stderr, code, _ := p.node.RunRoot(ctx,
fmt.Sprintf("sudo -u postgres pgbench -p %d -T %d pgbench", p.pgPort, seconds))
if code != 0 {
return 0, fmt.Errorf("pgbench: code=%d stderr=%s", code, stderr)
}
// Parse TPS from output
for _, line := range strings.Split(stdout, "\n") {
if strings.Contains(line, "number of transactions actually processed:") {
parts := strings.Split(line, ":")
if len(parts) >= 2 {
nStr := strings.TrimSpace(parts[1])
// Remove any non-numeric suffix
nStr = strings.Split(nStr, "/")[0]
nStr = strings.TrimSpace(nStr)
n, err := strconv.Atoi(nStr)
if err == nil {
return n, nil
}
}
}
}
return 0, nil // couldn't parse but pgbench succeeded
}
// PgBenchInit initializes pgbench tables.
func (p *pgHelper) PgBenchInit(ctx context.Context) error {
p.node.RunRoot(ctx, fmt.Sprintf(
"sudo -u postgres /usr/lib/postgresql/*/bin/createdb -p %d pgbench 2>/dev/null", p.pgPort))
_, stderr, code, _ := p.node.RunRoot(ctx,
fmt.Sprintf("sudo -u postgres pgbench -p %d -i pgbench", p.pgPort))
if code != 0 {
return fmt.Errorf("pgbench init: code=%d stderr=%s", code, stderr)
}
return nil
}
// CountHistory returns SELECT count(*) FROM pgbench_history.
func (p *pgHelper) CountHistory(ctx context.Context) (int, error) {
stdout, stderr, code, _ := p.node.RunRoot(ctx,
fmt.Sprintf("sudo -u postgres psql -p %d -t -c 'SELECT count(*) FROM pgbench_history' pgbench", p.pgPort))
if code != 0 {
return 0, fmt.Errorf("count history: code=%d stderr=%s", code, stderr)
}
nStr := strings.TrimSpace(stdout)
n, err := strconv.Atoi(nStr)
if err != nil {
return 0, fmt.Errorf("parse count: %q: %w", nStr, err)
}
return n, nil
}
// Mount mounts the device at mnt. Runs e2fsck -y first to repair any
// filesystem inconsistencies from incomplete replication.
func (p *pgHelper) Mount(ctx context.Context) error {
p.node.RunRoot(ctx, fmt.Sprintf("mkdir -p %s", p.mnt))
// e2fsck -y auto-fixes errors (returns 0=clean, 1=corrected, 2=corrected+reboot).
// Only fail on exit code >= 4 (uncorrectable).
_, stderr, code, _ := p.node.RunRoot(ctx, fmt.Sprintf("e2fsck -y %s 2>/dev/null", p.dev))
if code >= 4 {
return fmt.Errorf("e2fsck: code=%d stderr=%s", code, stderr)
}
_, stderr, code, _ = p.node.RunRoot(ctx, fmt.Sprintf("mount %s %s", p.dev, p.mnt))
if code != 0 {
return fmt.Errorf("mount: code=%d stderr=%s", code, stderr)
}
return nil
}
// Unmount force-unmounts the mount point.
func (p *pgHelper) Unmount(ctx context.Context) {
p.node.RunRoot(ctx, fmt.Sprintf("umount -f %s 2>/dev/null", p.mnt))
}
// Cleanup stops postgres, unmounts, and removes mount point.
func (p *pgHelper) Cleanup(ctx context.Context) {
p.Stop(ctx)
p.Unmount(ctx)
p.node.RunRoot(ctx, fmt.Sprintf("rm -rf %s", p.mnt))
}