Skip to content

Port Aeron adapter to wingfoil-next - #601

Open
0-jake-0 wants to merge 2 commits into
nextfrom
aeron-next
Open

Port Aeron adapter to wingfoil-next#601
0-jake-0 wants to merge 2 commits into
nextfrom
aeron-next

Conversation

@0-jake-0

Copy link
Copy Markdown
Contributor

Ports the classic wingfoil::adapters::aeron module onto the Op model — the Aeron IPC/UDP low-latency message transport — behind the aeron (rusteron-client, C++ FFI) or aeron-rs (pure Rust) backend features, with aeron-driver embedding a media driver in-process. Last entry in Phase 4 item 8.

With this, every classic I/O adapter has a wingfoil-next twin. (The other three of this batch: #598 fluvio, #599 iceoryx2, #600 web.)

Surface

Sources (free builder fns on &GraphBuilder, realtime-only):

  • aeron_sub_fragment(&g, run_mode, subscriber, parser, opts) — the typed-parser surface with FragmentHeader access (position, session_id, stream_id) and the Ok(Some) / Ok(None) / Err contract (a malformed fragment is logged and skipped, never aborting the cycle).
  • aeron_sub_fragment_with_status(...) — the parallel-additive sibling returning (data, status).

Both polling modes are ported:

Mode Machinery
Spin (primary) a busy-spin custom_node polling Aeron inside the cycle on the graph thread
Threaded (secondary) source_at_start → a background poll thread over the channel layer, with classic's exponential idle back-off

Sink: AeronSinkOps::{aeron_pub, aeron_pub_with_status} on Stream<Burst<T>>, over register_op1, with classic's exact status ordering (Closed terminal and checked first; BackPressure records BackPressured and drops the rest of the burst; a successful offer records Connected; an empty burst falls back to is_connected).

Also carried over: the fragment_limit per-poll cap, the SpinThreaded downgrade for backends that lock on poll, the ChannelUri builders, ClaimBuffer's zero-copy commit/abort contract, and the TransportError / AeronStatus types. Like classic, the feature uses no async/tokio.

Deviations from classic

Canonical list is in the module's # Deviations from classic block; summarised:

  1. The sources take a &GraphBuilder + RunMode, return Result, and reject RunMode::HistoricalFrom at wiring — a live Aeron subscription has no historical timeline, and the threaded mode's channel receiver would block-collect the never-closing poll thread and deadlock at start (register B2, ratified). Classic's spin subscriber silently ran against the fast-forwarded historical clock. The publisher keeps classic's behaviour exactly: its real-time check fires at graph start().
  2. The status side-channel is a plain stream, not a node type. Classic's AeronStatusStream (a MutableNode the producer drove via clear()/record() and wired as an active downstream) has no next twin — next multiplexes status with data over one internal envelope and splits it with map_filter, the zmq_sub shape. Observable behaviour (transition-only emission, derivation order, in-band ordering in threaded mode) is identical; notably the spin mode now carries status in-band too.
  3. The sink is an extension trait returning Stream<()>, not classic's AeronPub returning Rc<dyn Node>.
  4. The mock backends are public test support — next's adapter tests live in tests/ and compile against the public library, so MockSubscriber / MockPublisher can't stay #[cfg(test)]-gated inside the crate.
  5. The plain aeron_sub_fragment never derives status — classic's Option<Rc<RefCell<AeronStatusStream>>> becomes a track_status flag. Same behaviour, no allocation.

Tests

  • tests/aeron_adapter.rs (aeron) — parity port of the classic node-level unit tests, 20 mock-backed cases needing no media driver: burst shaping (one poll ⇒ one atomic burst), the parser None/Err paths, the synthesised default header, all four status-derivation cases for the subscriber (connected / disconnected / terminal close / steady-state no-re-emission) in both modes, the wiring guards, and the four publisher status cases plus the empty-burst fallback. All green.
  • tests/aeron_integration.rs (aeron-integration-test) — parity port of the classic media-driver suite: no-driver connect failure, the three try_claim cases (zero-copy round trip, typed back-pressure, drop-aborts), the fragment_limit cap, spin/threaded/collapse round trips, real fragment positions, both status side-channels over a live driver, the no-dedup publisher, both ChannelUri shapes, and the aeron-rs backend round trip (which is what exercises the SpinThreaded downgrade end to end).

Docker isn't available in the dev sandbox, so the driver-backed suite was not executed locally; it runs in CI.

Other changes

  • examples/aeron/{main.rs,status_circuit_breaker.rs,README.md} — both classic examples ported. The circuit breaker is notable: where classic needed a hand-written MutableNode reading both streams, next expresses it with the ordinary fluent vocabulary (fold latches the transition; a two-input custom_node gates data on it as a passive upstream).
  • .github/workflows/aeron-next-integration.yml + registration in integration-tests.yml. The workflow installs the toolchain rusteron's build script needs — cmake ≥3.30 (Ubuntu 24.04 ships 3.28), clang, uuid-dev, libbsd-dev.
  • next/docs/port-plan.md Phase 4 item 8 and next/docs/deviation-register.md (B2 scope) updated.

The classic Criterion benches (aeron_publication_latency, aeron_subscription_throughput, aeron_transceiver, aeron_allocation_tracking) are not ported — next's bench suite is a separate work item, as for every adapter so far.

Checks

cargo fmt --all -- --check, cargo lint, cargo lint-all (the full workspace all-features gate — it builds here because the Aeron native toolchain was installed for this port, unlike the earlier adapters in this batch), cargo clippy -p wingfoil-next --all-features --all-targets -- -D warnings, and cargo test -p wingfoil-next --features aeron all pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FKhf2tiM8nwtSBzC1St3ga


Generated by Claude Code

claude added 2 commits July 29, 2026 06:28
Ports the classic `wingfoil::adapters::aeron` module onto the Op model,
behind the `aeron` (rusteron-client, C++ FFI) or `aeron-rs` (pure Rust)
backend features, with `aeron-driver` embedding a media driver in-process.

Both polling modes are ported:

- `AeronMode::Spin` rides a busy-spin `custom_node` polling Aeron inside
  the cycle on the graph thread.
- `AeronMode::Threaded` rides `source_at_start` — a background poll thread
  over the channel layer, with classic's exponential idle back-off.

The typed parser with `FragmentHeader` access, the `fragment_limit`
per-poll cap, the `Spin`->`Threaded` downgrade for backends that lock on
poll, both status side-channels, the `ChannelUri` builders, `ClaimBuffer`'s
zero-copy commit/abort contract, and the `TransportError` / `AeronStatus`
types are all carried over. Like classic, the feature uses no `async`/tokio.

Deviations (documented in the module's `# Deviations from classic` block
and the deviation register): the sources take a `&GraphBuilder` + `RunMode`,
return `Result`, and reject `RunMode::HistoricalFrom` at wiring (the
publisher keeps classic's real-time check at graph start); the status
side-channel is a plain stream rather than classic's `AeronStatusStream`
node — status is multiplexed with data over one internal envelope and split
with `map_filter`, so spin mode now carries it in-band too; the sink is the
`AeronSinkOps` extension trait returning `Stream<()>`; and the mock
backends are public test support, since next's tests live outside the lib.

Tests: `tests/aeron_adapter.rs` ports the classic node-level unit tests as
20 mock-backed cases needing no media driver; `tests/aeron_integration.rs`
ports the classic media-driver suite behind `aeron-integration-test`, wired
into CI via `aeron-next-integration.yml` (which installs the cmake >= 3.30 /
clang / uuid / libbsd toolchain rusteron needs). Both classic examples are
ported to `examples/aeron/`.

With this, every classic I/O adapter has a wingfoil-next twin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKhf2tiM8nwtSBzC1St3ga
The main `Build, Test, & Lint` job runs
`cargo test -p wingfoil-next --all-features --lib --tests`, which enables
`aeron-integration-test` and so runs this suite *without*
`--test-threads=1` (the dedicated workflow passes it; the main job cannot).

These tests are inherently non-parallel: the media driver writes its CNC
file to a fixed `AERON_DIR` under a bind-mounted `/dev/shm`, and the Aeron
client is pointed at it through a process-global environment variable that
`start_media_driver` — and `no_driver_connection_fails`, which points it at
an empty directory — rewrite. Two tests in flight at once clobber each
other's driver directory and env var.

Enforce the constraint in code rather than leaving it to the caller: a
process-wide `serial_guard()` taken at the top of every test. The mutex is
poison-tolerant so one failing test does not cascade into "poisoned"
failures for the rest.

This is the same class of defect as the Fluvio suite fix; the remedy
differs because Aeron's per-test driver is fine once serialised, whereas
Fluvio's fixed host ports mean only one cluster can exist at all.

Docker is unavailable in the dev sandbox, so this was verified by
compilation and clippy only; the suite itself runs in CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKhf2tiM8nwtSBzC1St3ga
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.

2 participants