mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
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>
126 lines
3.0 KiB
Go
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
|
|
}
|