11# Builders for Shadow simulator runs: config values -> kubernetes-client objects
22# and yaml dicts. Pure data, no I/O. See the "Using Shadow at DST" runbook.
3- from typing import Optional
3+ from typing import Literal , Optional
44
55from kubernetes .client import (
66 V1Capabilities ,
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 : str = "yamux" ,
52- metrics_interval_s : int = 15 ,
53- 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 ,
5454) -> dict :
55- """Build the shadow.yaml dict: N peer hosts running `./main` + a publisher host
56- running the pod-api-requester in batch mode against the peers' `/publish`
57- endpoints. The traffic shape (message count, size, pacing) lives in the
58- requester's own config (see `render_publisher_config`), mounted at
59- `{_CONFIG_MOUNT}/{_PUBLISHER_CONFIG}`."""
60- if connect_to >= num_nodes :
61- raise ValueError (f"connect_to ({ connect_to } ) must be smaller than num_nodes ({ num_nodes } )." )
62-
63- peer_env = {
55+ """Environment for a relay (pod-*) node."""
56+ env = {
6457 "PEERS" : str (num_nodes ),
6558 "CONNECTTO" : str (connect_to ),
6659 "SHADOWENV" : "true" , # env.nim requires the literal string "true"
6760 "MUXER" : muxer ,
61+ "DISCOVERY" : discovery ,
62+ "STARTSLEEP" : str (start_sleep ),
6863 "METRICS_INTERVAL_S" : str (metrics_interval_s ),
6964 }
70- peer_process = {
71- "path" : "./main" ,
72- "start_time" : "5s" ,
73- "expected_final_state" : "running" , # daemon: don't error when alive at stop_time
74- "environment" : peer_env ,
75- }
76- hosts = {
65+ if lsquic_tick_floor_us > 0 :
66+ # Needs the tick-floor test-node image (stock images ignore the env var).
67+ env ["LSQUIC_TICK_FLOOR_US" ] = str (lsquic_tick_floor_us )
68+ if discovery == "kad-dht" :
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 {
7779 f"pod-{ i } " : {
7880 "network_node_id" : 0 ,
79- "processes" : [peer_process ],
81+ "processes" : [
82+ {
83+ "path" : "./main" ,
84+ "start_time" : f"{ 5000 + i * start_jitter_ms } ms" ,
85+ # daemon: don't error when alive at stop_time
86+ "expected_final_state" : "running" ,
87+ "environment" : peer_env ,
88+ }
89+ ],
8090 }
8191 for i in range (num_nodes )
8292 }
83- 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 {
84134 "network_node_id" : 0 ,
85135 "processes" : [
86136 {
@@ -94,16 +144,75 @@ def render_shadow_yaml(
94144 }
95145 ],
96146 }
97- return {
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 )
197+ # Always render the seed so the run's shadow.yaml records it (Shadow defaults to 1).
198+ config = {
98199 "general" : {
99200 "stop_time" : f"{ sim_stop_time_s } s" ,
100201 "progress" : True ,
202+ "seed" : seed ,
101203 },
102204 "network" : {
103205 "graph" : {"type" : "1_gbit_switch" },
104206 },
105207 "hosts" : hosts ,
106208 }
209+ if model_unblocked_syscall_latency :
210+ config ["general" ]["model_unblocked_syscall_latency" ] = True
211+ if strace_logging_mode != "off" :
212+ # Global (all hosts) and voluminous — a straced host writes ~100s of MB per
213+ # simulated minute of activity. Diagnostics at small N only.
214+ config ["experimental" ] = {"strace_logging_mode" : strace_logging_mode }
215+ return config
107216
108217
109218def render_publisher_config (
0 commit comments