Skip to content

Commit 846864f

Browse files
authored
Merge pull request #549 from element-hq/bbz/large-numbers-of-sfu-udp-ports
Handle wide Matrix RTC SFU UDP port ranges
2 parents 011b4c7 + 276c525 commit 846864f

7 files changed

Lines changed: 120 additions & 13 deletions

File tree

charts/matrix-stack/templates/matrix-rtc/sfu_deployment.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ spec:
114114
{{- if .enabled }}
115115
{{- $portType := .type -}}
116116
{{- with .portRange }}
117+
{{- /* container.port is metadata. Omit if a large range is provided so that we don't run into document size limits when submitting to the API server */ -}}
118+
{{- if le (sub (.endPort | int) (.startPort | int)) 100 }}
117119
{{- range $port := untilStep (.startPort | int) (.endPort | int) 1 }}
118120
- containerPort: {{ $port }}
119121
name: rtc-udp-{{ $port }}
@@ -123,6 +125,7 @@ spec:
123125
{{- end }}
124126
{{- end }}
125127
{{- end }}
128+
{{- end }}
126129
{{- end }}
127130
{{- end }}
128131
{{- end }}
Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{{- /*
2-
Copyright 2024 New Vector Ltd
2+
Copyright 2024-2025 New Vector Ltd
33

44
SPDX-License-Identifier: AGPL-3.0-only
55
*/ -}}
@@ -8,29 +8,36 @@ SPDX-License-Identifier: AGPL-3.0-only
88
{{- if .enabled -}}
99
{{- with .sfu -}}
1010
{{- if and .enabled .exposedServices.rtcUdp.enabled (eq .exposedServices.rtcUdp.portType "NodePort") -}}
11+
{{- $range_start := (.exposedServices.rtcUdp.portRange.startPort | int) -}}
12+
{{- $range_end := (.exposedServices.rtcUdp.portRange.endPort | int) -}}
13+
{{- $range_size := sub $range_end $range_start -}}
14+
{{- $number_of_ranges := ((add ((div $range_size 250) | floor | int) 1) | int) -}}
15+
{{- range $port_segment := until $number_of_ranges }}
16+
---
1117
apiVersion: v1
1218
kind: Service
1319
metadata:
1420
labels:
15-
{{- include "element-io.matrix-rtc-sfu-rtc.labels" (dict "root" $ "context" .) | nindent 4 }}
16-
name: {{ $.Release.Name }}-matrix-rtc-sfu-udp-range
21+
{{- include "element-io.matrix-rtc-sfu-rtc.labels" (dict "root" $ "context" $.Values.matrixRTC.sfu) | nindent 4 }}
22+
name: {{ $.Release.Name }}-matrix-rtc-sfu-udp-range-{{ $port_segment }}
1723
namespace: {{ $.Release.Namespace }}
1824
spec:
1925
type: NodePort
2026
externalTrafficPolicy: Local
2127
ports:
22-
{{- with .exposedServices.rtcUdp.portRange }}
23-
{{- range $port := untilStep (.startPort | int) (.endPort | int) 1 }}
28+
{{- $segment_start := ((add $range_start (mul 250 $port_segment)) | int) }}
29+
{{- $segment_end := ((min (add $range_end 1) (add $segment_start 250)) | int) }}
30+
{{- range $port := untilStep $segment_start $segment_end 1 }}
2431
- name: rtc-udp-{{ $port }}
2532
port: {{ $port }}
2633
targetPort: {{ $port }}
2734
nodePort: {{ $port }}
2835
protocol: UDP
29-
{{- end }}
3036
{{- end }}
3137
selector:
3238
app.kubernetes.io/instance: "{{ $.Release.Name }}-matrix-rtc-sfu"
3339
{{- end }}
3440
{{- end }}
3541
{{- end }}
3642
{{- end }}
43+
{{- end }}

newsfragments/549.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Omit the UDP port range metadata for Matrix RTC's SFU if the range is larger than 100 ports.

