Port FIX adapter to wingfoil-next - #595
Merged
Merged
Conversation
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
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.
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 noasync/tokio.What's ported
encode/decode/build/find/drain), theFixSessionlogon/logout/heartbeat machine, andhandle_initiator/handle_acceptor— a verbatim port.FixPollMode::Threadedridessource_at_start(a background session thread over thechannellayer, with initiator reconnect-after-drop, acceptor re-accept, and the lock-freekanalinject channel drained per loop);FixPollMode::AlwaysSpinrides a busy-spincustom_node(non-blocking socket reads on the graph thread), with the socket connect/bind deferred to graphstart()viacompose_spawn_at_startand a best-effort Logout at teardown.fix_connect/fix_accept(both modes),fix_connect_tls/fix_connect_tls_logon,FixConnection+fix_sub,FixOperators::fix_send,FixSender/SendError, and the pluggableFixLogonauth seam (None / Password /customEd25519-signer over theLogonContext).Engine change (custom_node honours ALWAYS)
A busy-spin
custom_nodemust set the engine'shas_alwaysflag so the realtime kernel doesn't park between cycles.custom_nodeaccepted anActivationbut ignored thealwaysbit's kernel implication (onlypollset it), so anALWAYScustom node — a socket-polling source — was never driven each cycle.custom_nodenow setshas_alwayswhenactivation.always(mirroringpoll). Surfaced by the FIXAlwaysSpinsource; guarded byfix_same_process_spin. Recorded as register A7. No effect onNONE/SCHEDULEScustom nodes (existingcustom_node/custom_optests still pass).Deviations from classic (all capabilities preserved)
&GraphBuilder+RunModeand rejectRunMode::HistoricalFromat wiring (classic checked real-time-ness at runstart(); the message is the same, "real-time"). Every next source wires onto a builder, and theThreadedmode's channel receiver would block-collect a never-closing live stream and deadlock atstart.Streams (notRc<dyn Stream>);fix_sendreturnsResult<Stream<()>>andfix_subaStream<()>. Connect + logon still happen atstart(), Logout at teardown.Threadedteardown drops classic'sArc<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 classicmodule-doc block plusnext/docs/deviation-register.md(A7).Tests & example
encode_decode_roundtrip,custom_logon_fields_are_sent,password_logon_sends_username_and_password,fix_sender_queue_full,fix_sender_closed.tests/fix_adapter.rs, featurefix): historical rejection for every source factory +fix_send's realtime-only start check.tests/fix_integration.rs, featurefix-integration-test): same-process acceptor+initiator round-trip in both poll modes, connection-refused, and initiator-reconnect-after-drop — real loopback TCP, no container.fix_loopbackported toexamples/fix_adapter.rs. The other four classic fix examples are peer-/credential-dependent (fix_client/fix_echo_serverneed a counterparty;lmax_demo/lmax_instrumentsneed 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-nextskill (busy-spincustom_nodesubsection), and CI (.github/workflows/fix-next-integration.yml+ hub registration).Verification
cargo fmt --all --check,cargo clippy -p wingfoil-next(default) and--all-featuresclean under-D warnings, allwingfoil-nexttest suites green with--features fix, the 4 integration tests pass with--features fix-integration-test -- --test-threads=1, and the example runs. (Substituted the scopedcargo clippy -p wingfoil-next --all-featuresfor the workspacelint-allper 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