fix(connector): real-time teardown, NODELAY, and multi-peer POSIX#36
Open
Thecave3 wants to merge 17 commits into
Open
fix(connector): real-time teardown, NODELAY, and multi-peer POSIX#36Thecave3 wants to merge 17 commits into
Thecave3 wants to merge 17 commits into
Conversation
37e683d to
d1f4655
Compare
Base automatically changed from
16-add-protobuf-as-a-new-encoding-methodology
to
main
July 9, 2026 19:18
ab72dfe to
5c32de0
Compare
Set ZMQ_LINGER=0 on every socket (RAN REP/SUB/PUB, dApp REQ/SUB/PUB, and the reset path). The default linger of -1 makes zmq_ctx_destroy block until queued messages are flushed, so a RAN with undelivered indications on its PUB socket hung forever in dispose() and the process effectively ignored SIGTERM (the handler ran, but stop() never returned). Observed as benchmark agents wedged in poll() for days. Assisted-by: Claude:claude-fable-5
shutdown() interrupted the setup and inbound listeners but never the outbound one, so a RAN stopped before any dApp connected hung forever in the outbound thread's blocking accept() and stop() never returned. Assisted-by: Claude:claude-fable-5
Set TCP_NODELAY / SCTP_NODELAY on every accepted and connected TCP/SCTP socket (setup, inbound, outbound, and the dApp client paths). Without it each framed message (small length prefix + payload) stalls ~1 RTT waiting for the previous segment's ACK, measured as a hard ~214 msg/s throughput ceiling on SCTP regardless of offered rate. Set explicitly on accepted fds since Linux does not reliably inherit the option from the listener; no-op for UNIX-domain sockets. Assisted-by: Claude:claude-fable-5
send_in_chunks used a separate send() for the 4-byte length prefix and another for the payload; coalesce them into a single writev() (falling back to chunked send() for the remainder of oversized payloads). Halves the data-path syscall count and removes the tiny per-message segment that Nagle-style bundling was stalling on. Assisted-by: Claude:claude-fable-5
The RAN's POSIX data channels each accepted exactly one connection, once, and independently: with two dApps the subscription channel and the indication channel could be won by different peers (cross-wiring), so the subscribed dApp received nothing while extra clients sat in the listen backlog forever believing they were connected. Both listeners now accept for the connector's lifetime. receive() polls the listener plus every accepted subscriber socket (raw framing per peer preserved for the Python dApp framework); send() drains the indication listener and broadcasts each framed message to all peers, dropping dead ones, which mirrors the ZMQ PUB/SUB semantics the rest of libe3 assumes (RAN broadcasts, each dApp filters by its own dApp identifier; no peer attached = message dropped like PUB with no subscriber). Peers can attach and re-attach at any time; no wire format change and no identification handshake needed. The multi-peer dispatch integration test now runs the same no-crosstalk scenario over both ZMQ and POSIX. Assisted-by: Claude:claude-fable-5
Replace sleep_for(period) with a monotonic sleep_until deadline that advances by one period per cycle, so the offered rate equals 1/period independent of per-cycle work. The uncompensated sleep made each cycle cost period + work, capping the offered rate at ~6.7 kHz when 10 kHz was configured, which bounded every throughput benchmark by the load generator instead of the library. Falls back to re-anchoring (no catch-up bursts) when the loop is behind schedule. Assisted-by: Claude:claude-fable-5
bench_full_loop_latency computed phase deltas with integer ns->us division, truncating the six sub-microsecond phases (collect, encode, decode, process) to 0 in the report; emit fractional microseconds with two decimals instead (table structure unchanged for downstream parsers). bench_encoding_size: document precisely how info_bytes is computed (analytical fixed-width accounting of the sample PDU's field values, not an encoder measurement) and why ASN.1 APER can legitimately encode below that baseline (range-constrained integers in minimal bit width, no per-field tags; verified against the E3AP ASN.1 module, e.g. SubscriptionDelete 5 wire bytes vs 12 info bytes). Assisted-by: Claude:claude-fable-5
sm_data_thread_, its sm_data_handler_loop() polling stub, and the SM_POLL_INTERVAL constant were declared but never started or wired. A Service Model produces indications on the application's own thread and emits them through the outbound-emitter callback into the outbound queue; the RAN role runs setup/inbound/outbound plus the report worker. Remove the dead declaration, the empty poll loop, the always-false join, and the stale comments. No functional change; ctest 22/22. Assisted-by: Claude:claude-opus-4-8
79b0a1d to
b83ae4e
Compare
Contributor
⏱️ Full-loop Latency Benchmark (commit
|
| Phase | mean | p50 | p99 | max |
|---|---|---|---|---|
| 1. Collect indication data | 0.09 | 0.07 | 0.21 | 17.00 |
| 2. Create & encode indication | 0.70 | 0.62 | 1.20 | 13.62 |
| 3. Deliver indication (RAN -> dApp) | 88.69 | 80.92 | 165.39 | 176.55 |
| 4. Decode indication | 0.55 | 0.52 | 0.89 | 17.73 |
| 5. Process data | 0.03 | 0.03 | 0.04 | 0.13 |
| 6. Create & encode control | 0.28 | 0.26 | 0.49 | 10.69 |
| 7. Deliver control (dApp -> RAN) | 119.67 | 119.66 | 177.58 | 193.09 |
| 8. Decode & handle control | 0.50 | 0.47 | 0.91 | 11.23 |
| Total round-trip | 210.52 | 196.74 | 326.11 | 334.71 |
Benchmarked on
ubuntu-latest, Release build, ZMQ + IPC, ASN.1 APER.
Contributor
🔄 E2E dApp Integration Results (commit
|
Contributor
🔀 E2E Topologies — multi-dApp / multi-RAN (commit
|
Set ZMQ_LINGER=0 on every socket (RAN REP/SUB/PUB, dApp REQ/SUB/PUB, and the reset path). The default linger of -1 makes zmq_ctx_destroy block until queued messages are flushed, so a RAN with undelivered indications on its PUB socket hung forever in dispose() and the process effectively ignored SIGTERM (the handler ran, but stop() never returned). Observed as benchmark agents wedged in poll() for days. Assisted-by: Claude:claude-fable-5
shutdown() interrupted the setup and inbound listeners but never the outbound one, so a RAN stopped before any dApp connected hung forever in the outbound thread's blocking accept() and stop() never returned. Assisted-by: Claude:claude-fable-5
Set TCP_NODELAY / SCTP_NODELAY on every accepted and connected TCP/SCTP socket (setup, inbound, outbound, and the dApp client paths). Without it each framed message (small length prefix + payload) stalls ~1 RTT waiting for the previous segment's ACK, measured as a hard ~214 msg/s throughput ceiling on SCTP regardless of offered rate. Set explicitly on accepted fds since Linux does not reliably inherit the option from the listener; no-op for UNIX-domain sockets. Assisted-by: Claude:claude-fable-5
send_in_chunks used a separate send() for the 4-byte length prefix and another for the payload; coalesce them into a single writev() (falling back to chunked send() for the remainder of oversized payloads). Halves the data-path syscall count and removes the tiny per-message segment that Nagle-style bundling was stalling on. Assisted-by: Claude:claude-fable-5
The RAN's POSIX data channels each accepted exactly one connection, once, and independently: with two dApps the subscription channel and the indication channel could be won by different peers (cross-wiring), so the subscribed dApp received nothing while extra clients sat in the listen backlog forever believing they were connected. Both listeners now accept for the connector's lifetime. receive() polls the listener plus every accepted subscriber socket (raw framing per peer preserved for the Python dApp framework); send() drains the indication listener and broadcasts each framed message to all peers, dropping dead ones, which mirrors the ZMQ PUB/SUB semantics the rest of libe3 assumes (RAN broadcasts, each dApp filters by its own dApp identifier; no peer attached = message dropped like PUB with no subscriber). Peers can attach and re-attach at any time; no wire format change and no identification handshake needed. The multi-peer dispatch integration test now runs the same no-crosstalk scenario over both ZMQ and POSIX. Assisted-by: Claude:claude-fable-5
Replace sleep_for(period) with a monotonic sleep_until deadline that advances by one period per cycle, so the offered rate equals 1/period independent of per-cycle work. The uncompensated sleep made each cycle cost period + work, capping the offered rate at ~6.7 kHz when 10 kHz was configured, which bounded every throughput benchmark by the load generator instead of the library. Falls back to re-anchoring (no catch-up bursts) when the loop is behind schedule. Assisted-by: Claude:claude-fable-5
bench_full_loop_latency computed phase deltas with integer ns->us division, truncating the six sub-microsecond phases (collect, encode, decode, process) to 0 in the report; emit fractional microseconds with two decimals instead (table structure unchanged for downstream parsers). bench_encoding_size: document precisely how info_bytes is computed (analytical fixed-width accounting of the sample PDU's field values, not an encoder measurement) and why ASN.1 APER can legitimately encode below that baseline (range-constrained integers in minimal bit width, no per-field tags; verified against the E3AP ASN.1 module, e.g. SubscriptionDelete 5 wire bytes vs 12 info bytes). Assisted-by: Claude:claude-fable-5
sm_data_thread_, its sm_data_handler_loop() polling stub, and the SM_POLL_INTERVAL constant were declared but never started or wired. A Service Model produces indications on the application's own thread and emits them through the outbound-emitter callback into the outbound queue; the RAN role runs setup/inbound/outbound plus the report worker. Remove the dead declaration, the empty poll loop, the always-false join, and the stale comments. No functional change; ctest 22/22. Assisted-by: Claude:claude-opus-4-8
b83ae4e to
4c04a3d
Compare
…com/wineslab/libe3 into fix/connector-rt-teardown-multipeer
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.
Stacked on #35. Seven commits fixing defects surfaced by the paper's full baseline benchmark sweep:
Teardown / SIGTERM
ZMQ_LINGER=0. The default (-1) madezmq_ctx_destroyblock forever flushing undelivered PUB messages, so a RAN ignored SIGTERM (observed: benchmark agents wedged inpoll()for days).shutdown()now also unblocks the outbound listener'saccept().Real-time data path
send_in_chunkscoalesces the 4-byte length prefix and payload into a singlewritev().simple_agent:sleep_untildeadline scheduler, so offered rate equals 1/period (the oldsleep_forcapped 10 kHz configs at ~6.7 kHz, bounding every benchmark by the load generator).Multi-peer POSIX
The RAN's POSIX data channels each accepted exactly one connection, independently, so two dApps could cross-wire (subscription channel won by A, indication channel by B) and everyone received nothing. Both listeners now accept for the connector's lifetime;
receive()polls all subscriber sockets;send()broadcasts framed messages to all indication sockets, mirroring the ZMQ PUB/SUB semantics (RAN broadcasts, dApps filter by dApp id). No wire-format change; plain connect-and-read clients (Python dApp framework) keep working. Verified: two concurrent dApps over posix/tcp each receive the full indication stream. The multi-peer dispatch integration test now runs over both ZMQ and POSIX.Benchmarks
bench_full_loop_latencyemits fractional microseconds (sub-µs phases were truncated to 0).bench_encoding_size: documented theinfo_bytesaccounting and why APER packs below it (range-constrained integers, no per-field tags; verified against the E3AP ASN.1 module).All 21 unit/integration tests pass (
build-bench, ASN.1+JSON+protobuf, sanitizers off).🤖 Generated with Claude Code