Skip to content

Commit e869a65

Browse files
committed
refactor: update topology JSON schema to require payload_size and period_ms for publishers and intermediates
1 parent 6d0e094 commit e869a65

4 files changed

Lines changed: 95 additions & 44 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ python3 generate_exec_scripts.py ../topology_example/simple.json --rmw fastdds -
1717

1818
### Options Supported by Generated Scripts
1919

20-
Generated `host*_run.sh` and `local_run.sh` scripts support the runtime options below. `--eval-time` is applied to every launched node (Publisher / Subscriber / Intermediate). Payload size and period are taken from JSON topology values and passed directly to Publisher / Intermediate nodes. `--trial-idx` is available only on `host*_run.sh` and `local_run.sh`. For the JSON schema, see [topology_example/README.md](./topology_example/README.md).
20+
Generated `host*_run.sh` and `local_run.sh` scripts support the runtime options below. `--eval-time` is applied to every launched node (Publisher / Subscriber / Intermediate). `payload_size` and `period_ms` must be specified in each Publisher / Intermediate topic entry in the topology JSON, and those values are passed directly to Publisher / Intermediate nodes. `--trial-idx` is available only on `host*_run.sh` and `local_run.sh`. For the JSON schema, see [topology_example/README.md](./topology_example/README.md).
2121

