Skip to content

e3ap: surface assigned message ids for dApp sends and relayed xApp control - #72

Merged
Thecave3 merged 2 commits into
mainfrom
67-68-message-id-correlation
Aug 27, 2026
Merged

e3ap: surface assigned message ids for dApp sends and relayed xApp control#72
Thecave3 merged 2 commits into
mainfrom
67-68-message-id-correlation

Conversation

@Thecave3

Copy link
Copy Markdown
Collaborator

Closes #67. Closes #68.

Based on top of feat/latency-tracing (open, unmerged) at the author's request, since that branch is expected to land first.

Both issues are correlation gaps around E3-MessageID that already exists internally but never reaches the API surface. No E3AP wire/ASN.1/JSON/Protobuf grammar changes in either case.

#67send_control()/send_report() (E3Agent + E3Interface) gain a trailing uint32_t* out_message_id = nullptr, mirroring subscribe()'s existing out_request_id. The id was already computed internally before queueing; this just plumbs it out. Fully source-compatible — no existing caller needs to change, and the ErrorCode return is unchanged in both the success and failure case.

#68XAppControlAction gains a decode-time-only message_id field, set by the dApp inbound loop from the wrapping E3-PDU's id (already decoded into Pdu::message_id, just never forwarded). This corrects the issue's own follow-up note, which claimed "either [fix] is source-breaking for existing handler registrations" — that's true of the alternative (changing XAppControlHandler's signature), but not of the struct-field approach implemented here: it's ABI-breaking (struct layout changes) but source-compatible for all four existing registrations (examples/simple_dapp.cpp, swig/e3_dapp_session.cpp, tests/integration/bench_latrec_load.cpp, plus the header/setter declarations) — none construct or destructure the struct positionally.

SWIG binding follows both fixes through to Python: send_control()/send_report() now return the assigned message id (positive, 1..1000) on success instead of always 0, matching subscribe()'s existing convention; E3_EVENT_XAPP_CONTROL events now carry the id in the existing request_id field. This is a Python-visible behavior change — updating the dApp library's outbound loop (which currently checks rc != SUCCESS for these two calls) is left as a follow-up in that repo, not done here.

Tests: extended tests/integration/test_role_pair_zmq_ipc.cpp's existing RAN+dApp round trip — asserts send_control's out_message_id matches the id the RAN sees as request_message_id, and that two RAN → dApp xApp control relays deliver distinct, nonzero message ids to the dApp handler.

VERSION: 0.1.1 → 0.1.2 (patch, same class of change as 1ad9ced). feat/latency-tracing already carries 0.1.1; this bumps past it since that branch is expected to merge first.

Local verification (both with --enable-swig and integration tests on, rebased on feat/latency-tracing so LIBE3_ENABLE_LATREC is exercised throughout):

  • ./build_libe3 -c -r -t — Release + tests: 28/28 passed
  • ./build_libe3 -c -g -t — Debug + tests: 28/28 passed
  • python3 scripts/check_commit_trailers.py --base origin/feat/latency-tracing --head HEAD — passes
  • Branch is a linear, fast-forward-able descendant of feat/latency-tracing, no merge commits

Happy to split further or adjust scope if either fix should land separately.

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

CI report — 92130a1 — ✅ all checks passed

Workflow Result Time Run
Commit policy ✅ success 2m11s #91
E2E dApp Integration ✅ success 3m02s #106
E2E Topologies (multi-dApp / multi-RAN) ✅ success 2m29s #104
Full-loop Latency Benchmark ✅ success 1m11s #105
Unit Tests ✅ success 4m39s #132
latrec portability ⏭️ not triggered (paths filter)
MPMC Queue Benchmark ⏭️ not triggered (paths filter)
E2E Topologies (multi-dApp / multi-RAN)

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=0) 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=1 max=1 @seq=0) 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=1 max=1 @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=t2a ran=ran-a sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0.2 max=1 @seq=0) 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 max=0 @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.8 max=1 @seq=1) 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 max=0 @seq=0) 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 max=0 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]
E2E dApp Integration

✅ 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
Full-loop Latency Benchmark

Full-loop latency

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

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

