ec distribution: tolerate zero-value replication configs

A ReplicationConfig{} with zeroed minimums made maxShare divide by zero;
clamp each level's unit count to at least one. Covers the new behaviors
with tests.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Chris Lu
2026-10-21 14:11:13 -07:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 2b71843a22
commit 015cbddc38
2 changed files with 34 additions and 6 deletions
@@ -31,9 +31,9 @@ type ECDistribution struct {
// replication policy describes.
func CalculateDistribution(ec ECConfig, rep ReplicationConfig) *ECDistribution {
total := ec.TotalShards()
dcUnits := rep.MinDataCenters
rackUnits := rep.MinDataCenters * rep.MinRacksPerDC
nodeUnits := rackUnits * rep.MinNodesPerRack
dcUnits := max(rep.MinDataCenters, 1)
rackUnits := dcUnits * max(rep.MinRacksPerDC, 1)
nodeUnits := rackUnits * max(rep.MinNodesPerRack, 1)
return &ECDistribution{
ECConfig: ec,
ReplicationConfig: rep,
@@ -55,11 +55,14 @@ func evenShare(total, units int) int {
// share plus the slack that would pad the total to the next multiple of the
// unit count.
func maxShare(total, units int) int {
max := ceilDivide(total, units) + units - total%units
if max > total {
if units < 1 {
units = 1
}
m := ceilDivide(total, units) + units - total%units
if m > total {
return total
}
return max
return m
}
func ceilDivide(a, b int) int {
@@ -246,3 +246,28 @@ func TestPlanRebalanceNilAnalysis(t *testing.T) {
_, err := NewRebalancer(DefaultECConfig(), ReplicationConfig{}).PlanRebalance(nil)
assert.Error(t, err)
}
func TestPlanRebalanceNoDuplicateShardOnNode(t *testing.T) {
a := NewTopologyAnalysis()
a.AddNode(makeNode("n1", "dc1", "r1", 10, 0, 1, 2, 3))
a.AddNode(makeNode("n2", "dc1", "r1", 10, 3))
a.Finalize()
plan, err := NewRebalancer(DefaultECConfig(), ReplicationConfig{MinDataCenters: 1, MinRacksPerDC: 1, MinNodesPerRack: 2}).PlanRebalance(a)
require.NoError(t, err)
// n2 already holds 3, so the balancing move brings a different shard
assert.Equal(t, 3, a.ShardsByNode["n1"])
assert.Equal(t, 2, a.ShardsByNode["n2"])
assert.Equal(t, []int{3, 2}, a.AllNodes["n2"].ShardIDs)
for _, move := range plan.Moves {
assert.NotContains(t, move.DestNode.ShardIDs[:len(move.DestNode.ShardIDs)-1], move.ShardID,
"shard %d must not be moved onto a node already holding it", move.ShardID)
}
}
func TestCalculateDistributionZeroReplication(t *testing.T) {
dist := CalculateDistribution(DefaultECConfig(), ReplicationConfig{})
assert.Equal(t, 14, dist.TargetShardsPerDC)
assert.NotPanics(t, func() { dist.Summary(); dist.FaultToleranceAnalysis() })
}