Skip to content

Fix/setup robustness and multi agent#29

Open
Ninjabippo1205 wants to merge 2 commits into
mainfrom
fix/setup-robustness-and-multi-agent
Open

Fix/setup robustness and multi agent#29
Ninjabippo1205 wants to merge 2 commits into
mainfrom
fix/setup-robustness-and-multi-agent

Conversation

@Ninjabippo1205

@Ninjabippo1205 Ninjabippo1205 commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Harden the setup path: any setup request the RAN agent cannot answer positively
    (undecodable bytes / wrong encoding, unexpected PDU type, response-encode failure)
    now gets an explicit, properly encoded SetupResponse with ResponseCode::NEGATIVE
    instead 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-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 receive a
    decodable negative SetupResponse and (2) a well-formed SetupRequest on the
    same 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, and
    dual-encoder builds.
  • Fix JSON-only CI: test_asn1_size and test_e2e_report_path hardcode ASN.1 and
    are now only built when LIBE3_ENABLE_ASN1=ON.

Per review: the empty-ranFunctionData padding was dropped (root cause tracked in
#30) and the per-interface SmRegistry change was split out (discussion in #31).

Type of change

  • Bug fix
  • New feature / enhancement
  • New Service Model
  • Refactor (no behavior change)
  • Documentation
  • Test / CI / packaging
  • Other (explain):

Linked issue

Closes #28

Mandatory test checklist

  • ./build_libe3 -c -d build -j $(nproc) -r -t passes (Release build + tests)
  • ./build_libe3 -c -d build -j $(nproc) -g -t passes (Debug build + tests)
  • cd build && ctest --output-on-failure is clean
  • MPMC queue benchmark (./build/test_bench_mpmc_queue) shows no regression vs main
  • VERSION bumped per SemVer if the public API or ABI changed — not needed: no
    public API/ABI change (main is already 0.0.6)
  • If public headers under include/ were touched, ./build_libe3 --docs renders
    without new Doxygen warnings
  • If new build dependencies were added, they are installed by ./build_libe3 -I
  • If the libe3.pc interface changed, downstream consumers still link cleanly

CI checklist

  • Unit Tests workflow is green (Debug + Release matrix on ubuntu-latest)
  • MPMC Queue Benchmark job has posted results to this PR with no regression

Twin-repo coordination

  • This PR does not change the E3 wire protocol or public ABI, OR a paired PR
    exists in each affected twin repo. (The negative SetupResponse is a standard
    E3AP PDU that dApp peers already decode; no wire-format change.)

Paired PR(s): none needed

Workflow confirmation

  • Commits will be squashed at merge time. Updates to this PR will be applied
    via rebase only — no merge commits, no duplicated history.
  • I have read and followed CONTRIBUTING.md.

@Ninjabippo1205 Ninjabippo1205 requested a review from Thecave3 as a code owner June 12, 2026 04:40
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

⏱️ Full-loop Latency Benchmark (commit b884c25)

Full-loop latency benchmark (N=1006 after 50 warmup)

All values in microseconds (us). Link: zmq, transport: ipc, encoding: ASN.1 APER.

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.

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

🔄 E2E dApp Integration Results (commit b884c25)

posix/ipc

  • dApp exit: 0
  • Indications received: 7

posix/tcp

  • dApp exit: 0
  • Indications received: 7

zmq/ipc

  • dApp exit: 0
  • Indications received: 7

zmq/tcp

  • dApp exit: 0
  • Indications received: 7

example_simple_agent + example_simple_dapp on ubuntu-latest, Release build

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

🔀 E2E Topologies — multi-dApp / multi-RAN (commit b884c25)

zmq/ipc

  • 1 RAN - 1 dApp: indications=5
    • dapp peer=t11 ran=ran-solo sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0.4 max=1 @seq=3) hist[<=1:5 2-5:0 6-10:0 >10:0]
  • 1 RAN - 2 dApps: dApp#1 ind=5 sub=1, dApp#2 ind=6 sub=2, RAN saw 2 dApps
    • dapp1 peer=t12 ran=ran-shared sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0.4 max=1 @seq=3) hist[<=1:5 2-5:0 6-10:0 >10:0]
    • dapp2 peer=t12 ran=ran-shared sub=2 indications=6 seq=[0..5] dropped=0 (0%) age_ms(avg=0.333333 max=1 @seq=3) hist[<=1:6 2-5:0 6-10:0 >10:0]
  • 2 RANs - 1 dApp: from ran-a ind=5, from ran-b ind=5
    • dapp peer=t2a ran=ran-a sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0.6 max=1 @seq=1) hist[<=1:5 2-5:0 6-10:0 >10:0]
    • dapp peer=t2b ran=ran-b sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0.6 max=1 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]

zmq/tcp

  • 1 RAN - 1 dApp: indications=5
    • dapp peer=default ran=ran-solo sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0.2 max=1 @seq=4) hist[<=1:5 2-5:0 6-10:0 >10:0]
  • 1 RAN - 2 dApps: dApp#1 ind=5 sub=1, dApp#2 ind=6 sub=2, RAN saw 2 dApps
    • dapp1 peer=default ran=ran-shared sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0 max=0 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]
    • dapp2 peer=default ran=ran-shared sub=2 indications=6 seq=[0..5] dropped=0 (0%) age_ms(avg=0 max=0 @seq=0) hist[<=1:6 2-5:0 6-10:0 >10:0]
  • 2 RANs - 1 dApp: from ran-a ind=5, from ran-b ind=5
    • dapp peer=default ran=ran-a sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0.8 max=1 @seq=1) hist[<=1:5 2-5:0 6-10:0 >10:0]
    • dapp peer=off100 ran=ran-b sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0.4 max=1 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]

example_simple_agent + example_simple_dapp on ubuntu-latest, Release. Indication age is report-only.

@Thecave3

Copy link
Copy Markdown
Collaborator

let's use this branch to work exclusively on the fix

@Thecave3 Thecave3 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@Ninjabippo1205 Ninjabippo1205 force-pushed the fix/setup-robustness-and-multi-agent branch from 03e89ea to b884c25 Compare July 3, 2026 19:41
@Ninjabippo1205

Copy link
Copy Markdown
Contributor Author

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.

@Ninjabippo1205 Ninjabippo1205 requested a review from Thecave3 July 3, 2026 19:49
Comment thread src/core/e3_interface.cpp
@@ -640,9 +646,11 @@ void E3Interface::handle_setup_request(const SetupRequest& request, uint32_t req

if (!encode_result) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this happens then there's a bug on our end, so probably the solution is abort

Comment thread src/core/e3_interface.cpp
}
}

void E3Interface::send_negative_setup_reply(uint32_t request_id) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's have every new function documented in such a way that is picked up by the documentation.

Comment thread src/core/e3_interface.cpp
config_.e3ap_version,
std::nullopt, // no dApp identifier assigned
config_.ran_identifier.empty() ? "unknown" : config_.ran_identifier,
{} // no RAN functions advertised

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think probably #30 outcome is that this should be null, thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Malformed or wrong-encoding setup request permanently wedges the agent's setup socket

2 participants