Files
seaweedfs/weed/storage/blockvol/testrunner/actions/helpers.go
T
Ping QiuandClaude Opus 4.6 1b3edd7856 feat: CP11A-2 coordinated expand protocol for replicated block volumes
Two-phase prepare/commit/cancel protocol ensures all replicas expand
atomically. Standalone volumes use direct-commit (unchanged behavior).

Engine: PrepareExpand/CommitExpand/CancelExpand with on-disk
PreparedSize+ExpandEpoch in superblock, crash recovery clears stale
prepare state on open, v.mu serializes concurrent expand operations.

Proto: 3 new RPCs (PrepareExpand/CommitExpand/CancelExpandBlockVolume).

Coordinator: expandClean flag pattern — ReleaseExpandInflight only on
clean success or full cancel. Partial replica commit failure calls
MarkExpandFailed (keeps ExpandInProgress=true, suppresses heartbeat
size updates). ClearExpandFailed for manual reconciliation.

Registry: AcquireExpandInflight records PendingExpandSize+ExpandEpoch.
ExpandFailed state blocks new expands until cleared.

Tests: 15 engine + 4 VS + 10 coordinator + heartbeat suppression
regression + updated QA CP82/durability tests with prepare/commit mocks.

Also includes CP11A-1 remaining: QA storage profile tests, QA
io_backend config tests, testrunner perf-baseline scenarios and
coordinated-expand actions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 15:06:48 -07:00

126 lines
3.0 KiB
Go

package actions
import (
"fmt"
"strconv"
"strings"
"time"
tr "github.com/seaweedfs/seaweedfs/weed/storage/blockvol/testrunner"
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol/testrunner/infra"
)
// getNode retrieves the infra.Node for the named node from the action context.
func getNode(actx *tr.ActionContext, name string) (*infra.Node, error) {
if name == "" {
// Try to get the first available node.
for _, n := range actx.Nodes {
if nn, ok := n.(*infra.Node); ok {
return nn, nil
}
}
return nil, fmt.Errorf("no nodes available")
}
n, ok := actx.Nodes[name]
if !ok {
return nil, fmt.Errorf("node %q not found", name)
}
nn, ok := n.(*infra.Node)
if !ok {
return nil, fmt.Errorf("node %q is not an infra.Node", name)
}
return nn, nil
}
// getTargetNode retrieves the node associated with a target.
func getTargetNode(actx *tr.ActionContext, targetName string) (*infra.Node, error) {
spec, ok := actx.Scenario.Targets[targetName]
if !ok {
return nil, fmt.Errorf("target %q not in scenario", targetName)
}
return getNode(actx, spec.Node)
}
// getTargetHost returns the host address for a target's node.
func getTargetHost(actx *tr.ActionContext, targetName string) (string, error) {
spec, ok := actx.Scenario.Targets[targetName]
if !ok {
return "", fmt.Errorf("target %q not in scenario", targetName)
}
nodeSpec, ok := actx.Scenario.Topology.Nodes[spec.Node]
if !ok {
return "", fmt.Errorf("node %q not in topology", spec.Node)
}
if nodeSpec.IsLocal {
return "127.0.0.1", nil
}
return nodeSpec.Host, nil
}
func parseDuration(s string) (time.Duration, error) {
return time.ParseDuration(s)
}
func parseDurationMs(s string) (uint32, error) {
d, err := time.ParseDuration(s)
if err != nil {
// Try parsing as plain number (milliseconds).
ms, err2 := strconv.ParseUint(s, 10, 32)
if err2 != nil {
return 0, err
}
return uint32(ms), nil
}
return uint32(d.Milliseconds()), nil
}
func parseInt(s string, def int) int {
if s == "" {
return def
}
v, err := strconv.Atoi(s)
if err != nil {
return def
}
return v
}
// parseSizeBytes converts a human-readable size string (e.g. "50M", "1G", "104857600") to bytes.
func parseSizeBytes(s string) (uint64, error) {
s = strings.TrimSpace(s)
if s == "" {
return 0, fmt.Errorf("empty size string")
}
upper := strings.ToUpper(s)
var multiplier uint64 = 1
switch {
case strings.HasSuffix(upper, "G"):
multiplier = 1024 * 1024 * 1024
s = strings.TrimSuffix(upper, "G")
case strings.HasSuffix(upper, "M"):
multiplier = 1024 * 1024
s = strings.TrimSuffix(upper, "M")
case strings.HasSuffix(upper, "K"):
multiplier = 1024
s = strings.TrimSuffix(upper, "K")
default:
s = upper
}
v, err := strconv.ParseUint(s, 10, 64)
if err != nil {
return 0, fmt.Errorf("parse size %q: %w", s, err)
}
return v * multiplier, nil
}
func parseIntSlice(s string) []int {
var result []int
for _, part := range strings.Split(s, ",") {
part = strings.TrimSpace(part)
if v, err := strconv.Atoi(part); err == nil {
result = append(result, v)
}
}
return result
}