Overview & Architecture
The airpuls-ric server is the RIC process an operator deploys. It owns the E2 SCTP termination on the southbound interface (facing E2 nodes — gNBs, eNBs, the emulator) and serves xApps over a length-framed IPC protocol on the northbound interface. This page is the mental model you need before configuring, deploying, and operating it.
Where the RIC sits
flowchart LR
subgraph nodes["E2 nodes (southbound)"]
GNB["OAI gNB<br/>+ E2 agent"]
EMU["E2 emulator<br/>(OAI-free)"]
end
RIC["airpuls-ric<br/>SB workers · dispatcher · registries · NB workers"]
subgraph xapps["xApps (northbound)"]
X1["kpm-monitor"]
X2["ho-trigger"]
X3["your xApp"]
end
GNB -- "E2AP / SCTP :36421" --> RIC
EMU -- "E2AP / SCTP :36421" --> RIC
RIC -- "IPC :36422 (tcp:// or unix://)" --> X1
RIC -- "IPC" --> X2
RIC -- "IPC" --> X3
RIC -. "/metrics :9090" .-> PROM["Prometheus"]
SMO["Non-RT RIC / SMO"] -- "A1 policy (HTTPS)" --> A1P["A1-P Producer"]
A1P -. "policy over Redis" .-> xapps
- Southbound (SB) — E2AP over SCTP. E2 nodes connect in, complete the E2 Setup handshake, and thereafter exchange Subscription, Indication, and Control messages.
- Northbound (NB) — the xApp IPC. xApps connect out-of-process over
a
tcp://orunix://endpoint (defaulttcp://127.0.0.1:36422) and receive a callback-based surface via the SDK. - A1 (policy) — the O-RAN A1 policy interface, facing the Non-RT RIC (SMO). It is terminated by a companion A1-P Producer service, which validates accepted policies and distributes them to enforcing xApps over Redis, then reports each policy's enforcement status back. xApps enforce policies with the SDK's A1 policy enforcer. The RIC core itself stays E2/IPC-only; A1 is a distinct plane.
- Observability — an in-loop Prometheus endpoint (default
:9090), scraped by an external Prometheus / Grafana stack.
The RIC never parses a byte of an xApp's Service Model payload beyond what E2AP requires to route it: it resolves each node's RAN functions to SM plugins by OID at E2 Setup, then routes Subscriptions and Indications by RAN Function ID and subscription id.
Threading model
The RIC runs a partitioned multi-threaded event loop. Two worker
pools are sized independently at config-load time (default
min(nproc/2, 4) each, clamped to [1, 16]; set 1 for strictly
serial behaviour):
- Southbound (SB) workers — terminate the E2 SCTP interface and run
the E2AP codec + dispatcher. Each worker owns its own
GMainContextand asn1c codec state. - Northbound (NB) workers — own the xApp IPC. Each NB worker has its
own
GMainContext, its own listen fd (TCP +SO_REUSEPORTwhen more than one; aunix://endpoint is clamped to a single NB worker with a startupWARNING), and its own codec state. Each xApp connection is accepted by exactly one NB worker and never migrates, so single-writer-per-connection holds by construction.
A response produced on an SB worker is delivered to the NB worker that
owns the target xApp connection through a lock-free SPSC matrix
(cross_thread_post), so the socket write always runs on the owning NB
thread. Within a single worker, GLib guarantees its callbacks are never
re-entered concurrently, so per-worker state needs no locks.
When to change the pool size
Leave the defaults for most deployments. Set both pools to 1
(transport.southbound.worker_threads: 1, transport.northbound.worker_threads: 1) for
deterministic, strictly-serial behaviour when debugging or
reproducing an issue.
Raise them only when a saturation profile shows a single worker
pool saturating a core. See Configuration.
Southbound message flow
Every south-bound SCTP event enters through a per-SB-worker callback, which decodes the E2AP PDU and hands it to the named O(1) dispatcher:
on_sctp_readable() ← per SB worker
├── SCTP notification (association lifecycle)
│ ├── COMM_UP → registry_add()
│ ├── COMM_LOST → cleanup_e2_node()
│ └── SHUTDOWN → cleanup_e2_node()
│
└── SCTP data (E2AP PDU) → codec->decode() → dispatcher_dispatch()
├── E2 Setup Request → handle_e2_setup_request()
├── RIC Subscription Resp/Fail → handle_ric_subscription_*()
├── RIC Subscription Delete → handle_ric_subscription_delete_*()
├── RIC Subscription Modification → handle_ric_subscription_modification_*()
├── RIC Indication → handle_ric_indication()
│ └── forwarded to the owning xApp
│ via cross_thread_post → NB worker
├── RIC Control ACK/Failure → handle_ric_control_*() → pending tracker
├── E2 Reset Request → handle_e2_reset_request()
├── E2 Removal Request → handle_e2_removal_request()
├── RIC Service Update → handle_ric_service_update()
├── E2 Node Configuration Update → handle_e2_node_config_update()
├── E2 Connection Update → handle_e2_connection_update()
└── Error Indication → handle_error_indication()
What each registry tracks
| Registry | Responsibility |
|---|---|
| E2 node registry | Stateful, one entry per E2 association; state machine, duplicate Global E2 Node ID detection, reconnect handling |
| Subscription manager | Composite-key FSM, monotonic subscription_id, xApp ownership, bulk cleanup on node/xApp disconnect, pending-timeout expiration |
| xApp manager | Per-connection id allocation, connection → NB-worker ownership |
| SM plugin registry | OID-based resolution (with ran_func_id lookup) — the capability gate for every subscription |
A single codec for every E2AP version
The RIC compiles one codec against the latest in-tree ASN.1 and handles all E2AP versions through the protocol's own extensibility markers and criticality framework — there are no version-specific code paths. Unknown extension IEs are tolerated per their criticality; the ASN.1 module is the contract for what the RIC can emit and decode.
Graceful shutdown
On SIGINT / SIGTERM the RIC runs a three-phase shutdown, each
phase bounded by a timeout: delete outstanding subscriptions → send E2
Removal to connected nodes → force quit. This lets nodes and xApps tear
down cleanly rather than seeing an abrupt transport drop.
Next steps
- Getting Started — dependencies, build, run
- Configuration — the full
nrtric.ymlreference - Docker Deployment — images and the Compose stack
- Service Models — the supported SM / style catalogue
- Observability — metrics, logs, and the Grafana dashboard