Fix/setup robustness and multi agent#29
Conversation
⏱️ Full-loop Latency Benchmark (commit
|
| Phase | mean | p50 | p99 | max |
|---|---|---|---|---|
| 1. Collect indication data | 0 | 0 | 0 | 10 |
| 2. Create & encode indication | 0 | 0 | 1 | 16 |
| 3. Deliver indication (RAN -> dApp) | 108 | 105 | 190 | 215 |
| 4. Decode indication | 0 | 0 | 1 | 15 |
| 5. Process data | 0 | 0 | 0 | 0 |
| 6. Create & encode control | 0 | 0 | 0 | 11 |
| 7. Deliver control (dApp -> RAN) | 120 | 110 | 209 | 617 |
| 8. Decode & handle control | 0 | 0 | 0 | 14 |
| Total round-trip | 231 | 195 | 350 | 698 |
Benchmarked on
ubuntu-latest, Release build, ZMQ + IPC, ASN.1 APER.
🔄 E2E dApp Integration Results (commit
|
🔀 E2E Topologies — multi-dApp / multi-RAN (commit
|
|
let's use this branch to work exclusively on the fix |
Thecave3
left a comment
There was a problem hiding this comment.
Thanks for the thorough writeup and the regression tests — the diagnosis in #28 is spot-on and the test harness is genuinely good. I'd like to split this before merge, though: #28 is only the setup-socket wedge, and the PR currently bundles two unrelated changes under it. Concretely:
Fix 1 (setup wedge) — keep, but reply with a real negative SetupResponse, not an empty frame. Our encoding is fixed and known a priori by every peer, so on a malformed/undecodable request we can always encode a proper SetupResponse with ResponseCode::NEGATIVE. When the bytes don't decode we don't have the original request_id — just default it (0 / next-available), since the dApp's lazy-pirate loop doesn't correlate replies by id anyway. That removes the need for the empty-frame helper entirely: the peer gets an explicit rejection instead of a silent timeout. test_setup_bad_request should then assert it receives a decodable negative response, not just n >= 0.
Fix 2 (0x00 padding) — please drop this. ranFunctionData being mandatory SIZE(1..32768) is correct; the issue is that an SM should never advertise empty data in the first place (a real SM always carries at least its name — see simple_agent / e3sm_simple), and ranFunctionList is OPTIONAL, so "nothing to advertise" means omitting the entry, not padding it. The only way an empty value reaches the encoder today is the C API's ran_function_data "may be NULL / len 0" contract, which directly contradicts the schema. Padding hides that mismatch (and makes a decoded {0x00} indistinguishable from real data). Tracked as #30 — let's fix it at the source there instead.
Fix 3 (per-interface SmRegistry) — let's pull this into its own PR. It's a structural change to a public header and ~10 call sites, unrelated to #28, and it carries a behavioral change (SMs destroyed without their destroy() hook if stop() isn't called). The multi-agent-in-one-process motivation is reasonable but deserves its own issue and review on its own merits — opened #31 to discuss whether/why we want to support that configuration. (For what it's worth, no external consumer references SmRegistry::instance(), so the API break is internal-only — splitting it costs nothing downstream.) test_multi_agent_registry moves with it.
The ASN.1-only CI gating (LIBE3_ENABLE_ASN1 test exclusion) is a clean, standalone improvement — happy to see that land either here or as its own tiny PR.
So: this PR becomes Fix 1 (negative-response variant) + test_setup_bad_request, closing #28; Fixes 2 and 3 each get their own issue/PR (#30, #31). Thanks again!
test_asn1_size drives the APER encoder directly and test_e2e_report_path hardcodes EncodingFormat::ASN1 for both the agent under test and its fake-dApp peer. On a JSON-only build (-DLIBE3_ENABLE_ASN1=OFF) the encoder factory returns nullptr for ASN.1, so both tests fail with 'encoder unavailable' (-12) rather than telling us anything about the build. Exclude the two targets at configure time when ASN.1 support is compiled out, mirroring the existing test_json_encoder skip. The tests themselves are unchanged, so ASN.1-enabled builds keep full coverage.
The RAN side of the setup channel is a ZMQ REP socket: it must send exactly one reply per received request before it can receive again. Bailing out of the setup loop without replying (undecodable bytes, wrong PDU type, response-encode failure) left the socket stuck in the send state, wedging dApp onboarding for every subsequent peer until the agent restarted. Every such path now answers with a properly encoded SetupResponse carrying ResponseCode::NEGATIVE: the encoding is fixed and known a priori by both peers, so an explicit rejection can always be produced (no empty-frame fallback needed). When the request bytes never decoded the original request id is unrecoverable; a fresh message id is substituted (E3-MessageID is INTEGER (1..1000), so 0 is not encodable) — dApps do not correlate setup replies by id. Regression test: test_setup_bad_request drives the setup channel of a real E3Agent with a raw ZMQ REQ peer and asserts (1) garbage bytes get a decodable negative SetupResponse and (2) a well-formed SetupRequest on the same channel still succeeds afterwards. Verified to fail on unfixed main (the recv in (1) times out). Closes #28
03e89ea to
b884c25
Compare
|
Thanks for the review. All three points addressed; the branch is now rebased on latest main and reduced to Fix 1 + the CI gating. Fix 1: reworked to an explicit negative SetupResponse. Every path that can't answer positively (undecodable bytes, wrong PDU type, response-encode failure) now encodes and sends a real SetupResponse with ResponseCode::NEGATIVE; the empty-frame helper is gone. One deviation from "default the id to 0": E3-MessageID is INTEGER (1..1000) in the grammar, so 0 is not APER-encodable, when the original id is unrecoverable the agent substitutes a freshly generated one (your "next-available" option). test_setup_bad_request now asserts the reply decodes to a negative SetupResponse (and that a well-formed setup on the same channel still succeeds afterwards); re-verified to fail on unfixed main. The response-encode-failure path in handle_setup_request is exactly where the empty-ranFunctionData case (#30) bites, and the negative reply covers it cleanly, ranFunctionList is OPTIONAL, so a bare negative response always encodes. The peer gets an explicit rejection while #30 fixes the root cause at the C API boundary. Fix 2: dropped as requested; nothing of it remains in this PR. Fix 3: split out to its own branch (fix/per-interface-sm-registry) with test_multi_agent_registry; I'll open the PR against #31 once that discussion settles. Your lifecycle concern is addressed there: ~SmRegistry now runs clear() (stop-if-running, then destroy()) so the hooks fire on every teardown path, including an agent destroyed without a prior stop(), with a regression test for exactly that case. |
| @@ -640,9 +646,11 @@ void E3Interface::handle_setup_request(const SetupRequest& request, uint32_t req | |||
|
|
|||
| if (!encode_result) { | |||
There was a problem hiding this comment.
If this happens then there's a bug on our end, so probably the solution is abort
| } | ||
| } | ||
|
|
||
| void E3Interface::send_negative_setup_reply(uint32_t request_id) { |
There was a problem hiding this comment.
Let's have every new function documented in such a way that is picked up by the documentation.
| config_.e3ap_version, | ||
| std::nullopt, // no dApp identifier assigned | ||
| config_.ran_identifier.empty() ? "unknown" : config_.ran_identifier, | ||
| {} // no RAN functions advertised |
There was a problem hiding this comment.
I think probably #30 outcome is that this should be null, thoughts?
Summary
(undecodable bytes / wrong encoding, unexpected PDU type, response-encode failure)
now gets an explicit, properly encoded
SetupResponsewithResponseCode::NEGATIVEinstead of no reply at all. Previously the agent bailed out of the setup loop
without completing the ZMQ REQ/REP exchange, leaving the REP socket wedged — one
bad client disabled dApp onboarding until the agent/gNB was restarted. When the
request bytes never decoded the original request id is unrecoverable; a fresh
message id is substituted (
E3-MessageIDisINTEGER (1..1000), so 0 is notencodable) — dApps do not correlate setup replies by id.
test_setup_bad_request: drives the setup channel of a realE3Agentwith a raw ZMQ REQ peer and asserts (1) garbage bytes receive adecodable negative SetupResponse and (2) a well-formed
SetupRequeston thesame channel still receives a positive response afterwards. Verified to fail on
unfixed
main(the recv in (1) times out). Runs on ASN.1-only, JSON-only, anddual-encoder builds.
test_asn1_sizeandtest_e2e_report_pathhardcode ASN.1 andare now only built when
LIBE3_ENABLE_ASN1=ON.Per review: the empty-
ranFunctionDatapadding was dropped (root cause tracked in#30) and the per-interface
SmRegistrychange was split out (discussion in #31).Type of change
Linked issue
Closes #28
Mandatory test checklist
./build_libe3 -c -d build -j $(nproc) -r -tpasses (Release build + tests)./build_libe3 -c -d build -j $(nproc) -g -tpasses (Debug build + tests)cd build && ctest --output-on-failureis clean./build/test_bench_mpmc_queue) shows no regression vsmainVERSIONbumped per SemVer if the public API or ABI changed — not needed: nopublic API/ABI change (
mainis already 0.0.6)include/were touched,./build_libe3 --docsrenderswithout new Doxygen warnings
./build_libe3 -Ilibe3.pcinterface changed, downstream consumers still link cleanlyCI checklist
Unit Testsworkflow is green (Debug + Release matrix onubuntu-latest)MPMC Queue Benchmarkjob has posted results to this PR with no regressionTwin-repo coordination
exists in each affected twin repo. (The negative
SetupResponseis a standardE3AP PDU that dApp peers already decode; no wire-format change.)
Paired PR(s): none needed
Workflow confirmation
via rebase only — no merge commits, no duplicated history.
CONTRIBUTING.md.