# Description Tags mean p50 p99 max
1 Collect indication data RECORD_BEGIN to ENCODE_E3SM_BEGIN 0.11 0.10 0.26 2.37
2 Create & encode indication ENCODE_E3SM_BEGIN to ENCODE_E3SM_DONE 0.94 0.89 1.46 18.96
3 Encode E3AP (indication) EMIT_ENTER to ENQUEUE, then DEQUEUE to ENCODE_E3AP_DONE 3.00 3.10 5.54 10.19
4 Queuing (indication) ENQUEUE to DEQUEUE 7.00 7.54 17.82 57.73
5 Delivery (indication) ENCODE_E3AP_DONE to SEND_DONE 0.91 0.26 24.48 40.95
6 E3 wire (RAN -> dApp) SEND_DONE to RECV 108830348517511.97 52.20 75.06 18446744073709548.00
7 Decode E3AP (indication) RECV to DECODE_E3AP_DONE 1.85 1.77 3.81 16.76
8 libe3 dispatch (indication) DECODE_E3AP_DONE to DELIVER_BEGIN 0.13 0.13 0.18 0.22
9 Decode indication DELIVER_BEGIN to DECODE_E3SM_DONE 0.63 0.56 0.92 2.08
10 Process data DECODE_E3SM_DONE to ENCODE_E3SM_BEGIN 0.05 0.05 0.06 0.09
11 Create & encode control ENCODE_E3SM_BEGIN to ENCODE_E3SM_DONE 0.32 0.32 0.47 1.76
12 Encode E3AP (control) EMIT_ENTER to ENQUEUE, then DEQUEUE to ENCODE_E3AP_DONE 3.75 3.56 6.12 6.41
13 Queuing (control) ENQUEUE to DEQUEUE 16.11 15.94 27.21 56.45
14 Delivery (control) ENCODE_E3AP_DONE to SEND_DONE 5.71 5.27 10.61 21.70
15 E3 wire (dApp -> RAN) SEND_DONE to RECV 50.90 50.66 67.93 111.68
16 Decode E3AP (control) RECV to DECODE_E3AP_DONE 2.97 2.87 6.67 15.73
17 libe3 dispatch (control) DECODE_E3AP_DONE to DECODE_E3SM_BEGIN 0.32 0.31 0.53 0.64
18 Decode & handle control DECODE_E3SM_BEGIN to DECODE_E3SM_DONE 0.43 0.40 0.63 0.80
Total Total round-trip 147.11 146.66 195.06 892.41

ubuntu-latest, Release build, ZMQ + IPC, ASN.1 APER.

These numbers are measured inside a GitHub Actions container and should be treated as an upper bound on E3AP's and the library's own latency, not a representative deployment measurement.

Ready to merge (fast-forward only)

A maintainer can land the reviewed commits with:

git fetch origin
git checkout main && git merge --ff-only 92130a1273ddd670c5bd265b1fa2daf35e35281f && git push origin main

Head: 92130a1273ddd670c5bd265b1fa2daf35e35281f (branch 67-68-message-id-correlation). If --ff-only fails as non-fast-forward, the branch must be rebased on the latest main.

One comment per PR, rewritten in place once every workflow for 92130a1 finished.

…App control

Closes #67 and #68: both are correlation gaps around E3-MessageID that
already exists internally but never reaches the API surface.

- send_control()/send_report() (E3Agent + E3Interface) gain a trailing
  uint32_t* out_message_id = nullptr, mirroring subscribe()'s existing
  out_request_id. The generated id was already computed before queueing;
  this only plumbs it out. Source-compatible: no existing caller needs to
  change, and the ErrorCode return is unchanged in both the success and
  failure case.
- XAppControlAction gains a decode-time-only message_id field, set by the
  dApp inbound loop from the wrapping E3-PDU's id (already decoded into
  Pdu::message_id, never forwarded before). No ASN.1/JSON/Protobuf grammar
  changes — the id was always on the wire, just not threaded through the
  C++ handler call. This is ABI-breaking (struct layout changes) but not
  source-breaking for any of the four existing XAppControlHandler
  registrations (examples/simple_dapp.cpp, swig/e3_dapp_session.cpp,
  tests/integration/bench_latrec_load.cpp, plus the header/setter
  declarations) — none construct or destructure the struct positionally.

The SWIG binding follows both fixes through to Python: send_control()/
send_report() now return the assigned message id (positive, 1..1000) on
success instead of always 0, matching subscribe()'s existing convention;
E3_EVENT_XAPP_CONTROL events now carry the id in the existing request_id
field. This is a Python-visible behavior change — updating the dApp
library's outbound loop (which currently checks `rc != SUCCESS` for these
two calls) is left as a follow-up in that repo.

Extends tests/integration/test_role_pair_zmq_ipc.cpp's existing RAN+dApp
round trip: asserts send_control's out_message_id matches the id the RAN
sees as request_message_id, and that two RAN -> dApp xApp control relays
deliver distinct, nonzero message ids to the dApp handler.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Andrea Lacava <a.lacava@northeastern.edu>
Patch bump for the additive/source-compatible API surfaced in the
previous commit (#67, #68) — same class of change as 1ad9ced, which
bumped 0.1.0 -> 0.1.1 for the analogous subscribe() out_request_id.

Jumps to 0.1.2 rather than 0.1.1 because feat/latency-tracing (open,
unmerged) already carries VERSION 0.1.1 on its own branch; this keeps the
two in sequence instead of colliding whenever that branch lands.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Andrea Lacava <a.lacava@northeastern.edu>
@Thecave3
Thecave3 force-pushed the 67-68-message-id-correlation branch from fa6c62b to 92130a1 Compare August 27, 2026 16:52
Base automatically changed from feat/latency-tracing to main August 27, 2026 19:30
@Thecave3
Thecave3 merged commit 92130a1 into main Aug 27, 2026
24 checks passed
@Thecave3
Thecave3 deleted the 67-68-message-id-correlation branch August 27, 2026 19:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant