Skip to content

Port FIX adapter to wingfoil-next - #595

Merged
0-jake-0 merged 2 commits into
nextfrom
claude/io-adapter-port-vexhn5
Jul 28, 2026
Merged

Port FIX adapter to wingfoil-next#595
0-jake-0 merged 2 commits into
nextfrom
claude/io-adapter-port-vexhn5

Conversation

@0-jake-0

Copy link
Copy Markdown
Contributor

Ports the classic wingfoil::adapters::fix (Financial Information eXchange) adapter onto the wingfoil-next Op model — Phase 4 item 6 of the port plan. Faithful superset port of the synchronous, poll-based FIX session engine; like classic (and unlike the async adapters) it uses no async/tokio.

What's ported

  • Codec + session state machine — the hand-written FIX 4.4 tag-value codec (encode/decode/build/find/drain), the FixSession logon/logout/heartbeat machine, and handle_initiator/handle_acceptor — a verbatim port.
  • Both poll modes
    • FixPollMode::Threaded rides source_at_start (a background session thread over the channel layer, with initiator reconnect-after-drop, acceptor re-accept, and the lock-free kanal inject channel drained per loop);
    • FixPollMode::AlwaysSpin rides a busy-spin custom_node (non-blocking socket reads on the graph thread), with the socket connect/bind deferred to graph start() via compose_spawn_at_start and a best-effort Logout at teardown.
  • Full public surfacefix_connect / fix_accept (both modes), fix_connect_tls / fix_connect_tls_logon, FixConnection + fix_sub, FixOperators::fix_send, FixSender / SendError, and the pluggable FixLogon auth seam (None / Password / custom Ed25519-signer over the LogonContext).

Engine change (custom_node honours ALWAYS)

A busy-spin custom_node must set the engine's has_always flag so the realtime kernel doesn't park between cycles. custom_node accepted an Activation but ignored the always bit's kernel implication (only poll set it), so an ALWAYS custom node — a socket-polling source — was never driven each cycle. custom_node now sets has_always when activation.always (mirroring poll). Surfaced by the FIX AlwaysSpin source; guarded by fix_same_process_spin. Recorded as register A7. No effect on NONE/SCHEDULES custom nodes (existing custom_node/custom_op tests still pass).

Deviations from classic (all capabilities preserved)

  1. The source factories take a &GraphBuilder + RunMode and reject RunMode::HistoricalFrom at wiring (classic checked real-time-ness at run start(); the message is the same, "real-time"). Every next source wires onto a builder, and the Threaded mode's channel receiver would block-collect a never-closing live stream and deadlock at start.
  2. The sources return Streams (not Rc<dyn Stream>); fix_send returns Result<Stream<()>> and fix_sub a Stream<()>. Connect + logon still happen at start(), Logout at teardown.
  3. The Threaded teardown drops classic's Arc<Mutex<Option<TcpStream>>> socket-shutdown handle for a stop-flag-against-the-200 ms-read-timeout exit (the zmq pattern — no lock on the graph path; teardown costs up to one read-timeout longer).

The canonical deviation list is the adapter's # Deviations from classic module-doc block plus next/docs/deviation-register.md (A7).

Tests & example

  • Unit (inline, no network): encode_decode_roundtrip, custom_logon_fields_are_sent, password_logon_sends_username_and_password, fix_sender_queue_full, fix_sender_closed.
  • Wiring (tests/fix_adapter.rs, feature fix): historical rejection for every source factory + fix_send's realtime-only start check.
  • Integration (tests/fix_integration.rs, feature fix-integration-test): same-process acceptor+initiator round-trip in both poll modes, connection-refused, and initiator-reconnect-after-drop — real loopback TCP, no container.
  • Example: classic fix_loopback ported to examples/fix_adapter.rs. The other four classic fix examples are peer-/credential-dependent (fix_client/fix_echo_server need a counterparty; lmax_demo/lmax_instruments need an LMAX account) and are not ported.

The credentialed LMAX-demo integration tests are not ported (external credentials).

Bookkeeping

next/docs/port-plan.md (fix marked ✅), next/docs/deviation-register.md (A7), the /new-adapter-next skill (busy-spin custom_node subsection), and CI (.github/workflows/fix-next-integration.yml + hub registration).

Verification

cargo fmt --all --check, cargo clippy -p wingfoil-next (default) and --all-features clean under -D warnings, all wingfoil-next test suites green with --features fix, the 4 integration tests pass with --features fix-integration-test -- --test-threads=1, and the example runs. (Substituted the scoped cargo clippy -p wingfoil-next --all-features for the workspace lint-all per the sandbox caveat — the classic aeron C toolchain is unavailable here; CI runs the full workspace lint.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01HA2a5JgbvUJCqrRAswbS7f


Generated by Claude Code

claude added 2 commits July 28, 2026 17:53
Port the classic `wingfoil::adapters::fix` FIX (Financial Information
eXchange) protocol adapter onto the next Op-pattern engine, behind the
`fix` feature. Like classic (and unlike the async adapters) it uses no
async/tokio: the hand-written FIX 4.4 tag-value codec, the FixSession
state machine, and both poll modes are a verbatim port.

- Both poll modes: `Threaded` rides `source_at_start` (a background
  session thread over the channel layer, with initiator reconnect,
  acceptor re-accept, and the lock-free kanal inject channel drained per
  loop); `AlwaysSpin` rides a busy-spin custom_node (non-blocking socket
  reads on the graph thread) with the connect/bind deferred to start()
  and a best-effort Logout at teardown.
- Full public surface: fix_connect / fix_accept (both modes),
  fix_connect_tls / fix_connect_tls_logon, FixConnection + fix_sub,
  FixOperators::fix_send, FixSender / SendError, and the FixLogon
  (None / Password / custom Ed25519-signer) auth seam.
- Engine fix: custom_node now honours Activation::ALWAYS (sets
  has_always) so a busy-spin custom node is actually driven each realtime
  cycle — a latent gap the FIX spin source surfaced (register A7).

Deviations (all classic capabilities preserved): the source factories
take &GraphBuilder + RunMode and reject RunMode::HistoricalFrom at wiring
(a live session has no historical timeline to replay); the sources return
Streams (not Rc<dyn Stream>); and the Threaded teardown drops classic's
Arc<Mutex<Option<TcpStream>>> socket-shutdown handle for a
stop-flag-against-the-read-timeout exit (the zmq pattern — no lock on the
graph path).

Tests: no-service wiring tests in tests/fix_adapter.rs (fix);
same-process socket round-trip + reconnect + connection-refused parity
tests in tests/fix_integration.rs (fix-integration-test, real loopback,
no container); classic fix_loopback example ported to
examples/fix_adapter.rs. Port-plan, deviation register, adapter skill, and
CI hub updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HA2a5JgbvUJCqrRAswbS7f
… not ported

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HA2a5JgbvUJCqrRAswbS7f
@0-jake-0
0-jake-0 merged commit 264ca46 into next Jul 28, 2026
7 checks passed
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