newsfragments/549.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix Matrix RTC's SFU constructing an invalid Service if given too wide a nodePort range.

tests/manifests/test_matrix_rtc.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import pytest
66
import yaml
77

8+
from .utils import template_id
9+
810

911
@pytest.mark.parametrize("values_file", ["matrix-rtc-minimal-values.yaml"])
1012
@pytest.mark.asyncio_cooperative
@@ -39,3 +41,68 @@ async def test_external_ip_underrides(values, make_templates):
3941
break
4042
else:
4143
raise RuntimeError("Could not find config.yaml")
44+
45+
46+
async def get_sfu_udp_port_range_services(start_port, end_point, values, make_templates):
47+
values["matrixRTC"]["sfu"]["exposedServices"]["rtcUdp"]["portRange"]["startPort"] = start_port
48+
values["matrixRTC"]["sfu"]["exposedServices"]["rtcUdp"]["portRange"]["endPort"] = end_point
49+
50+
services = []
51+
for template in await make_templates(values):
52+
if template["kind"] == "Service" and "matrix-rtc-sfu-udp-range" in template["metadata"]["name"]:
53+
services.append(template)
54+
return services
55+
56+
57+
def assert_sharded_udp_range_ports(start_port, end_port, service):
58+
id = template_id(service)
59+
service_ports = service["spec"]["ports"]
60+
assert len(service_ports) == (end_port - start_port + 1), f"{id} doesn't have the correct number of ports in it"
61+
62+
assert service_ports[0]["port"] == start_port, f"{id} doesn't start with port {start_port}"
63+
expected_port = start_port
64+
for index, port in enumerate(service_ports):
65+
assert port["port"] == expected_port, (
66+
f"{id}.port[{index}]['port'] isn't {expected_port} ({start_port} to {end_port})"
67+
)
68+
assert port["targetPort"] == expected_port, (
69+
f"{id}.port[{index}]['targetPort'] isn't {expected_port} ({start_port} to {end_port})"
70+
)
71+
assert port["nodePort"] == expected_port, (
72+
f"{id}.port[{index}]['nodePort'] isn't {expected_port} ({start_port} to {end_port})"
73+
)
74+
assert port["name"] == f"rtc-udp-{expected_port}", (
75+
f"{id}.port[{index}]['name'] isn't rtc-udp-{expected_port} ({start_port} to {end_port})"
76+
)
77+
expected_port += 1
78+
assert port["port"] == end_port, f"{id} doesn't end with port {end_port}"
79+
80+
81+
@pytest.mark.parametrize("values_file", ["matrix-rtc-exposed-services-values.yaml"])
82+
@pytest.mark.asyncio_cooperative
83+
async def test_udp_range_services_are_sharded(values, make_templates):
84+
start_port = 32000
85+
86+
services = await get_sfu_udp_port_range_services(start_port, start_port, values, make_templates)
87+
assert len(services) == 1, "SFU UDP Range service is incorrectly sharded for 1 port"
88+
assert_sharded_udp_range_ports(start_port, start_port, services[0])
89+
90+
services = await get_sfu_udp_port_range_services(start_port, start_port + 1, values, make_templates)
91+
assert len(services) == 1, "SFU UDP Range service is incorrectly sharded for 2 ports"
92+
assert_sharded_udp_range_ports(start_port, start_port + 1, services[0])
93+
94+
services = await get_sfu_udp_port_range_services(start_port, start_port + 249, values, make_templates)
95+
assert len(services) == 1, "SFU UDP Range service is incorrectly sharded for 250 ports"
96+
assert_sharded_udp_range_ports(start_port, start_port + 249, services[0])
97+
98+
services = await get_sfu_udp_port_range_services(start_port, start_port + 250, values, make_templates)
99+
assert len(services) == 2, "SFU UDP Range service is incorrectly sharded for 251 ports"
100+
assert_sharded_udp_range_ports(start_port, start_port + 249, services[0])
101+
assert_sharded_udp_range_ports(start_port + 250, start_port + 250, services[1])
102+
103+
services = await get_sfu_udp_port_range_services(start_port, start_port + 999, values, make_templates)
104+
assert len(services) == 4, "SFU UDP Range service is incorrectly sharded for 1000 ports"
105+
assert_sharded_udp_range_ports(start_port, start_port + 249, services[0])
106+
assert_sharded_udp_range_ports(start_port + 250, start_port + 499, services[1])
107+
assert_sharded_udp_range_ports(start_port + 500, start_port + 749, services[2])
108+
assert_sharded_udp_range_ports(start_port + 750, start_port + 999, services[3])

