Skip to content

Commit 08cf069

Browse files
radikenclaude
andcommitted
shadow: extract render_shadow_yaml host builders
Pull the peer env and the peer / bootstrap / publisher host construction out of render_shadow_yaml into small pure functions so each is unit-testable. Output is unchanged; adds tests for the extracted builders. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6046e82 commit 08cf069

2 files changed

Lines changed: 186 additions & 63 deletions

File tree

src/deployments/shadow/builders.py

Lines changed: 111 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -42,36 +42,18 @@
4242
)
4343

4444

45-
def render_shadow_yaml(
45+
def _peer_env(
4646
*,
4747
num_nodes: int,
48-
sim_stop_time_s: int,
49-
publisher_start_s: int,
50-
connect_to: int = 2,
51-
muxer: Literal["yamux", "mplex", "quic"] = "yamux",
52-
discovery: Literal["static", "kad-dht"] = "static",
53-
start_sleep: int = 60,
54-
metrics_interval_s: int = 15,
55-
seed: int = 1,
56-
model_unblocked_syscall_latency: bool = False,
57-
strace_logging_mode: str = "off",
58-
lsquic_tick_floor_us: int = 0,
59-
start_jitter_ms: int = 0,
60-
requester_app_path: str = _REQUESTER_APP_PATH,
48+
connect_to: int,
49+
muxer: str,
50+
discovery: str,
51+
start_sleep: int,
52+
metrics_interval_s: int,
53+
lsquic_tick_floor_us: int,
6154
) -> dict:
62-
"""Build the shadow.yaml dict: N peer hosts running `./main` + a publisher host
63-
running the pod-api-requester in batch mode against the peers' `/publish`
64-
endpoints. The traffic shape (message count, size, pacing) lives in the
65-
requester's own config (see `render_publisher_config`), mounted at
66-
`{_CONFIG_MOUNT}/{_PUBLISHER_CONFIG}`.
67-
68-
discovery selects mesh formation: "static" dials CONNECTTO peers by pod-N
69-
hostname; "kad-dht" adds a `bootstrap-0` anchor host that peers discover
70-
through (Shadow resolves it by hostname, so no k8s Service is needed)."""
71-
if connect_to >= num_nodes:
72-
raise ValueError(f"connect_to ({connect_to}) must be smaller than num_nodes ({num_nodes}).")
73-
74-
peer_env = {
55+
"""Environment for a relay (pod-*) node."""
56+
env = {
7557
"PEERS": str(num_nodes),
7658
"CONNECTTO": str(connect_to),
7759
"SHADOWENV": "true", # env.nim requires the literal string "true"
@@ -82,14 +64,18 @@ def render_shadow_yaml(
8264
}
8365
if lsquic_tick_floor_us > 0:
8466
# Needs the tick-floor test-node image (stock images ignore the env var).
85-
peer_env["LSQUIC_TICK_FLOOR_US"] = str(lsquic_tick_floor_us)
67+
env["LSQUIC_TICK_FLOOR_US"] = str(lsquic_tick_floor_us)
8668
if discovery == "kad-dht":
87-
peer_env["NODE_ROLE"] = "RoleNormal"
88-
peer_env["SERVICE"] = "bootstrap-0"
89-
# start_jitter_ms staggers per-pod process start so peers don't wake and dial at
90-
# one simulated instant (lockstep wakes force simultaneous-dial collisions that
91-
# never occur on real hosts).
92-
hosts = {
69+
env["NODE_ROLE"] = "RoleNormal"
70+
env["SERVICE"] = "bootstrap-0"
71+
return env
72+
73+
74+
def _peer_hosts(num_nodes: int, peer_env: dict, start_jitter_ms: int) -> dict:
75+
"""The N relay hosts (pod-0..pod-(N-1)). start_jitter_ms staggers per-pod process
76+
start so peers don't wake and dial at one simulated instant (lockstep wakes force
77+
simultaneous-dial collisions that never occur on real hosts)."""
78+
return {
9379
f"pod-{i}": {
9480
"network_node_id": 0,
9581
"processes": [
@@ -104,35 +90,47 @@ def render_shadow_yaml(
10490
}
10591
for i in range(num_nodes)
10692
}
107-
if discovery == "kad-dht":
108-
hosts["bootstrap-0"] = {
109-
"network_node_id": 0,
110-
"processes": [
111-
{
112-
"path": "./main",
113-
"start_time": "5s",
114-
"expected_final_state": "running",
115-
"environment": {
116-
"PEERS": str(num_nodes),
117-
"SHADOWENV": "true",
118-
"MUXER": muxer,
119-
"DISCOVERY": "kad-dht",
120-
"NODE_ROLE": "RoleBootstrap",
121-
# the single anchor must accept every node's bootstrap dial,
122-
# so lift its cap above the network size (default is 250).
123-
"MAXCONNECTIONS": str(num_nodes + 100),
124-
"STARTSLEEP": str(start_sleep),
125-
"METRICS_INTERVAL_S": str(metrics_interval_s),
126-
**(
127-
{"LSQUIC_TICK_FLOOR_US": str(lsquic_tick_floor_us)}
128-
if lsquic_tick_floor_us > 0
129-
else {}
130-
),
131-
},
132-
}
133-
],
134-
}
135-
hosts["publisher"] = {
93+
94+
95+
def _bootstrap_host(
96+
*,
97+
num_nodes: int,
98+
muxer: str,
99+
start_sleep: int,
100+
metrics_interval_s: int,
101+
lsquic_tick_floor_us: int,
102+
) -> dict:
103+
"""The kad-dht anchor (bootstrap-0); peers discover through it by hostname."""
104+
env = {
105+
"PEERS": str(num_nodes),
106+
"SHADOWENV": "true",
107+
"MUXER": muxer,
108+
"DISCOVERY": "kad-dht",
109+
"NODE_ROLE": "RoleBootstrap",
110+
# the single anchor must accept every node's bootstrap dial, so lift its cap
111+
# above the network size (default is 250).
112+
"MAXCONNECTIONS": str(num_nodes + 100),
113+
"STARTSLEEP": str(start_sleep),
114+
"METRICS_INTERVAL_S": str(metrics_interval_s),
115+
}
116+
if lsquic_tick_floor_us > 0:
117+
env["LSQUIC_TICK_FLOOR_US"] = str(lsquic_tick_floor_us)
118+
return {
119+
"network_node_id": 0,
120+
"processes": [
121+
{
122+
"path": "./main",
123+
"start_time": "5s",
124+
"expected_final_state": "running",
125+
"environment": env,
126+
}
127+
],
128+
}
129+
130+
131+
def _publisher_host(publisher_start_s: int, requester_app_path: str) -> dict:
132+
"""The publisher host: runs the pod-api-requester in batch mode against the peers."""
133+
return {
136134
"network_node_id": 0,
137135
"processes": [
138136
{
@@ -146,6 +144,56 @@ def render_shadow_yaml(
146144
}
147145
],
148146
}
147+
148+
149+
def render_shadow_yaml(
150+
*,
151+
num_nodes: int,
152+
sim_stop_time_s: int,
153+
publisher_start_s: int,
154+
connect_to: int = 2,
155+
muxer: Literal["yamux", "mplex", "quic"] = "yamux",
156+
discovery: Literal["static", "kad-dht"] = "static",
157+
start_sleep: int = 60,
158+
metrics_interval_s: int = 15,
159+
seed: int = 1,
160+
model_unblocked_syscall_latency: bool = False,
161+
strace_logging_mode: str = "off",
162+
lsquic_tick_floor_us: int = 0,
163+
start_jitter_ms: int = 0,
164+
requester_app_path: str = _REQUESTER_APP_PATH,
165+
) -> dict:
166+
"""Build the shadow.yaml dict: N peer hosts running `./main` + a publisher host
167+
running the pod-api-requester in batch mode against the peers' `/publish`
168+
endpoints. The traffic shape (message count, size, pacing) lives in the
169+
requester's own config (see `render_publisher_config`), mounted at
170+
`{_CONFIG_MOUNT}/{_PUBLISHER_CONFIG}`.
171+
172+
discovery selects mesh formation: "static" dials CONNECTTO peers by pod-N
173+
hostname; "kad-dht" adds a `bootstrap-0` anchor host that peers discover
174+
through (Shadow resolves it by hostname, so no k8s Service is needed)."""
175+
if connect_to >= num_nodes:
176+
raise ValueError(f"connect_to ({connect_to}) must be smaller than num_nodes ({num_nodes}).")
177+
178+
peer_env = _peer_env(
179+
num_nodes=num_nodes,
180+
connect_to=connect_to,
181+
muxer=muxer,
182+
discovery=discovery,
183+
start_sleep=start_sleep,
184+
metrics_interval_s=metrics_interval_s,
185+
lsquic_tick_floor_us=lsquic_tick_floor_us,
186+
)
187+
hosts = _peer_hosts(num_nodes, peer_env, start_jitter_ms)
188+
if discovery == "kad-dht":
189+
hosts["bootstrap-0"] = _bootstrap_host(
190+
num_nodes=num_nodes,
191+
muxer=muxer,
192+
start_sleep=start_sleep,
193+
metrics_interval_s=metrics_interval_s,
194+
lsquic_tick_floor_us=lsquic_tick_floor_us,
195+
)
196+
hosts["publisher"] = _publisher_host(publisher_start_s, requester_app_path)
149197
# Always render the seed so the run's shadow.yaml records it (Shadow defaults to 1).
150198
config = {
151199
"general": {

src/deployments/shadow/tests/test_builders.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
from ruamel.yaml import YAML
55

66
from src.deployments.shadow.builders import (
7+
_bootstrap_host,
8+
_peer_env,
9+
_peer_hosts,
10+
_publisher_host,
711
build_configmap,
812
build_pvc,
913
build_shadow_job,
@@ -69,6 +73,77 @@ def test_request_and_action_cross_references_resolve(self):
6973
assert cfg["actions"][0]["targets"] == [cfg["targets"][0]["name"]]
7074

7175

76+
# --------------------------------------------------------------------------- #
77+
# host builders (the pieces render_shadow_yaml assembles)
78+
# --------------------------------------------------------------------------- #
79+
class TestHostBuilders:
80+
def _env(self, **over):
81+
base = dict(
82+
num_nodes=5,
83+
connect_to=2,
84+
muxer="yamux",
85+
discovery="static",
86+
start_sleep=60,
87+
metrics_interval_s=15,
88+
lsquic_tick_floor_us=0,
89+
)
90+
base.update(over)
91+
return _peer_env(**base)
92+
93+
def test_peer_env_static_omits_kad_and_tickfloor(self):
94+
env = self._env()
95+
assert env["MUXER"] == "yamux" and env["DISCOVERY"] == "static"
96+
assert "NODE_ROLE" not in env and "SERVICE" not in env
97+
assert "LSQUIC_TICK_FLOOR_US" not in env
98+
99+
def test_peer_env_kad_adds_role_and_service(self):
100+
env = self._env(discovery="kad-dht")
101+
assert env["NODE_ROLE"] == "RoleNormal"
102+
assert env["SERVICE"] == "bootstrap-0"
103+
104+
def test_peer_env_tickfloor_only_when_set(self):
105+
assert (
106+
_peer_env(
107+
num_nodes=5,
108+
connect_to=2,
109+
muxer="quic",
110+
discovery="static",
111+
start_sleep=60,
112+
metrics_interval_s=15,
113+
lsquic_tick_floor_us=10000,
114+
)["LSQUIC_TICK_FLOOR_US"]
115+
== "10000"
116+
)
117+
118+
def test_peer_hosts_stagger_start_time_by_jitter(self):
119+
hosts = _peer_hosts(3, {"MUXER": "yamux"}, start_jitter_ms=50)
120+
assert [hosts[f"pod-{i}"]["processes"][0]["start_time"] for i in range(3)] == [
121+
"5000ms",
122+
"5050ms",
123+
"5100ms",
124+
]
125+
assert all(hosts[h]["processes"][0]["expected_final_state"] == "running" for h in hosts)
126+
127+
def test_bootstrap_host_lifts_connection_cap(self):
128+
h = _bootstrap_host(
129+
num_nodes=1000,
130+
muxer="yamux",
131+
start_sleep=60,
132+
metrics_interval_s=15,
133+
lsquic_tick_floor_us=0,
134+
)
135+
env = h["processes"][0]["environment"]
136+
assert env["NODE_ROLE"] == "RoleBootstrap"
137+
assert env["MAXCONNECTIONS"] == "1100" # num_nodes + 100
138+
139+
def test_publisher_host_runs_requester_batch(self):
140+
h = _publisher_host(90, "/app/api_requester.py")
141+
proc = h["processes"][0]
142+
assert proc["path"] == "/usr/bin/python3"
143+
assert "--mode batch" in proc["args"]
144+
assert proc["start_time"] == "90s"
145+
146+
72147
# --------------------------------------------------------------------------- #
73148
# render_shadow_yaml (peer hosts + publisher host)
74149
# --------------------------------------------------------------------------- #

0 commit comments

Comments
 (0)