2222
| Option | Short | Description | Default |
2323
|---|---|---|---|
@@ -37,7 +37,7 @@ Generated `host*_run.sh` and `local_run.sh` scripts support the runtime options
3737
./host1_run.sh -t 120
3838
```
3939

40-
`--eval-time` is applied to all nodes launched through `*_run.sh` or `local_run.sh`. Payload size and publish period are read from each Publisher/Intermediate entry in the topology JSON.
40+
`--eval-time` is applied to all nodes launched through `*_run.sh` or `local_run.sh`. `payload_size` and `period_ms` are read from each Publisher/Intermediate entry in the topology JSON.
4141

4242
### Pull the Shared Docker Image
4343

manager_scripts/generate_exec_scripts.py

Lines changed: 79 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,62 @@
3232
DEFAULT_EVAL_TIME = 60
3333

3434

35-
def _json_default_int(json_content, key, fallback):
36-
"""Return an integer default from top-level JSON, or fallback."""
37-
value = json_content.get(key, fallback)
35+
def _require_positive_int(entry, key, context):
36+
"""Read a required positive integer field from entry."""
37+
if key not in entry:
38+
raise ValueError(f"{context}: '{key}' is required")
3839
try:
39-
return int(value)
40-
except (TypeError, ValueError):
41-
return fallback
40+
value = int(entry[key])
41+
except (TypeError, ValueError) as exc:
42+
raise ValueError(
43+
f"{context}: '{key}' must be an integer"
44+
) from exc
45+
if value <= 0:
46+
raise ValueError(f"{context}: '{key}' must be > 0")
47+
return value
48+
4249

50+
def _validate_payload_period_constraints(json_content):
51+
"""Enforce payload/period schema constraints for topology JSON."""
52+
forbidden_root_keys = [
53+
key for key in ("payload_size", "period_ms") if key in json_content
54+
]
55+
if forbidden_root_keys:
56+
raise ValueError(
57+
"Top-level keys are not allowed: "
58+
f"{', '.join(forbidden_root_keys)}. "
59+
"Set payload_size and period_ms per Publisher/Intermediate topic entry."
60+
)
61+
62+
hosts = json_content.get("hosts")
63+
if not isinstance(hosts, list):
64+
raise ValueError("root key 'hosts' must be an array")
65+
66+
for host_idx, host in enumerate(hosts):
67+
host_name = host.get("host_name", f"hosts[{host_idx}]")
68+
nodes = host.get("nodes", [])
69+
if not isinstance(nodes, list):
70+
raise ValueError(f"host '{host_name}': 'nodes' must be an array")
71+
72+
for node_idx, node in enumerate(nodes):
73+
node_name = node.get("node_name", f"node[{node_idx}]")
74+
if node.get("publisher"):
75+
for pub_idx, pub in enumerate(node["publisher"]):
76+
context = f"node '{node_name}' publisher[{pub_idx}]"
77+
_require_positive_int(pub, "payload_size", context)
78+
_require_positive_int(pub, "period_ms", context)
4379

44-
def _topic_numeric_field(topic_entry, key, json_content, fallback):
45-
"""Return a numeric topic field, falling back to top-level JSON then fallback."""
46-
if key in topic_entry:
47-
try:
48-
return int(topic_entry[key])
49-
except (TypeError, ValueError):
50-
pass
51-
return _json_default_int(json_content, key, fallback)
80+
if "intermediate" in node:
81+
intermediate_entries = _normalize_intermediate_entries(
82+
node["intermediate"], node_name
83+
)
84+
for entry_idx, intermediate_entry in enumerate(intermediate_entries):
85+
for pub_idx, pub in enumerate(intermediate_entry.get("publisher", [])):
86+
context = (
87+
f"node '{node_name}' intermediate[{entry_idx}] publisher[{pub_idx}]"
88+
)
89+
_require_positive_int(pub, "payload_size", context)
90+
_require_positive_int(pub, "period_ms", context)
5291

5392

5493
def _normalize_ws_dir(ws_dir):
@@ -254,16 +293,19 @@ def _append_host_script_prelude(
254293
)
255294

256295

257-
def _append_publisher_block(lines, node_name, pub_list, qos_opts, json_content):
296+
def _append_publisher_block(lines, node_name, pub_list, qos_opts):
258297
topic_names = ",".join(p["topic_name"] for p in pub_list)
259298
payload_sizes = [
260-
_topic_numeric_field(
261-
p, "payload_size", json_content, DEFAULT_PAYLOAD_SIZE)
262-
for p in pub_list
299+
_require_positive_int(
300+
p, "payload_size", f"node '{node_name}' publisher[{idx}]"
301+
)
302+
for idx, p in enumerate(pub_list)
263303
]
264304
period_mses = [
265-
_topic_numeric_field(p, "period_ms", json_content, DEFAULT_PERIOD_MS)
266-
for p in pub_list
305+
_require_positive_int(
306+
p, "period_ms", f"node '{node_name}' publisher[{idx}]"
307+
)
308+
for idx, p in enumerate(pub_list)
267309
]
268310
payload_args = " ".join(f"--size {int(v)}" for v in payload_sizes)
269311
period_args = " ".join(f"--period {int(v)}" for v in period_mses)
@@ -336,16 +378,19 @@ def _collect_intermediate_pub_sub(intermediate_entries):
336378
return pub_defs, sub_topics
337379

338380

339-
def _append_intermediate_block(lines, node_name, pub_defs, sub_topics, qos_opts, json_content):
381+
def _append_intermediate_block(lines, node_name, pub_defs, sub_topics, qos_opts):
340382
pub_topics = [p["topic_name"] for p in pub_defs]
341383
payload_sizes = [
342-
_topic_numeric_field(
343-
p, "payload_size", json_content, DEFAULT_PAYLOAD_SIZE)
344-
for p in pub_defs
384+
_require_positive_int(
385+
p, "payload_size", f"node '{node_name}' intermediate publisher[{idx}]"
386+
)
387+
for idx, p in enumerate(pub_defs)
345388
]
346389
period_mses = [
347-
_topic_numeric_field(p, "period_ms", json_content, DEFAULT_PERIOD_MS)
348-
for p in pub_defs
390+
_require_positive_int(
391+
p, "period_ms", f"node '{node_name}' intermediate publisher[{idx}]"
392+
)
393+
for idx, p in enumerate(pub_defs)
349394
]
350395
payload_args = " ".join(f"--size {int(v)}" for v in payload_sizes)
351396
period_args = " ".join(f"--period {int(v)}" for v in period_mses)
@@ -385,8 +430,7 @@ def generate_exec_scripts(json_content, rmw, output_dir):
385430
os.makedirs(output_dir, exist_ok=True)
386431

387432
payload_size_default = DEFAULT_PAYLOAD_SIZE
388-
period_ms_default = _json_default_int(
389-
json_content, "period_ms", DEFAULT_PERIOD_MS)
433+
period_ms_default = DEFAULT_PERIOD_MS
390434
eval_time_default = DEFAULT_EVAL_TIME
391435

392436
qos_config = json_content.get("qos", {})
@@ -420,7 +464,6 @@ def generate_exec_scripts(json_content, rmw, output_dir):
420464
node_name,
421465
node["publisher"],
422466
qos_opts,
423-
json_content,
424467
)
425468
if node.get("subscriber"):
426469
_append_subscriber_block(
@@ -442,7 +485,6 @@ def generate_exec_scripts(json_content, rmw, output_dir):
442485
pub_defs,
443486
sub_topics,
444487
qos_opts,
445-
json_content,
446488
)
447489

448490
_append_host_script_epilogue(lines, host_name)
@@ -536,8 +578,7 @@ def _append_zenohd_service(lines, project_root, output_dir):
536578
def generate_compose(json_content, rmw, output_dir, project_root):
537579
"""Generate local_compose.yaml for validation on a development machine."""
538580
payload_size_default = DEFAULT_PAYLOAD_SIZE
539-
period_ms_default = _json_default_int(
540-
json_content, "period_ms", DEFAULT_PERIOD_MS)
581+
period_ms_default = DEFAULT_PERIOD_MS
541582
eval_time_default = DEFAULT_EVAL_TIME
542583
lines = ["services:"]
543584

@@ -568,8 +609,7 @@ def generate_compose(json_content, rmw, output_dir, project_root):
568609
def generate_compose_per_host(json_content, rmw, output_dir, project_root):
569610
"""Generate one host-specific host*_compose.yaml file per host."""
570611
payload_size_default = DEFAULT_PAYLOAD_SIZE
571-
period_ms_default = _json_default_int(
572-
json_content, "period_ms", DEFAULT_PERIOD_MS)
612+
period_ms_default = DEFAULT_PERIOD_MS
573613
eval_time_default = DEFAULT_EVAL_TIME
574614
for host_dict in json_content["hosts"]:
575615
host_name = host_dict["host_name"]
@@ -627,7 +667,7 @@ def _run_script_common_prefix(
627667
'',
628668
'Notes:',
629669
' --eval-time is applied to all nodes started via this script.',
630-
' Payload size and period are taken from topology JSON values.',
670+
' payload_size and period_ms must be set in each Publisher/Intermediate entry in topology JSON.',
631671
'EOF',
632672
'}',
633673
"",
@@ -678,8 +718,7 @@ def generate_host_run_scripts(json_content, output_dir, project_root):
678718
"""Generate host*_run.sh wrapper scripts for host-specific Compose files."""
679719
rel_root = os.path.relpath(project_root, output_dir)
680720
payload_size_default = DEFAULT_PAYLOAD_SIZE
681-
period_ms_default = _json_default_int(
682-
json_content, "period_ms", DEFAULT_PERIOD_MS)
721+
period_ms_default = DEFAULT_PERIOD_MS
683722
eval_time_default = DEFAULT_EVAL_TIME
684723
for host_dict in json_content["hosts"]:
685724
host_name = host_dict["host_name"]
@@ -731,7 +770,7 @@ def generate_local_run_script(json_content, rmw, output_dir, project_root):
731770
lines,
732771
rel_root,
733772
DEFAULT_PAYLOAD_SIZE,
734-
_json_default_int(json_content, "period_ms", DEFAULT_PERIOD_MS),
773+
DEFAULT_PERIOD_MS,
735774
DEFAULT_EVAL_TIME,
736775
)
737776
lines.extend(
@@ -943,6 +982,8 @@ def generate_metadata_file(
943982
with open(args.json_path, "r") as f:
944983
json_content = json.load(f)
945984

985+
_validate_payload_period_constraints(json_content)
986+
946987
# Generate into a temporary directory first, then swap it in after success.
947988
tmp_dir = output_dir + ".tmp"
948989
if os.path.exists(tmp_dir):

topology_example/README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ Notes:
4747
| Key | Required | Type | Description |
4848
|---|---|---|---|
4949
| topic_name | Required | string | Topic name to publish. |
50+
| payload_size | Required | number | Payload size (bytes). Must be a positive integer. |
51+
| period_ms | Required | number | Publish period (ms). Must be a positive integer. |
5052

5153
### Elements of the `subscriber` Array
5254

@@ -62,12 +64,13 @@ Notes:
6264
| subscriber | Required | array | Topic definitions for subscribed input topics. |
6365

6466
Each element of the `intermediate` array is an object that contains the `publisher` and `subscriber` arrays shown above. Elements inside those arrays use the same `topic_name` field described above.
67+
For `intermediate[].publisher[]`, `payload_size` and `period_ms` are also required.
6568

6669
## 3. Notes
6770

6871
The RMW implementation is selected with the command-line arguments to `generate_exec_scripts.py`. Defining it in the JSON file has no effect.
6972

70-
`--eval-time` can be passed to `_run.sh` or `_exec.sh` to override duration. `payload_size` and `period_ms` are taken directly from Publisher/Intermediate entries in the topology JSON and are not overridden by runtime script options.
73+
`--eval-time` can be passed to `_run.sh` or `_exec.sh` to override duration.
7174

7275
## 4. Minimal Template
7376

@@ -80,7 +83,11 @@ The RMW implementation is selected with the command-line arguments to `generate_
8083
{
8184
"node_name": "pub1",
8285
"publisher": [
83-
{ "topic_name": "topic_a" }
86+
{
87+
"topic_name": "topic_a",
88+
"payload_size": 64,
89+
"period_ms": 100
90+
}
8491
]
8592
}
8693
]
@@ -116,7 +123,11 @@ The RMW implementation is selected with the command-line arguments to `generate_
116123
{
117124
"node_name": "pub1",
118125
"publisher": [
119-
{ "topic_name": "topic_a" }
126+
{
127+
"topic_name": "topic_a",
128+
"payload_size": 64,
129+
"period_ms": 100
130+
}
120131
]
121132
}
122133
]

topology_example/simple.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"eval_time": 120,
3-
"period_ms": 50,
43
"qos": {
54
"history": "KEEP_LAST",
65
"depth": 10,

0 commit comments

Comments
 (0)