Skip to content

Commit d64497c

Browse files
Backport of scheduler: don't sort reserved port ranges before adding to bitmap into release/1.10.x (#26725)
Co-authored-by: Tim Gross <[email protected]>
1 parent 470a82a commit d64497c

File tree

5 files changed

+65
-79
lines changed

5 files changed

+65
-79
lines changed

.changelog/26712.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:improvement
2+
scheduling: Improve performance of scheduling when checking reserved ports usage
3+
```

nomad/structs/funcs.go

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"encoding/binary"
1010
"fmt"
1111
"math"
12+
"slices"
1213
"sort"
1314
"strconv"
1415
"strings"
@@ -485,15 +486,18 @@ func CompareMigrateToken(allocID, nodeSecretID, otherMigrateToken string) bool {
485486
// port ranges. A port number is a single integer and a port range is two
486487
// integers separated by a hyphen. As an example the following spec would
487488
// convert to: ParsePortRanges("10,12-14,16") -> []uint64{10, 12, 13, 14, 16}
489+
// This function may return duplicates or overlapping ranges, so we limit the
490+
// maximum number of ports returned to MaxValidPort.
488491
func ParsePortRanges(spec string) ([]uint64, error) {
492+
count := 0
489493
parts := strings.Split(spec, ",")
490494

491495
// Hot path the empty case
492496
if len(parts) == 1 && parts[0] == "" {
493497
return nil, nil
494498
}
495499

496-
ports := make(map[uint64]struct{})
500+
ports := []uint64{}
497501
for _, part := range parts {
498502
part = strings.TrimSpace(part)
499503
rangeParts := strings.Split(part, "-")
@@ -507,11 +511,17 @@ func ParsePortRanges(spec string) ([]uint64, error) {
507511
if err != nil {
508512
return nil, err
509513
}
510-
514+
if port == 0 {
515+
return nil, fmt.Errorf("port must be > 0")
516+
}
511517
if port > MaxValidPort {
512518
return nil, fmt.Errorf("port must be < %d but found %d", MaxValidPort, port)
513519
}
514-
ports[port] = struct{}{}
520+
count++
521+
if count > MaxValidPort {
522+
return nil, fmt.Errorf("maximum of %d ports can be reserved", MaxValidPort)
523+
}
524+
ports = append(ports, port)
515525
}
516526
case 2:
517527
// We are parsing a range
@@ -526,36 +536,29 @@ func ParsePortRanges(spec string) ([]uint64, error) {
526536
}
527537

528538
if end < start {
529-
return nil, fmt.Errorf("invalid range: starting value (%v) less than ending (%v) value", end, start)
539+
return nil, fmt.Errorf("invalid range: ending value (%v) less than starting (%v) value", end, start)
530540
}
531541

532542
// Full range validation is below but prevent creating
533543
// arbitrarily large arrays here
544+
if start == 0 {
545+
return nil, fmt.Errorf("port must be > 0")
546+
}
534547
if end > MaxValidPort {
535548
return nil, fmt.Errorf("port must be < %d but found %d", MaxValidPort, end)
536549
}
537-
550+
count += int(end - start)
551+
if count > MaxValidPort {
552+
return nil, fmt.Errorf("maximum of %d ports can be reserved", MaxValidPort)
553+
}
554+
ports = slices.Grow(ports, int(end-start))
538555
for i := start; i <= end; i++ {
539-
ports[i] = struct{}{}
556+
ports = append(ports, i)
540557
}
541558
default:
542559
return nil, fmt.Errorf("can only parse single port numbers or port ranges (ex. 80,100-120,150)")
543560
}
544561
}
545562

546-
var results []uint64
547-
for port := range ports {
548-
if port == 0 {
549-
return nil, fmt.Errorf("port must be > 0")
550-
}
551-
if port > MaxValidPort {
552-
return nil, fmt.Errorf("port must be < %d but found %d", MaxValidPort, port)
553-
}
554-
results = append(results, port)
555-
}
556-
557-
sort.Slice(results, func(i, j int) bool {
558-
return results[i] < results[j]
559-
})
560-
return results, nil
563+
return ports, nil
561564
}

nomad/structs/funcs_test.go

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -966,14 +966,16 @@ func TestVaultNamespaceSet(t *testing.T) {
966966
require.ElementsMatch(t, expected, got)
967967
}
968968

969-
// TestParsePortRanges asserts ParsePortRanges errors on invalid port ranges.
969+
// TestParsePortRanges asserts ParsePortRanges errors on invalid port ranges and
970+
// returns the expected values
970971
func TestParsePortRanges(t *testing.T) {
971972
ci.Parallel(t)
972973

973974
cases := []struct {
974-
name string
975-
spec string
976-
err string
975+
name string
976+
spec string
977+
expect []uint64
978+
err string
977979
}{
978980
{
979981
name: "UnmatchedDash",
@@ -995,14 +997,44 @@ func TestParsePortRanges(t *testing.T) {
995997
spec: "9223372036854775807", // (2**63)-1
996998
err: "port must be < 65536 but found 9223372036854775807",
997999
},
1000+
{
1001+
name: "OverlappingRanges",
1002+
spec: "1-3,2-4",
1003+
expect: []uint64{1, 2, 3, 2, 3, 4}, // we don't care about dupes
1004+
},
1005+
{
1006+
name: "ReversedRange",
1007+
spec: "3-1",
1008+
err: "invalid range: ending value (1) less than starting (3) value",
1009+
},
1010+
{
1011+
name: "ZeroRange",
1012+
spec: "0-1",
1013+
err: "port must be > 0",
1014+
},
1015+
{
1016+
name: "OverlappingOutOfOrderRanges",
1017+
spec: "2-4,1-3",
1018+
expect: []uint64{2, 3, 4, 1, 2, 3}, // we don't care about dupes
1019+
},
1020+
{
1021+
name: "HugeRange",
1022+
spec: "1-65536,1-65536",
1023+
err: "maximum of 65536 ports can be reserved",
1024+
},
9981025
}
9991026

10001027
for i := range cases {
10011028
tc := cases[i]
10021029
t.Run(tc.name, func(t *testing.T) {
10031030
results, err := ParsePortRanges(tc.spec)
1004-
require.Nil(t, results)
1005-
require.EqualError(t, err, tc.err)
1031+
if tc.err == "" {
1032+
must.NoError(t, err)
1033+
must.Eq(t, tc.expect, results)
1034+
} else {
1035+
must.Nil(t, results)
1036+
must.EqError(t, err, tc.err)
1037+
}
10061038
})
10071039
}
10081040
}

nomad/structs/structs.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3774,11 +3774,6 @@ type NodeReservedNetworkResources struct {
37743774
ReservedHostPorts string
37753775
}
37763776

3777-
// ParseReservedHostPorts returns the reserved host ports.
3778-
func (n *NodeReservedNetworkResources) ParseReservedHostPorts() ([]uint64, error) {
3779-
return ParsePortRanges(n.ReservedHostPorts)
3780-
}
3781-
37823777
// AllocatedResources is the set of resources to be used by an allocation.
37833778
type AllocatedResources struct {
37843779
// Tasks is a mapping of task name to the resources for the task.

nomad/structs/structs_test.go

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7258,53 +7258,6 @@ func TestSpread_Validate(t *testing.T) {
72587258
}
72597259
}
72607260

7261-
func TestNodeReservedNetworkResources_ParseReserved(t *testing.T) {
7262-
ci.Parallel(t)
7263-
7264-
require := require.New(t)
7265-
cases := []struct {
7266-
Input string
7267-
Parsed []uint64
7268-
Err bool
7269-
}{
7270-
{
7271-
"1,2,3",
7272-
[]uint64{1, 2, 3},
7273-
false,
7274-
},
7275-
{
7276-
"3,1,2,1,2,3,1-3",
7277-
[]uint64{1, 2, 3},
7278-
false,
7279-
},
7280-
{
7281-
"3-1",
7282-
nil,
7283-
true,
7284-
},
7285-
{
7286-
"1-3,2-4",
7287-
[]uint64{1, 2, 3, 4},
7288-
false,
7289-
},
7290-
{
7291-
"1-3,4,5-5,6,7,8-10",
7292-
[]uint64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
7293-
false,
7294-
},
7295-
}
7296-
7297-
for i, tc := range cases {
7298-
r := &NodeReservedNetworkResources{ReservedHostPorts: tc.Input}
7299-
out, err := r.ParseReservedHostPorts()
7300-
if (err != nil) != tc.Err {
7301-
t.Fatalf("test case %d: %v", i, err)
7302-
}
7303-
7304-
require.Equal(out, tc.Parsed)
7305-
}
7306-
}
7307-
73087261
func TestMultiregion_CopyCanonicalize(t *testing.T) {
73097262
ci.Parallel(t)
73107263

0 commit comments

Comments
 (0)