You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We have no load test for udp_bridge. The 2026-04-21 BizzyBoat
deployment surfaced failure modes — resend amplification (#9),
bridge wedge (#10), Starlink-uplink saturation, and stats-path
stall during Starlink-only ops (#20) — that we currently can't
reproduce off-boat. PR #16's test_connection_rate_limit covers
one narrow correctness invariant; nothing exercises the field
shape: sustained rate, multi-topic, with impairment.
Even a crude bench harness would have caught these. The failures
aren't Starlink-specific subtleties — they're "this code path was
never exercised at sustained rate with any loss." The field was
the first environment to push it hard.
The legacy mininet scripts in udp_bridge/test/mininet/ are ROS 1
and non-functional on Jazzy.
MVP scope: range-degradation scenario
One scenario, shipped first. Linux netns + tc qdisc netem, no
sudo (unshare -rn). Smoke runs under colcon test on every
build; full scenario is pytest-discovered but @pytest.mark.skipif'd unless UDP_BRIDGE_BENCH_SCENARIOS=1 is
set (standard opt-in pattern).
Topology
Three concurrent veth-pair-connected netns paths from day one:
Path
Role
MVP profile
WiFi
Primary, short-range, ranged
Walked through the trajectory below
Cell
Always-on, low-bandwidth
5 Mbit / 1% loss / 60 ms / 20 ms jitter (constant)
Starlink
Always-on, medium-bandwidth
8 Mbit / 0.4% loss / 40 ms / 30 ms jitter (constant)
Cell and Starlink baselines drawn from the 2026-05-18 pier link
calibration; characteristic, not calibrated. The MVP walks only
the WiFi path through the trajectory below; cell and Starlink
are present and forwarding so cross-path interactions are
observable. Per-path profile walking is a follow-up refinement.
Test topic mix
Pub/sub scripts in the boat netns publish a representative mix.
Each path's Connection topics_list carries a subset, mirroring
how operational traffic is currently split (pre-#19):
Tier
Carried by
Example topics
Aggregate rate
Critical
WiFi + Cell + Starlink (replicated)
heartbeat, mission_state
~0.1 Mbit/s
Telemetry
WiFi + Starlink
odom, nav_status, cmd_vel ack
~0.5 Mbit/s
Bulk
WiFi only
one camera stream, viz markers
up to WiFi link cap
Replication on multiple paths uses today's mechanism (same topic
appearing in multiple topics_list entries — the "Pattern A"
workaround documented in #19). When #19 lands, equivalent
coverage is expressed via topic-priority routing on a single
per-host Connection instead of replicated Connections, and the
mix is restated in those terms.
Mix configured via bizzyboat.yaml-style fragments under test/bench/configs/.
Trajectory
Drive the WiFi link through a sail-out-and-return trajectory using
an orchestrator script that issues periodic tc qdisc change on
the boat-side veth (cell and Starlink remain at the constant
profiles above). Phases held N seconds each:
Phase
rate
loss
delay
jitter
In-range clean
30 Mbit
0%
5 ms
1 ms
Fringe
10 Mbit
0.5%
15 ms
5 ms
Lossy
3 Mbit
3%
40 ms
15 ms
Critical
500 Kbit
10%
100 ms
30 ms
Over-horizon
drop
100%
—
—
Recovery
mirror trajectory back to in-range clean
Trajectory shape informed by the 2026-04-21 BizzyBoat range
observations and 2026-05-18 pier link calibration; coarse, not
calibrated to specific bags.
Why this scenario as MVP: it exposes resend amplification,
wedge, and rate-limit overshoot together under the conditions
that produced them in the field. It exercises transitions —
especially the over-horizon → recovery edge — which is where the
operationally critical failures live.
Invariants over the run (thresholds documented in the
per-scenario README):
Recv-Q never climbs monotonically for more than N seconds in
any phase, including the over-horizon → recovery edge.
No ERROR-severity log emitted during the over-horizon window
(graceful disconnect is a mode, not a fault).
After the over-horizon → in-range edge, DataRates.success_bytes_per_second returns to within X% of
in-range baseline within T seconds.
On the recovery leg, resend/tx_ok ratio does not exceed the
instantaneous loss rate by more than F×.
Forwarding resumes without bridge restart after the over-horizon
window ends.
(multi-link) Each path's observed forwarded topics ⊆ its
configured topics_list. Any unauthorized topic on a path is
a fault (catches mis-routing).
(multi-link) During WiFi-path degradation, cell-path and
Starlink-path success_bytes_per_second for tier-Critical
(and, for Starlink, tier-Telemetry) topics are unaffected
within Y%. WiFi saturation must not poison the other paths.
(multi-link) Operator-side subscribers for tier-Critical
topics observe no gap longer than G seconds during the
over-horizon window. Cell/Starlink replicas keep them alive.
X, T (recovery completeness, speed) — from 2026-04-21
BizzyBoat range bags where the boat returned to range. X is
the observed DataRates ratio post-recovery vs. pre-event
baseline; T is the observed convergence time.
F (resend amplification ceiling) — anchor between the
2026-05-01 pathological case (resend_dropped_bps ~13 MB/s on
a 1.0 MB/s VPN, i.e. 13× ceiling) and the 2026-05-18 healthy
baseline (~30% structural overhead on Starlink/VPN). Pick F
well below pathological — likely 2× — and document the choice.
Y (cross-path poisoning tolerance) — no field bag isolates
this cleanly (2026-05-01 was Starlink-only). Initial Y is
bootstrapped from the MVP harness itself (cell rate with WiFi
idle as a reference), and revised once a deployment captures
WiFi-saturation-with-cell-up-and-running.
G (Critical-topic gap tolerance during over-horizon) — from
2026-05-01 bags where heartbeat was carried over Starlink
alone. G is 2× the observed worst-case inter-arrival in the
non-degraded portion of that bag.
Sets up N veth-pair-connected netns paths via unshare -rn
(MVP wires three: WiFi, cell, Starlink).
Applies independent tc qdisc netem per path; orchestrator
walks WiFi-path parameters per the phase table, holds cell
and Starlink at constant baselines.
Spawns the bridge + scripted pub/sub in each netns.
Records bridge_info + recv_q_trace for the run window.
Runs assertions on the captured bag.
Reuse bag_analysis/extractors/udp_bridge.py (in marine_tools)
for parsing — same code path as on-boat bag analysis.
Smoke and scenarios registered as pytest tests under test/bench/. Smoke (clean link, 10s baseline pub/sub, asserts
non-zero success_bytes_per_second) runs on every colcon test. Full scenario opt-in via UDP_BRIDGE_BENCH_SCENARIOS=1. If unshare -rn is unavailable
in the test environment, both skip with a clear reason rather
than failing.
Plan-task verifies unshare -rn + tc qdisc work in the
workspace's CI runner before merging the MVP.
Target shape: follow-up scenarios
These remain the eventual library; each is filed as its own
follow-up issue after the MVP lands. They are not part of #18:
Resend amplification reproducer — clean baseline link +
scripted brief outages. Assert resend/tx_ok ratio approaches
actual loss rate; rx_duplicate/rx approaches 0 once Resend loop amplifies traffic rather than tracking loss #9's
debounce + backoff land.
Wedge reproducer — high-rate publisher + remote subscriber
killed mid-stream. Assert Recv-Q does NOT monotonically climb
past N seconds.
Rate-limit accuracy under load — multiple high-rate
forwarding subscriptions on one Connection with a known cap.
Assert achieved success bytes/s is within a band of the cap.
(Subsumed) Pure saturation behavior is exercised by the
Critical and Lossy phases of the MVP scenario; a dedicated
saturation scenario may not be needed.
Out of scope
ROS 2 port of the legacy mininet .bash / .launch
scaffolding (netns is simpler; mininet adds an abandoned
dependency).
Calibration of phase parameters to specific field bags. This is
a v2 refinement for tuning fixes; not required for exposing
bugs. The MVP only needs phase parameters in the right ballpark.
Per-path walked impairment profiles. MVP walks only the WiFi
path; cell and Starlink are constant. Walking all three
simultaneously (e.g. weather-degraded Starlink, terrain-shadowed
cell) is a follow-up.
Context
We have no load test for
udp_bridge. The 2026-04-21 BizzyBoatdeployment surfaced failure modes — resend amplification (#9),
bridge wedge (#10), Starlink-uplink saturation, and stats-path
stall during Starlink-only ops (#20) — that we currently can't
reproduce off-boat. PR #16's
test_connection_rate_limitcoversone narrow correctness invariant; nothing exercises the field
shape: sustained rate, multi-topic, with impairment.
Even a crude bench harness would have caught these. The failures
aren't Starlink-specific subtleties — they're "this code path was
never exercised at sustained rate with any loss." The field was
the first environment to push it hard.
The legacy mininet scripts in
udp_bridge/test/mininet/are ROS 1and non-functional on Jazzy.
MVP scope: range-degradation scenario
One scenario, shipped first. Linux netns +
tc qdisc netem, nosudo (
unshare -rn). Smoke runs undercolcon teston everybuild; full scenario is pytest-discovered but
@pytest.mark.skipif'd unlessUDP_BRIDGE_BENCH_SCENARIOS=1isset (standard opt-in pattern).
Topology
Three concurrent veth-pair-connected netns paths from day one:
Cell and Starlink baselines drawn from the 2026-05-18 pier link
calibration; characteristic, not calibrated. The MVP walks only
the WiFi path through the trajectory below; cell and Starlink
are present and forwarding so cross-path interactions are
observable. Per-path profile walking is a follow-up refinement.
Test topic mix
Pub/sub scripts in the boat netns publish a representative mix.
Each path's Connection
topics_listcarries a subset, mirroringhow operational traffic is currently split (pre-#19):
Replication on multiple paths uses today's mechanism (same topic
appearing in multiple
topics_listentries — the "Pattern A"workaround documented in #19). When #19 lands, equivalent
coverage is expressed via topic-priority routing on a single
per-host Connection instead of replicated Connections, and the
mix is restated in those terms.
Mix configured via
bizzyboat.yaml-style fragments undertest/bench/configs/.Trajectory
Drive the WiFi link through a sail-out-and-return trajectory using
an orchestrator script that issues periodic
tc qdisc changeonthe boat-side veth (cell and Starlink remain at the constant
profiles above). Phases held N seconds each:
Trajectory shape informed by the 2026-04-21 BizzyBoat range
observations and 2026-05-18 pier link calibration; coarse, not
calibrated to specific bags.
Why this scenario as MVP: it exposes resend amplification,
wedge, and rate-limit overshoot together under the conditions
that produced them in the field. It exercises transitions —
especially the over-horizon → recovery edge — which is where the
operationally critical failures live.
Invariants over the run (thresholds documented in the
per-scenario README):
Recv-Qnever climbs monotonically for more than N seconds inany phase, including the over-horizon → recovery edge.
(graceful disconnect is a mode, not a fault).
DataRates.success_bytes_per_secondreturns to within X% ofin-range baseline within T seconds.
resend/tx_okratio does not exceed theinstantaneous loss rate by more than F×.
window ends.
bridge_infoandtopic_statisticspublication rates on bothsides remain ≥ R% of in-range baseline throughout the
over-horizon and recovery phases. (Catches Stats-timer publication stalls during Starlink-only ops while data-republish keeps working #20-class stalls in
the stats path while the data plane keeps working.)
configured
topics_list. Any unauthorized topic on a path isa fault (catches mis-routing).
Starlink-path
success_bytes_per_secondfor tier-Critical(and, for Starlink, tier-Telemetry) topics are unaffected
within Y%. WiFi saturation must not poison the other paths.
topics observe no gap longer than G seconds during the
over-horizon window. Cell/Starlink replicas keep them alive.
drop fraction by tier is monotonic: Bulk drops first, then
Telemetry, then Critical. Pre-Add per-topic priority/class scheduling to the rate-limiter #19, the MVP measures and reports
this without failing on it (FIFO drop is the known-current
behavior; the test documents the gap rather than enforcing the
fix that doesn't exist yet).
Threshold sourcing
Each placeholder is replaced with a concrete value during
plan-task, drawn from the named bag(s):in
~/data/logs/analysis/. Pick N as 2× the longest observedtransient flush in a healthy recovery, not a wedge.
BizzyBoat range bags where the boat returned to range. X is
the observed DataRates ratio post-recovery vs. pre-event
baseline; T is the observed convergence time.
2026-05-01 pathological case (
resend_dropped_bps~13 MB/s ona 1.0 MB/s VPN, i.e. 13× ceiling) and the 2026-05-18 healthy
baseline (~30% structural overhead on Starlink/VPN). Pick F
well below pathological — likely 2× — and document the choice.
this cleanly (2026-05-01 was Starlink-only). Initial Y is
bootstrapped from the MVP harness itself (cell rate with WiFi
idle as a reference), and revised once a deployment captures
WiFi-saturation-with-cell-up-and-running.
2026-05-01 bags where heartbeat was carried over Starlink
alone. G is 2× the observed worst-case inter-arrival in the
non-degraded portion of that bag.
— from 2026-05-19 bags (Stats-timer publication stalls during Starlink-only ops while data-republish keeps working #20). R is set such that a sustained
drop to 0% (as observed in Stats-timer publication stalls during Starlink-only ops while data-republish keeps working #20) trips the invariant, while
brief transients (one missed publication per node) do not.
Initial value: 50% of baseline averaged over a 10s window.
Plan-task extracts the concrete values from these bags and lands
them in the per-scenario README before implementation begins.
Implementation sketch
test/bench/run_scenario.py) that:unshare -rn(MVP wires three: WiFi, cell, Starlink).
tc qdisc netemper path; orchestratorwalks WiFi-path parameters per the phase table, holds cell
and Starlink at constant baselines.
bridge_info+recv_q_tracefor the run window.bag_analysis/extractors/udp_bridge.py(inmarine_tools)for parsing — same code path as on-boat bag analysis.
test/bench/. Smoke (clean link, 10s baseline pub/sub, assertsnon-zero
success_bytes_per_second) runs on everycolcon test. Full scenario opt-in viaUDP_BRIDGE_BENCH_SCENARIOS=1. Ifunshare -rnis unavailablein the test environment, both skip with a clear reason rather
than failing.
unshare -rn+tc qdiscwork in theworkspace's CI runner before merging the MVP.
Target shape: follow-up scenarios
These remain the eventual library; each is filed as its own
follow-up issue after the MVP lands. They are not part of #18:
scripted brief outages. Assert
resend/tx_okratio approachesactual loss rate;
rx_duplicate/rxapproaches 0 once Resend loop amplifies traffic rather than tracking loss #9'sdebounce + backoff land.
killed mid-stream. Assert
Recv-Qdoes NOT monotonically climbpast N seconds.
forwarding subscriptions on one Connection with a known cap.
Assert achieved success bytes/s is within a band of the cap.
Critical and Lossy phases of the MVP scenario; a dedicated
saturation scenario may not be needed.
Out of scope
.bash/.launchscaffolding (netns is simpler; mininet adds an abandoned
dependency).
a v2 refinement for tuning fixes; not required for exposing
bugs. The MVP only needs phase parameters in the right ballpark.
path; cell and Starlink are constant. Walking all three
simultaneously (e.g. weather-degraded Starlink, terrain-shadowed
cell) is a follow-up.
Why now
share the property "would have been caught by basic load
testing." The cost of going to the boat to validate each
candidate fix for Resend loop amplifies traffic rather than tracking loss #9 is the test gap, expressed in deployment
hours.
setup. The MVP scenario provides that on the recovery leg.
Authored-By:
Claude Code AgentModel:
Claude Opus 4.7 (1M context)