libe3 0.2.5
C++ library for E3AP RAN vendor integration
Loading...
Searching...
No Matches
e3_dapp_session.hpp
Go to the documentation of this file.
1/**
2 * @file e3_dapp_session.hpp
3 * @brief Python-facing dApp session over libe3 — batched, low-latency seam.
4 *
5 * @section why Why this class exists
6 * libe3's core @ref libe3::E3Agent drives the dApp role with `std::function`
7 * handlers (@ref libe3::E3Agent::set_indication_handler et al.). SWIG cannot
8 * bridge those callbacks to Python across libe3's inbound worker threads
9 * without either running Python under the GIL *on the hot thread* (directors —
10 * deadlock-prone, serialises dispatch) or paying a GIL round-trip per message.
11 * Both are unacceptable at the E3AP target rate (real-time, sub-millisecond
12 * cadence, very high message throughput).
13 *
14 * @ref libe3::py::DAppSession is the answer: it owns an @ref libe3::E3Agent in
15 * DAPP role, registers the C++ handlers itself, and funnels every inbound
16 * message into a **bounded lock-free ring** (@ref libe3::LockFreeQueue). Python
17 * drains that ring in **batches** via @ref poll_events, so one GIL acquire and
18 * one Python↔C++ boundary crossing are amortised across many messages. The
19 * SWIG wrapper releases the GIL around the blocking calls (see swig/libe3.i,
20 * `%module(threads="1")`), so libe3's C++ threads never stall on Python.
21 *
22 * The E3SM (service-model) payloads stay opaque here (`std::vector<uint8_t>`);
23 * dApp-library encodes/decodes them in Python. This class is E3AP only, mirroring
24 * the OAI split where libe3 owns E3AP and the SM owns encode/decode.
25 *
26 * @section lifecycle Lifecycle
27 * ctor (registers handlers) → @ref start → @ref wait_for_setup →
28 * (read setup via @ref dapp_id / setup accessors) → @ref subscribe →
29 * loop: @ref poll_events + @ref send_control / @ref send_report →
30 * @ref release → @ref stop.
31 *
32 * @section rejected Rejected alternatives (auditable)
33 * - SWIG directors: run the Python callback on libe3's inbound thread under the
34 * GIL — serialises dispatch, deadlock-prone if Python re-enters libe3.
35 * - per-message poll: one GIL round-trip + one boundary crossing per message
36 * caps throughput; batching is strictly better with no added latency (the
37 * ring still wakes on the first event).
38 *
39 * SPDX-FileCopyrightText: Copyright (c) 2026 Northeastern University
40 * SPDX-License-Identifier: Apache-2.0
41 */
42
43#ifndef LIBE3_PY_E3_DAPP_SESSION_HPP
44#define LIBE3_PY_E3_DAPP_SESSION_HPP
45
46#include <libe3/types.hpp>
47
48#include <cstdint>
49#include <memory>
50#include <string>
51#include <vector>
52
53namespace libe3 {
54namespace py {
55
56/**
57 * @brief Discriminator for @ref E3Event.
58 */
60 E3_EVENT_NONE = 0, ///< placeholder (never enqueued)
61 E3_EVENT_INDICATION = 1, ///< RAN → dApp IndicationMessage
62 E3_EVENT_XAPP_CONTROL = 2, ///< xApp → dApp XAppControlAction
63 E3_EVENT_SUBSCRIPTION_RESPONSE = 3, ///< RAN → dApp SubscriptionResponse
64 E3_EVENT_SETUP_RESPONSE = 4, ///< RAN → dApp SetupResponse
65 E3_EVENT_MESSAGE_ACK = 5 ///< RAN → dApp MessageAck
66};
67
68/**
69 * @brief One inbound E3AP event, POD-flat for a clean SWIG crossing.
70 *
71 * `payload` carries the opaque E3SM bytes for indications
72 * (protocol_data) and xApp controls (xapp_control_data); it is empty for
73 * subscription/setup responses and acks. Fields not relevant to a given
74 * `kind` are left zero.
75 */
76struct E3Event {
77 int kind{E3_EVENT_NONE}; ///< one of @ref E3EventKind
78 uint32_t dapp_id{0}; ///< dApp identifier the message targets
79 uint32_t ran_function_id{0}; ///< RAN function (indication / xApp control)
80 uint32_t subscription_id{0}; ///< subscription id (subscription response)
81 uint32_t request_id{0}; ///< request/message id (subscription response / ack / xApp control)
82 uint32_t sequence_id{0}; ///< loop correlation id (xApp control); echo it on the control you re-issue
83 int response_code{-1}; ///< 0=positive, 1=negative, -1=n/a
84 std::vector<uint8_t> payload; ///< opaque E3SM bytes (indication / xApp control)
85 uint64_t trace_seq{0}; ///< set when queued; keys the [latrec] LQ-stage records
86 // NOTE: in Python (libe3py) the payload is read via ev.get_payload(), which
87 // returns native `bytes`. SWIG member getters return a wrapped vector, so the
88 // binding exposes the payload as a by-value method instead (see swig/libe3.i).
89};
90
91/**
92 * @brief dApp-role E3AP session with a batched, lock-free inbound ring.
93 *
94 * Thread model: two producers push into the ring — the setup-response handler
95 * (libe3 setup thread) and the other handlers (libe3 inbound thread), which run
96 * concurrently at startup; the Python drainer thread is the single consumer via
97 * @ref poll_events. The ring is MPMC, so this is safe (do not "optimize" it to
98 * SPSC). All outbound verbs are thread-safe (delegated to @ref libe3::E3Agent).
99 */
101public:
102 /**
103 * @brief Construct the session and register all inbound handlers.
104 * @param config Agent config; `role` is forced to @ref E3Role::DAPP.
105 * @param queue_capacity Inbound ring capacity (rounded up to a power of
106 * two). Sized for high-throughput headroom; overflow increments the
107 * drop counter (@ref dropped_events) rather than blocking libe3.
108 */
109 explicit DAppSession(libe3::E3Config config, std::size_t queue_capacity = 8192);
110 ~DAppSession();
111
112 DAppSession(const DAppSession&) = delete;
114
115 /** @brief Start the agent, re-arming the inbound ring so a stop()->start()
116 * cycle works again. @return ErrorCode as int. */
117 int start();
118 /** @brief Block until the setup handshake completes. @return ErrorCode as int. */
119 int wait_for_setup(int timeout_ms);
120 /** @brief Send a ReleaseMessage (session keeps running until @ref stop). @return ErrorCode as int. */
121 int release();
122 /** @brief Stop the agent and shut down the ring so a blocked @ref poll_events
123 * returns an empty batch (i.e. empty-after-stop means "done"). Restartable
124 * via @ref start. */
125 void stop();
126
127 /** @brief dApp id assigned at setup, or -1 if not yet assigned. */
128 long dapp_id() const;
129 /** @brief RAN identifier from the SetupResponse, or "" if none. */
130 std::string ran_identifier() const;
131
132 /** @brief SetupResponse code: 0=positive, 1=negative, -1=not received. */
133 int setup_response_code() const;
134 /** @brief Number of RAN functions advertised in the SetupResponse. */
135 std::size_t setup_ran_function_count() const;
136 /** @brief RAN function id of advertised entry @p i (0 if out of range). */
137 uint32_t setup_ran_function_id(std::size_t i) const;
138 /** @brief Telemetry ids of advertised entry @p i. */
139 std::vector<uint32_t> setup_ran_function_telemetry(std::size_t i) const;
140 /** @brief Control ids of advertised entry @p i. */
141 std::vector<uint32_t> setup_ran_function_control(std::size_t i) const;
142 /** @brief Opaque ranFunctionData bytes of advertised entry @p i. */
143 std::vector<uint8_t> setup_ran_function_data(std::size_t i) const;
144
145 /**
146 * @brief Subscribe to a RAN function (after a positive setup).
147 * @param sub_time_ms subscription time, or -1 for unset.
148 * @param periodicity delivery periodicity (µs), or -1 for unset.
149 * @return the assigned request id (positive, 1..4294967295) on success, so
150 * the caller can correlate the SubscriptionResponse by its
151 * request_id; a negative ErrorCode on failure.
152 */
153 int64_t subscribe(uint32_t ran_function_id,
154 std::vector<uint32_t> telemetry_ids,
155 std::vector<uint32_t> control_ids,
156 int sub_time_ms,
157 int periodicity);
158 /** @brief Delete a previously created subscription. @return ErrorCode as int. */
159 int unsubscribe(uint32_t ran_function_id);
160
161 /**
162 * @brief Send a dApp control action to the RAN.
163 * @param sequence_id correlation id of the xApp procedure this control
164 * re-issues; 0 when the dApp decided on its own.
165 * @return the assigned message id (positive, 1..4294967295) on success, so
166 * the caller can correlate a later ack/response by id; a negative
167 * ErrorCode on failure. Returned as int64_t because a message id
168 * now spans the full uint32 range and would alias onto the negative
169 * error codes if narrowed to int.
170 */
171 int64_t send_control(uint32_t ran_function_id, uint32_t control_id,
172 std::vector<uint8_t> action_data,
173 uint32_t sequence_id = 0);
174 /**
175 * @brief Send a dApp report to the RAN.
176 * @param sequence_id correlation id this dApp assigns to the detection;
177 * mandatory on the wire, 1..4294967295 (0 is refused with
178 * INVALID_PARAM).
179 * @return the assigned message id (positive, 1..4294967295) on success, so
180 * the caller can correlate a later ack/response by id; a negative
181 * ErrorCode on failure.
182 */
183 int64_t send_report(uint32_t ran_function_id, std::vector<uint8_t> report_data,
184 uint32_t sequence_id);
185 /** @brief Acknowledge a request. @param response_code 0=positive,1=negative. @return ErrorCode as int. */
186 int send_message_ack(uint32_t request_id, int response_code);
187
188 /**
189 * @brief Drain up to @p max_batch inbound events, waiting up to
190 * @p timeout_ms for the first.
191 *
192 * Blocks on the ring for the first event (latency ≈ ring wakeup), then
193 * sweeps up any further already-queued events without blocking.
194 * @p timeout_ms <= 0 polls non-blocking (does not busy-wait). Returns an
195 * empty vector on a quiet tick or after @ref stop. The SWIG wrapper releases
196 * the GIL for the whole call so libe3's threads run freely while Python waits.
197 */
198 std::vector<E3Event> poll_events(std::size_t max_batch, int timeout_ms);
199
200 /** @brief Total inbound events dropped due to ring overflow (backpressure). */
201 unsigned long long dropped_events() const;
202
203private:
204 struct Impl;
205 std::unique_ptr<Impl> impl_;
206};
207
208} // namespace py
209} // namespace libe3
210
211#endif // LIBE3_PY_E3_DAPP_SESSION_HPP
uint32_t setup_ran_function_id(std::size_t i) const
RAN function id of advertised entry i (0 if out of range).
int setup_response_code() const
SetupResponse code: 0=positive, 1=negative, -1=not received.
int unsubscribe(uint32_t ran_function_id)
Delete a previously created subscription.
std::vector< E3Event > poll_events(std::size_t max_batch, int timeout_ms)
Drain up to max_batch inbound events, waiting up to timeout_ms for the first.
std::vector< uint8_t > setup_ran_function_data(std::size_t i) const
Opaque ranFunctionData bytes of advertised entry i.
std::vector< uint32_t > setup_ran_function_telemetry(std::size_t i) const
Telemetry ids of advertised entry i.
std::string ran_identifier() const
RAN identifier from the SetupResponse, or "" if none.
unsigned long long dropped_events() const
Total inbound events dropped due to ring overflow (backpressure).
void stop()
Stop the agent and shut down the ring so a blocked poll_events returns an empty batch (i....
int release()
Send a ReleaseMessage (session keeps running until stop).
DAppSession & operator=(const DAppSession &)=delete
DAppSession(const DAppSession &)=delete
int send_message_ack(uint32_t request_id, int response_code)
Acknowledge a request.
int start()
Start the agent, re-arming the inbound ring so a stop()->start() cycle works again.
int wait_for_setup(int timeout_ms)
Block until the setup handshake completes.
int64_t send_report(uint32_t ran_function_id, std::vector< uint8_t > report_data, uint32_t sequence_id)
Send a dApp report to the RAN.
long dapp_id() const
dApp id assigned at setup, or -1 if not yet assigned.
int64_t subscribe(uint32_t ran_function_id, std::vector< uint32_t > telemetry_ids, std::vector< uint32_t > control_ids, int sub_time_ms, int periodicity)
Subscribe to a RAN function (after a positive setup).
std::size_t setup_ran_function_count() const
Number of RAN functions advertised in the SetupResponse.
DAppSession(libe3::E3Config config, std::size_t queue_capacity=8192)
Construct the session and register all inbound handlers.
std::vector< uint32_t > setup_ran_function_control(std::size_t i) const
Control ids of advertised entry i.
int64_t send_control(uint32_t ran_function_id, uint32_t control_id, std::vector< uint8_t > action_data, uint32_t sequence_id=0)
Send a dApp control action to the RAN.
E3EventKind
Discriminator for E3Event.
@ E3_EVENT_NONE
placeholder (never enqueued)
@ E3_EVENT_SUBSCRIPTION_RESPONSE
RAN → dApp SubscriptionResponse.
@ E3_EVENT_SETUP_RESPONSE
RAN → dApp SetupResponse.
@ E3_EVENT_INDICATION
RAN → dApp IndicationMessage.
@ E3_EVENT_XAPP_CONTROL
xApp → dApp XAppControlAction
@ E3_EVENT_MESSAGE_ACK
RAN → dApp MessageAck.
Configuration for E3Agent.
Definition types.hpp:390
One inbound E3AP event, POD-flat for a clean SWIG crossing.
uint32_t ran_function_id
RAN function (indication / xApp control).
uint32_t sequence_id
loop correlation id (xApp control); echo it on the control you re-issue
int kind
one of E3EventKind
uint32_t dapp_id
dApp identifier the message targets
uint32_t request_id
request/message id (subscription response / ack / xApp control)
uint32_t subscription_id
subscription id (subscription response)
int response_code
0=positive, 1=negative, -1=n/a
uint64_t trace_seq
set when queued; keys the [latrec] LQ-stage records
std::vector< uint8_t > payload
opaque E3SM bytes (indication / xApp control)
E3AP Protocol Types - Vendor-neutral type definitions.