feat: send EC shards in parallel per destination disk (#12050)

* feat: send EC shards in parallel per destination disk

One stream per destination disk. max_shard_transfers unset means no global cap, 0 sends shards sequentially, and N is a global cap that still keeps one stream per disk. The value is persisted through TaskPolicy. The maintenance form is a checkbox plus Max transfers because unset and 0 differ. The key is intentionally omitted from GetConfigSpec.

* ec shard distribution: treat negative max_shard_transfers as sequential

The config layer (normalizedMaxShardTransfers) clamps negative values to 0,
which selects sequential distribution. shardTransferCap diverged: a negative
cap that reached ShardDistributionOptions directly would have selected
uncapped parallel mode, the most aggressive setting. Align the options layer
with the same normalization so invalid input degrades to the conservative
mode.

* ec: serialize shard transfers to a server when a target leaves disk choice to the receiver

disk_id 0 is auto-placement on the receiving volume server, so an
auto-placed stream can land on a disk another queued stream is pinned
to. Grouping by (destNode, disk_id) alone let both run at once; collapse
all of that server's transfers under the destNode key instead.

* worker: clamp max_shard_transfers to the int32 wire range

ReadIntConfig returned the 0 fallback for values above MaxInt32, turning
a large positive cap into sequential mode. Read int64 and clamp into
range instead.

* worker: drop unused shard-transfer form helper

The maintenance form renders and collects max_shard_transfers in
plugin.templ JavaScript; the Go copy had no callers.

* worker: bound max_shard_transfers conversion under an in-range guard

---------

Co-authored-by: Chepurnov Simon <195125934+CHEPURNOV-S-S@users.noreply.github.com>
Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-25 06:37:26 -07:00
1 parent 0c9872f842
commit fefac028ec
15 files changed
+1089 -26

No files matched your search

@@ -3,6 +3,7 @@ package erasure_coding
import (
"context"
"fmt"
"math"
"path/filepath"
"strings"
"time"
@@ -174,6 +175,20 @@ func (h *ErasureCodingHandler) Descriptor() *plugin_pb.JobTypeDescriptor {
},
},
},
{
SectionId: "shard_distribution",
Title: "Shard distribution",
Description: "How many EC shard copies are in flight. The checkbox is presentation only; the saved value is max_shard_transfers.",
Fields: []*plugin_pb.ConfigField{
{
Name: "max_shard_transfers",
Label: "Parallel shard distribution",
Description: "Unchecked sends shards sequentially. Checked with an empty max uses one in-flight copy per destination disk. A number is a global cap, still one copy per disk.",
FieldType: plugin_pb.ConfigFieldType_CONFIG_FIELD_TYPE_INT64,
Widget: plugin_pb.ConfigWidget_CONFIG_WIDGET_NUMBER,
},
},
},
},
DefaultValues: map[string]*plugin_pb.ConfigValue{
"quiet_for_seconds": {
@@ -545,6 +560,7 @@ func (h *ErasureCodingHandler) Execute(
h.grpcDialOption,
)
task.workerAddress = h.workerAddress
task.maxShardTransfers = deriveErasureCodingWorkerConfig(request.GetWorkerConfigValues()).TaskConfig.MaxShardTransfers
execCtx, execCancel := context.WithCancel(ctx)
defer execCancel()
task.SetProgressCallback(func(progress float64, stage string) {
@@ -669,12 +685,33 @@ func deriveErasureCodingWorkerConfig(values map[string]*plugin_pb.ConfigValue) *
deadband = 100000
}
taskConfig.FreeSlotsDeadband = deadband
taskConfig.MaxShardTransfers = maxShardTransfersFromValues(values)
return &erasureCodingWorkerConfig{
TaskConfig: taskConfig,
}
}
// maxShardTransfersFromValues returns nil when the key is absent so shard
// distribution keeps the uncapped default. 0 is sequential; N>0 is a global cap.
func maxShardTransfersFromValues(values map[string]*plugin_pb.ConfigValue) *int {
if values == nil {
return nil
}
value, ok := values["max_shard_transfers"]
if !ok || value == nil {
return nil
}
n := pluginworker.ReadInt64Config(values, "max_shard_transfers", 0)
v := 0
if n > math.MaxInt32 {
v = math.MaxInt32
} else if n > 0 {
v = int(n)
}
return &v
}
func buildErasureCodingProposal(
result *workertypes.TaskDetectionResult,
baseWorkingDir string,