Skip to content

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:// or unix:// endpoint (default tcp://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 GMainContext and asn1c codec state.
  • Northbound (NB) workers — own the xApp IPC. Each NB worker has its own GMainContext, its own listen fd (TCP + SO_REUSEPORT when more than one; a unix:// endpoint is clamped to a single NB worker with a startup WARNING), 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