Carry a sequenceId across the E2-E3 loop, and widen the id ranges - #78
Draft
Thecave3 wants to merge 2 commits into
Draft
Carry a sequenceId across the E2-E3 loop, and widen the id ranges#78Thecave3 wants to merge 2 commits into
Thecave3 wants to merge 2 commits into
Conversation
The E2-E3 loop had no identifier that survived a full procedure. A dApp report, the xApp control it provoked, and the control the dApp re-issued in response were three unrelated messages as far as the wire was concerned, so the RAN could not tell which xApp procedure an applied control belonged to, and the latency legs could only be paired by nearest-in-time guessing. Add E3-SequenceID and carry it on the three messages that make up a procedure: mandatory on E3-DAppReport (the dApp assigns it per detection) and on E3-XAppControlAction (the RAN bridge copies it from the E2SM-DAPP control header), optional on E3-DAppControlAction, where it is present only when the dApp is re-issuing an xApp control and absent on one it decided itself. E3-MessageAck is untouched: the RAN sends two independent acks, and the dApp's stays a delivery receipt. Widen the identifier ranges while the wire is changing anyway: dApp, RAN function, telemetry and control ids to 1..65535, message id and the new sequence id to 1..4294967295, and the identifier lists to 1000 entries. generate_message_id() no longer wraps at 1000, which is what let an id be reused while an older request was still unanswered -- the reason the aerial dApp documents that it cannot de-duplicate a relayed control. asn1c renders every widened field as a native integer, so nothing moves to the arbitrary-precision INTEGER_t and the codec stays allocation-free. DAppSession::send_control/send_report now return int64_t: a message id spans the full uint32 range and would alias onto the negative ErrorCode returns if it stayed int. Also stamp LATREC_ACK_RECV in handle_message_ack. Only the aerial native backend was stamping it, so the acknowledgment leg was invisible on any libe3 build. Widening an APER integer constraint changes the encoding, so every component has to be rebuilt together; libe3 goes to 0.2.0.
Contributor
CI report —
|
| Workflow | Result | Time | Run |
|---|---|---|---|
| Commit policy | ✅ success | 2m18s | #105 |
| E2E dApp Integration | ✅ success | 2m25s | #115 |
| E2E Topologies (multi-dApp / multi-RAN) | ✅ success | 2m21s | #113 |
| Full-loop Latency Benchmark | ✅ success | 0m56s | #114 |
| Unit Tests | ✅ success | 4m34s | #141 |
| 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=
5dapppeer=t11 ran=ran-solo 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]
- ✅ 1 RAN - 2 dApps: dApp#1 ind=
5sub=1, dApp#2 ind=6sub=2, RAN saw2dApps - ✅ 2 RANs - 1 dApp: from ran-a ind=
5, from ran-b ind=5
zmq/tcp
- ✅ 1 RAN - 1 dApp: indications=
5dapppeer=default ran=ran-solo 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]
- ✅ 1 RAN - 2 dApps: dApp#1 ind=
5sub=1, dApp#2 ind=6sub=2, RAN saw2dApps - ✅ 2 RANs - 1 dApp: from ran-a ind=
5, from ran-b ind=5
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=1113 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.09 | 0.09 | 0.16 | 0.29 |
| 2 | Create & encode indication | ENCODE_E3SM_BEGIN to ENCODE_E3SM_DONE |
0.55 | 0.50 | 0.92 | 13.96 |
| 3 | Encode E3AP (indication) | EMIT_ENTER to ENQUEUE, then DEQUEUE to ENCODE_E3AP_DONE |
2.82 | 2.44 | 4.74 | 16.88 |
| 4 | Queuing (indication) | ENQUEUE to DEQUEUE |
5.44 | 4.97 | 15.89 | 24.99 |
| 5 | Delivery (indication) | ENCODE_E3AP_DONE to SEND_DONE |
1.66 | 0.24 | 23.88 | 25.89 |
| 6 | E3 wire (RAN -> dApp) | SEND_DONE to RECV |
40.72 | 42.78 | 58.88 | 72.57 |
| 7 | Decode E3AP (indication) | RECV to DECODE_E3AP_DONE |
1.58 | 1.41 | 6.34 | 20.52 |
| 8 | libe3 dispatch (indication) | DECODE_E3AP_DONE to DELIVER_BEGIN |
0.08 | 0.07 | 0.18 | 0.23 |
| 9 | Decode indication | DELIVER_BEGIN to DECODE_E3SM_DONE |
0.54 | 0.53 | 0.75 | 1.72 |
| 10 | Process data | DECODE_E3SM_DONE to ENCODE_E3SM_BEGIN |
0.04 | 0.04 | 0.05 | 0.23 |
| 11 | Create & encode control | ENCODE_E3SM_BEGIN to ENCODE_E3SM_DONE |
0.26 | 0.26 | 0.40 | 0.66 |
| 12 | Encode E3AP (control) | EMIT_ENTER to ENQUEUE, then DEQUEUE to ENCODE_E3AP_DONE |
2.42 | 2.09 | 4.69 | 15.88 |
| 13 | Queuing (control) | ENQUEUE to DEQUEUE |
10.88 | 12.13 | 16.56 | 54.40 |
| 14 | Delivery (control) | ENCODE_E3AP_DONE to SEND_DONE |
4.82 | 4.06 | 8.57 | 19.09 |
| 15 | E3 wire (dApp -> RAN) | SEND_DONE to RECV |
44.31 | 45.12 | 60.43 | 879.06 |
| 16 | Decode E3AP (control) | RECV to DECODE_E3AP_DONE |
1.83 | 1.71 | 5.50 | 8.61 |
| 17 | libe3 dispatch (control) | DECODE_E3AP_DONE to DECODE_E3SM_BEGIN |
0.18 | 0.16 | 0.42 | 9.48 |
| 18 | Decode & handle control | DECODE_E3SM_BEGIN to DECODE_E3SM_DONE |
0.31 | 0.30 | 0.55 | 4.79 |
| Total | Total round-trip | 118.91 | 120.64 | 151.87 | 995.91 |
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 101659745835f7466905ab64a207b28d67660ce8 && git push origin mainHead: 101659745835f7466905ab64a207b28d67660ce8 (branch feat/e2e-mitigation-latency). 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
1016597finished.
swig/libe3_swig.hpp is a hand-maintained declaration of E3Agent that SWIG reads instead of the real header, so it has to be updated alongside it. It was not: send_report and send_control still took no sequence_id, and the generated wrapper called a two-argument send_report that no longer exists. Only the SWIG job caught it -- every C++ target compiles against the real header and was clean. The mirror now carries sequence_id, mandatory on a report and defaulting to 0 on a control, and says why out_message_id is deliberately not mirrored. The smoke test passes the id its send_report call now needs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The E2-E3 loop had no identifier that survived a full procedure. A dApp report, the xApp control it provoked, and the control the dApp re-issued in response were three unrelated messages as far as the wire was concerned. The RAN could not tell which xApp procedure an applied control belonged to, and the latency legs could only be paired by nearest-in-time guessing — the caveat
docs/path-b-e2-e3-loop.mdhas carried since it was written.What
E3-SequenceID, carried on the three messages that make up a procedure: mandatory onE3-DAppReport(the dApp assigns it per detection) andE3-XAppControlAction(the RAN bridge copies it from the E2SM-DAPP control header), optional onE3-DAppControlAction, where it is present only when the dApp re-issues an xApp control and absent on one it decided itself.E3-MessageAckuntouched. The RAN sends two independent acknowledgements; the dApp's stays a delivery receipt and the xApp's carries the apply outcome.1..65535, message and sequence ids to1..4294967295, identifier lists to 1000 entries.generate_message_id()no longer wraps at 1000, which is what let an id be reused while an older request was outstanding — the reason the aerial dApp documents that it cannot de-duplicate a relayed control.LATREC_ACK_RECVis now stamped inhandle_message_ack. Only the aerial native backend stamped it, so the acknowledgment leg was invisible on any libe3 build.Verification
LIBE3_ENABLE_LATREC.INTEGER_t, and the codec stays allocation-free on the indication path.DAppReportwithout asequence_id, and the encoder now rejects a mandatory field left at 0.Worth knowing
DAppSession::send_control/send_reportnow returnint64_t. A message id spans the full uint32 range and would have aliased onto the negativeErrorCodereturns if it stayedint— a latent bug the widening exposed.