Skip to content

Carry a sequenceId across the E2-E3 loop, and widen the id ranges - #78

Draft
Thecave3 wants to merge 2 commits into
mainfrom
feat/e2e-mitigation-latency
Draft

Carry a sequenceId across the E2-E3 loop, and widen the id ranges#78
Thecave3 wants to merge 2 commits into
mainfrom
feat/e2e-mitigation-latency

Conversation

@Thecave3

@Thecave3 Thecave3 commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

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.md has carried since it was written.

What

  • E3-SequenceID, carried on the three messages that make up a procedure: mandatory on E3-DAppReport (the dApp assigns it per detection) and 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 re-issues an xApp control and absent on one it decided itself.
  • E3-MessageAck untouched. The RAN sends two independent acknowledgements; the dApp's stays a delivery receipt and the xApp's carries the apply outcome.
  • Widened id ranges while the wire is changing anyway: dApp / RAN-function / telemetry / control / subscription ids to 1..65535, message and sequence ids to 1..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_RECV is now stamped in handle_message_ack. Only the aerial native backend stamped it, so the acknowledgment leg was invisible on any libe3 build.

Verification

  • 33/33 tests pass, including all 10 integration tests; builds clean with and without LIBE3_ENABLE_LATREC.
  • I compiled the generated codec and round-tripped a PDU at the maximum of every widened range. asn1c renders them all as native integers — nothing falls back to the heap-allocating INTEGER_t, and the codec stays allocation-free on the indication path.
  • Three tests failed exactly as intended and were fixed rather than the grammar: they built a DAppReport without a sequence_id, and the encoder now rejects a mandatory field left at 0.

Worth knowing

  • This is a wire break. Widening an APER integer constraint changes the encoding, so every component must be rebuilt together. Version goes to 0.2.0.
  • DAppSession::send_control/send_report now return int64_t. A message id spans the full uint32 range and would have aliased onto the negative ErrorCode returns if it stayed int — a latent bug the widening exposed.

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.
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

CI report — 1016597 — ✅ all checks passed

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=5
    • dapp peer=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=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 max=0 @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=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=t2a ran=ran-a 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]
    • 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.2 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=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.2 max=1 @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=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 main

Head: 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 1016597 finished.

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.
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.

1 participant