tests/manifests/test_pod_containers_ports.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ async def test_unique_ports_in_containers(templates):
2323
@pytest.mark.asyncio_cooperative
2424
async def test_ports_in_containers_are_named(templates):
2525
for template in templates:
26-
if template["kind"] in ["Deployment", "StatefulSet", "Job"]:
26+
if template["kind"] in ["Deployment", "StatefulSet"]:
2727
port_names = []
2828
for container in template["spec"]["template"]["spec"]["containers"]:
2929
for port in container.get("ports", []):
@@ -45,3 +45,21 @@ async def test_no_ports_in_jobs(templates):
4545
for container in template["spec"]["template"]["spec"]["containers"]:
4646
ports += [port["containerPort"] for port in container.get("ports", [])]
4747
assert len(ports) == 0, f"Ports are present in job: {template_id(template)}, {ports}"
48+
49+
50+
@pytest.mark.parametrize("values_file", values_files_to_test)
51+
@pytest.mark.asyncio_cooperative
52+
async def test_not_too_many_container_ports(templates):
53+
for template in templates:
54+
if template["kind"] in ["Deployment", "StatefulSet"]:
55+
for container in template["spec"]["template"]["spec"]["containers"]:
56+
number_of_ports = len(container.get("ports", []))
57+
# This limit is fairly arbitrary. Unlike with Services (which have a limit of 250 ports),
58+
# there doesn't appear to be a hard limit of number of ports on a Pod/container. However if
59+
# you go wild you hit maximum document size when attempting to put the manifest into the
60+
# cluster. 100 is chosen as anything more quickly makes `kubectl {describe,get}` unusable.
61+
# Container ports are "just" metadata, albeit one which helps the scheduler if `hostPorts`
62+
# are involved
63+
assert number_of_ports < 100, (
64+
f"{template_id(template)}/{container['name']} has too many ports ({number_of_ports} >= 100)"
65+
)

tests/manifests/test_services.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,22 @@
1313
async def test_ports_in_services_are_named(templates):
1414
for template in templates:
1515
if template["kind"] == "Service":
16-
id = template_id(template)
17-
assert "ports" in template["spec"], f"{id} does not specify a ports list"
18-
assert len(template["spec"]["ports"]) > 0, f"{id} does not include any ports"
19-
2016
port_names = []
2117
for port in template["spec"]["ports"]:
22-
assert "name" in port, f"{id} has a port without a name: {port}"
18+
assert "name" in port, f"{template_id(template)} has a port without a name: {port}"
2319
port_names.append(port["name"])
24-
assert len(port_names) == len(set(port_names)), f"Port names are not unique: {id}, {port_names}"
20+
assert len(port_names) == len(set(port_names)), (
21+
f"Port names are not unique: {template_id(template)}, {port_names}"
22+
)
23+
24+
25+
@pytest.mark.parametrize("values_file", services_values_files_to_test)
26+
@pytest.mark.asyncio_cooperative
27+
async def test_not_too_many_ports_in_services(templates):
28+
for template in templates:
29+
if template["kind"] == "Service":
30+
assert "ports" in template["spec"], f"{template_id(template)} does not specify a ports list"
31+
32+
number_of_ports = len(template["spec"]["ports"])
33+
assert number_of_ports > 0, f"{template_id(template)} does not include any ports"
34+
assert number_of_ports <= 250, f"{template_id(template)} has more than 250 ports"

0 commit comments

Comments
 (0)