Skip to content

feat(transport): add mantis-transport crate with blocking WS feed threads#19

Merged
Milerius merged 4 commits intomainfrom
feat/ws-transport-ingest
Apr 4, 2026
Merged

feat(transport): add mantis-transport crate with blocking WS feed threads#19
Milerius merged 4 commits intomainfrom
feat/ws-transport-ingest

Conversation

@brohamgoham
Copy link
Copy Markdown
Collaborator

@brohamgoham brohamgoham commented Apr 4, 2026

Summary:

  • New mantis-transport crate — WebSocket transport ingest layer
  • FeedThread: dedicated pinned blocking thread per feed with tungstenite (sync) + rustls TLS
  • WsConnection: managed WS connection with automatic PING heartbeats (10s Polymarket, 30s Binance)
  • BackoffConfig: exponential reconnection backoff 1s→30s with ±12.5% jitter
  • SocketTuning: CPU pinning via core_affinity, SO_BUSY_POLL (Linux, behind tuning feature)
  • Callback-based API — FeedThread::spawn(config, |msg| { ... }) returns FeedHandle with shutdown + monitoring counters
  • No tokio, no async, no Mutex — blocking IO with lock-free AtomicU64 counters
  • 5 integration tests (message receive, graceful shutdown, callback stop, reconnection, subscription)

Test plan:

cargo test -p mantis-transport — 5/5 pass
cargo clippy -p mantis-transport --all-targets -- -D warnings — zero warnings
cargo fmt -p mantis-transport -- --check — clean

Summary by CodeRabbit

  • New Features

    • WebSocket transport with automatic reconnection, backoff and heartbeat.
    • Binance reference and Polymarket market feed adapters.
    • Instrument registry for stable instrument IDs, bindings and lifecycle operations.
    • CPU pinning and socket tuning helpers for low-latency ingestion.
  • Documentation

    • Detailed transport and registry READMEs and a Polymarket probe script.
  • Tests

    • Integration tests for feeds, reconnection, subscriptions; optional live feed tests.
  • Chores

    • Workspace/toolchain and license allowlist updates; VCS ignore updated.

@brohamgoham brohamgoham self-assigned this Apr 4, 2026
@brohamgoham brohamgoham requested a review from Milerius April 4, 2026 06:31
@brohamgoham
Copy link
Copy Markdown
Collaborator Author

PR Opened, @Milerias

Proceeding with Phase B:

What's Next

Phase B is exchange-specific connections — wiring the generic FeedThread to actual Polymarket and Binance endpoints. The key work:

  1. Polymarket market WS — subscription JSON with token IDs, custom_feature_enabled: true, handle PONG text frames
  2. Polymarket user WS — auth with API key/secret/passphrase, subscribe with condition IDs
  3. Binance reference WS — URL-based subscription (fstream.binance.com for your geo), no subscription message needed
  4. Stale detection — track last message time per feed, flag if no data within threshold
    Live integration tests — behind live-tests feature flag, hit real endpoints

@Milerius
Copy link
Copy Markdown
Owner

Milerius commented Apr 4, 2026

Transport Layer PR Design Review

Overall Direction

  • acceptable

What This PR Gets Right Architecturally

  • It opens the transport layer at the system edge, not inside the engine crates. A dedicated mantis-transport crate for connection lifecycle, reconnection, and feed thread ownership is the right boundary direction.
  • The thread model is aligned with the low-latency architecture: blocking dedicated feed threads, no async runtime leaking into owner-thread code, no Mutex-centric coordination, and only simple atomic monitoring state.
  • It keeps the hot-path event model intact. This phase does not push websocket/client objects into mantis-events or mantis-types, which is exactly the discipline you want.
  • Taking the instrument-registry design doc as the target architecture, this Phase A move is directionally compatible with the intended model of canonical stable instruments plus venue-specific bindings. Nothing here yet prevents venue key -> InstrumentId -> InstrumentMeta from being inserted at the boundary before event emission.
  • For Polymarket specifically, nothing here yet forces raw token IDs to become the permanent internal identity. That is good, and it leaves room for the registry model of stable logical instruments with rotating current/next token bindings.

Main Design Risks Introduced By This PR

  • The main architectural risk is the public seam: FeedThread::spawn(config, FnMut(&str) -> bool). If this callback shape becomes the durable extension point, parsing, registry lookup, and event emission can easily drift into arbitrary caller code instead of staying transport-owned at the boundary.
  • That same &str seam is a poor fit for the planned simd-json::from_slice(&mut [u8]) style parse path. If Phase C really wants borrowed/in-place boundary parsing, this Phase A API is pointing in a different direction and may force an avoidable copy or a later API break.
  • The instrument-registry doc makes a very important split: hot boundary path should only do read-only venue id -> InstrumentId and InstrumentId -> InstrumentMeta, while registry mutation and binding rotation stay on the control path. The current generic callback does not yet enforce that separation, so it would be easy for later phases to mix control-path logic into the feed loop.
  • WsConnection currently mixes generic websocket transport with venue/application heartbeat behavior by owning text PING. Architecturally, I would keep generic WS ping/pong in the connection layer and push venue-specific heartbeat semantics into exchange adapters. Is the intent for venue heartbeat policy to live in WsConnection, or only in Phase B feed adapters?
  • The crate docs/dependency story currently read as if this crate already owns HotEvent/SPSC emission, but the actual API is still raw-message transport scaffolding. That mismatch can cause downstream code to couple to the wrong abstraction boundary.

Compatibility With Future Instrument Registry

  • Directionally, this is compatible with the registry design doc. The PR has not yet leaked symbol strings or token IDs into the engine event language.
  • It should be straightforward later to insert the exact read-mostly lookup pair the registry doc calls for: venue-specific key -> InstrumentId, then InstrumentId -> InstrumentMeta.
  • The important thing is to preserve the registry doc's hot-path/control-path split. The feed thread should do read-only resolution during ingest; instrument discovery, Polymarket current/next promotion, and binding-table rebuilds must stay outside that loop.
  • The long-term boundary should match the registry doc's intended shape: raw frame -> venue parse -> registry resolve -> InstrumentMeta conversion -> HotEvent batch -> SPSC publish. That still composes well with this PR, but the current public API does not yet enforce it.

Polymarket Rotation Compatibility

  • This Phase A opening is compatible with the Polymarket model described in the registry doc: stable logical instruments with rotating token bindings layered underneath.
  • Because the PR does not yet normalize raw token IDs into internal event identity, it has not baked in token-id-as-identity. That is the key thing to preserve.
  • The future risk is behavioral rather than structural: if later phases start hanging business logic directly off raw asset_id strings exposed by the callback, the system will drift away from the intended InstrumentId-centric design before the registry lands.

What Should Be Corrected Now

  • Do not let FnMut(&str) -> bool become the stable long-term seam. Either keep it explicitly provisional or move soon to a transport-owned decoder/emitter boundary that can support mutable frame buffers and in-place parsing.
  • Make the registry fit explicit in the transport design now: feed loops may perform read-only registry lookups, but registry mutation, binding rotation, and discovery logic remain off the hot ingest path.
  • Separate generic websocket responsibilities from venue-specific heartbeat rules now, before more venues are added on top of the same abstraction.
  • Make the Phase A scope explicit in the crate API/docs: this PR is connection scaffolding, not yet the final normalization/emission boundary. That avoids downstream consumers anchoring on the wrong abstraction.
  • Decide now that registry resolution and HotEvent emission stay on the IO-thread side of the boundary, not in downstream engine code that happens to receive raw messages.

What Can Safely Wait For Later Phases

  • The actual generic instrument registry implementation.
  • InstrumentMeta lookup and FixedI64 -> Ticks/Lots conversion.
  • HotEvent batch assembly, LAST_IN_BATCH, and per-queue SeqNum handling.
  • Venue-specific live endpoint coverage and the full Phase B/Phase C parsers.
  • Micro-optimizing the parse path, as long as the API seam is kept compatible with the intended zero-alloc/in-place direction.

Final Recommendation

  • This is a reasonable Phase A opening move and mostly the right architectural direction: transport concerns are being opened at the edge, on dedicated IO threads, without contaminating the engine hot path. It also still fits the instrument-registry design doc's target model of stable canonical instruments plus venue bindings. I would build on it, but I would correct the public seam before later phases harden around it: raw &str callbacks, lack of an enforced hot-path/control-path split around registry usage, and generic-layer text heartbeat policy are the three places most likely to create architectural drag if left as the foundation.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 4, 2026

Benchmark Report

Commit: 035b0e8d7f2617c2744e73b9e10183f0b8529d5a

Linux

CPU: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz | Arch: x86_64 | Compiler: rustc 1.96.0-nightly (2972b5e59 2026-04-03)

Latency (ns/op, lower is better)

Single Push+Pop

Element crossbeam drogalis-cpp mantis/copy mantis/general mantis/inline rigtorp-cpp rtrb
msg48 21.11 9.59 7.7 🏆 24.35 24.35 9.54 19.23
msg64 15.8 5.27 2.93 🏆 10.84 10.88 5.27 5.4
u64 13.75 3.54 2.24 🏆 - 2.67 2.93 3.24

Burst 100

Element crossbeam drogalis-cpp mantis/copy mantis/inline rigtorp-cpp rtrb
msg48 3172.64 1636.15 424.72 🏆 1948.12 1631.56 2082.72
msg64 2582.77 682.08 431.67 🏆 1057.75 673.9 673.22
u64 2532.61 434.04 389.2 🏆 426.87 472.4 471.73

Burst 1000

Element crossbeam drogalis-cpp mantis/copy mantis/inline rigtorp-cpp rtrb
msg48 31759.84 16687.16 4623.56 🏆 19459.25 16627.75 20572.99
msg64 25916.72 7470.7 4710.04 🏆 10852.49 7384.74 7608.73
u64 25369.53 4445.97 4366.86 4325.87 🏆 4941.98 4939.67

Batch 100

Element mantis/copy
msg48 74.59 🏆
u64 13.27 🏆

Batch 1000

Element mantis/copy
msg48 1078.81 🏆
u64 73.1 🏆

Full Drain

Element mantis/inline
u64 4639.39 🏆
Instructions per Op (lower is better)
Full results (all fields)
Workload ns/op p50 p99 cycles insns bmiss l1d llc
spsc/inline/single_item/u64 2.67 2.7 2.8 12.1 - - - -
spsc/inline/single_item/msg48 24.35 24.3 24.6 80.8 - - - -
spsc/inline/single_item/msg64 10.88 10.8 11.2 50.6 - - - -
spsc/inline/burst_100/u64 426.87 426.2 437.4 1560.5 - - - -
spsc/inline/burst_100/msg48 1948.12 1945.7 1982.1 7838.4 - - - -
spsc/inline/burst_100/msg64 1057.75 1055.8 1093 4418.2 - - - -
spsc/inline/burst_1000/u64 4325.87 4319.4 4435.5 18641.7 - - - -
spsc/inline/burst_1000/msg48 19459.25 19440.1 19789.8 91887.3 - - - -
spsc/inline/burst_1000/msg64 10852.49 10830.9 11230.5 54088.1 - - - -
spsc/inline/full_drain/u64 4639.39 4636.9 4678.4 20880 - - - -
copy/single/u64 2.24 2.2 2.3 9 - - - -
copy/single/msg48 7.7 7.7 7.9 28.6 - - - -
copy/single/msg64 2.93 2.9 3.1 9.2 - - - -
general/single/msg48 24.35 24.3 24.7 80.8 - - - -
general/single/msg64 10.84 10.8 11.1 50.5 - - - -
copy/burst/100/u64 389.2 388.2 394.2 1355.7 - - - -
copy/burst/100/msg48 424.72 424 437.8 1549.5 - - - -
copy/burst/100/msg64 431.67 431.4 436.5 1590 - - - -
copy/burst/1000/u64 4366.86 4354.5 4539.3 18880.4 - - - -
copy/burst/1000/msg48 4623.56 4616.5 4693.6 20738.1 - - - -
copy/burst/1000/msg64 4710.04 4707.3 4757 21400.6 - - - -
copy/batch/100/u64 13.27 13.2 14 41.8 - - - -
copy/batch/100/msg48 74.59 74.4 77.6 323.7 - - - -
copy/batch/1000/u64 73.1 72.9 76.3 309.9 - - - -
copy/batch/1000/msg48 1078.81 1074.5 1159.1 4535.8 - - - -
spsc/rtrb/single_item/u64 3.24 3.2 3.4 10.9 - - - -
spsc/rtrb/single_item/msg48 19.23 19.2 21.2 79.1 - - - -
spsc/rtrb/single_item/msg64 5.4 5.4 6.4 18.9 - - - -
spsc/rtrb/burst_100/u64 471.73 471.2 481.1 1824.1 - - - -
spsc/rtrb/burst_100/msg48 2082.72 2101.3 2241.9 8219.3 - - - -
spsc/rtrb/burst_100/msg64 673.22 657.9 781.4 3125.2 - - - -
spsc/rtrb/burst_1000/u64 4939.67 4936.9 4997.8 23100.2 - - - -
spsc/rtrb/burst_1000/msg48 20572.99 20843.7 22203.7 94083.4 - - - -
spsc/rtrb/burst_1000/msg64 7608.73 7974.8 8724.6 33330.3 - - - -
spsc/crossbeam/single_item/u64 13.75 13.7 13.9 48.3 - - - -
spsc/crossbeam/single_item/msg48 21.11 21.1 21.6 97.4 - - - -
spsc/crossbeam/single_item/msg64 15.8 15.8 16 60.4 - - - -
spsc/crossbeam/burst_100/u64 2532.61 2527.4 2599.7 11899.1 - - - -
spsc/crossbeam/burst_100/msg48 3172.64 3169.2 3221.9 11584.2 - - - -
spsc/crossbeam/burst_100/msg64 2582.77 2580 2624.3 12308.6 - - - -
spsc/crossbeam/burst_1000/u64 25369.53 25350.7 25693.6 94854.5 - - - -
spsc/crossbeam/burst_1000/msg48 31759.84 31722.8 32461.4 133078.1 - - - -
spsc/crossbeam/burst_1000/msg64 25916.72 25892.9 26321.6 98073 - - - -
spsc/rigtorp/single_item/u64 2.93 2.9 3.1 9.2 - - - -
spsc/rigtorp/single_item/msg48 9.54 10.1 11.4 35.9 - - - -
spsc/rigtorp/single_item/msg64 5.27 5.3 5.6 23.7 - - - -
spsc/rigtorp/burst_100/u64 472.4 472 477.5 1828.4 - - - -
spsc/rigtorp/burst_100/msg48 1631.56 1676.8 1838.7 6467.1 - - - -
spsc/rigtorp/burst_100/msg64 673.9 665 763.5 2918.4 - - - -
spsc/rigtorp/burst_1000/u64 4941.98 4936.3 5086.1 23088.7 - - - -
spsc/rigtorp/burst_1000/msg48 16627.75 17082 18641.3 77349.3 - - - -
spsc/rigtorp/burst_1000/msg64 7384.74 7666.3 8343.3 31645.6 - - - -
spsc/drogalis/single_item/u64 3.54 3.5 3.7 12.3 - - - -
spsc/drogalis/single_item/msg48 9.59 10.1 11.3 36.6 - - - -
spsc/drogalis/single_item/msg64 5.27 5.3 5.5 23.6 - - - -
spsc/drogalis/burst_100/u64 434.04 433.6 448.3 1599.6 - - - -
spsc/drogalis/burst_100/msg48 1636.15 1681.2 1842.4 6493.6 - - - -
spsc/drogalis/burst_100/msg64 682.08 678.9 780.1 2362 - - - -
spsc/drogalis/burst_1000/u64 4445.97 4438.4 4585.7 19514.3 - - - -
spsc/drogalis/burst_1000/msg48 16687.16 17057.8 18829.9 79111.7 - - - -
spsc/drogalis/burst_1000/msg64 7470.7 7709 8480.5 32672.9 - - - -
macOS

CPU: Apple M1 (Virtual) | Arch: aarch64 | Compiler: rustc 1.96.0-nightly (2972b5e59 2026-04-03)

Latency (ns/op, lower is better)

Single Push+Pop

Element crossbeam drogalis-cpp mantis/copy mantis/general mantis/inline rigtorp-cpp rtrb
msg48 10.71 9.78 7.93 🏆 9.26 9.26 9.84 8.98
msg64 10.76 9.34 7.67 🏆 9.92 10.02 9.77 10.05
u64 8.33 8.89 6.34 🏆 - 7.35 8.5 7.03

Burst 100

Element crossbeam drogalis-cpp mantis/copy mantis/inline rigtorp-cpp rtrb
msg48 1796.33 1739.46 320.61 🏆 496.69 1751.56 744.37
msg64 2094.49 1706.33 380.97 🏆 603.34 1781.14 817.11
u64 1657.58 1491.61 263.38 243.5 🏆 1560.58 633.39

Burst 1000

Element crossbeam drogalis-cpp mantis/copy mantis/inline rigtorp-cpp rtrb
msg48 19033.01 18144.19 4230.39 🏆 5098.61 17985.75 7363.97
msg64 19341.66 31121.95 4807.67 🏆 6467.79 33045.45 21277.34
u64 19471.36 14779.29 3380.52 2383.6 🏆 15324.62 6568.66

Batch 100

Element mantis/copy
msg48 87.78 🏆
u64 20.94 🏆

Batch 1000

Element mantis/copy
msg48 791.35 🏆
u64 129.91 🏆

Full Drain

Element mantis/inline
u64 3774.38 🏆
Instructions per Op (lower is better)
Full results (all fields)
Workload ns/op p50 p99 cycles insns bmiss l1d llc
spsc/inline/single_item/u64 7.35 7.3 7.7 0.2 - - - -
spsc/inline/single_item/msg48 9.26 9.2 9.5 0.3 - - - -
spsc/inline/single_item/msg64 10.02 9.9 10.8 0.4 - - - -
spsc/inline/burst_100/u64 243.5 240.9 257.9 8.1 - - - -
spsc/inline/burst_100/msg48 496.69 491.4 521.4 16.8 - - - -
spsc/inline/burst_100/msg64 603.34 593.7 645.6 22.8 - - - -
spsc/inline/burst_1000/u64 2383.6 2362 2547.9 90.9 - - - -
spsc/inline/burst_1000/msg48 5098.61 5081.2 5246.3 211.3 - - - -
spsc/inline/burst_1000/msg64 6467.79 6404.1 6804.4 206.4 - - - -
spsc/inline/full_drain/u64 3774.38 3717.5 4083.9 127.8 - - - -
copy/single/u64 6.34 6.3 6.6 0.2 - - - -
copy/single/msg48 7.93 7.9 8.2 0.3 - - - -
copy/single/msg64 7.67 7.6 8 0.2 - - - -
general/single/msg48 9.26 9.2 9.6 0.3 - - - -
general/single/msg64 9.92 9.9 10.2 0.4 - - - -
copy/burst/100/u64 263.38 262.2 271.8 9.2 - - - -
copy/burst/100/msg48 320.61 314.4 371.5 12.4 - - - -
copy/burst/100/msg64 380.97 379.5 391 11.3 - - - -
copy/burst/1000/u64 3380.52 3385.5 3431.8 110.1 - - - -
copy/burst/1000/msg48 4230.39 4134.9 4637 149.7 - - - -
copy/burst/1000/msg64 4807.67 4791.2 5098.5 200.7 - - - -
copy/batch/100/u64 20.94 20.8 22.6 0.9 - - - -
copy/batch/100/msg48 87.78 86.8 106 3.5 - - - -
copy/batch/1000/u64 129.91 125.7 169.9 4.7 - - - -
copy/batch/1000/msg48 791.35 778.6 924.4 25.1 - - - -
spsc/rtrb/single_item/u64 7.03 7 7.7 0.2 - - - -
spsc/rtrb/single_item/msg48 8.98 9 9.5 0.3 - - - -
spsc/rtrb/single_item/msg64 10.05 10.1 11 0.4 - - - -
spsc/rtrb/burst_100/u64 633.39 631 654.7 26 - - - -
spsc/rtrb/burst_100/msg48 744.37 726.4 968 22.9 - - - -
spsc/rtrb/burst_100/msg64 817.11 801.2 952.7 25.3 - - - -
spsc/rtrb/burst_1000/u64 6568.66 6567.3 7143.6 207.3 - - - -
spsc/rtrb/burst_1000/msg48 7363.97 7333.2 7863.3 249.5 - - - -
spsc/rtrb/burst_1000/msg64 21277.34 21261.6 22366.3 914.5 - - - -
spsc/crossbeam/single_item/u64 8.33 8.2 9 0.3 - - - -
spsc/crossbeam/single_item/msg48 10.71 10.2 13.8 0.4 - - - -
spsc/crossbeam/single_item/msg64 10.76 10.7 11.2 0.4 - - - -
spsc/crossbeam/burst_100/u64 1657.58 1622.2 1814.3 53.3 - - - -
spsc/crossbeam/burst_100/msg48 1796.33 1781.8 2086.8 57.1 - - - -
spsc/crossbeam/burst_100/msg64 2094.49 2037.6 2474 72.4 - - - -
spsc/crossbeam/burst_1000/u64 19471.36 18833.6 24504.7 827.2 - - - -
spsc/crossbeam/burst_1000/msg48 19033.01 18560.8 22953.9 840.1 - - - -
spsc/crossbeam/burst_1000/msg64 19341.66 19253.4 22053.6 839.3 - - - -
spsc/rigtorp/single_item/u64 8.5 8.4 9.1 0.3 - - - -
spsc/rigtorp/single_item/msg48 9.84 9.9 10.5 0.4 - - - -
spsc/rigtorp/single_item/msg64 9.77 9.6 10.5 0.4 - - - -
spsc/rigtorp/burst_100/u64 1560.58 1536 1712.7 47 - - - -
spsc/rigtorp/burst_100/msg48 1751.56 1717.2 1880.2 59.3 - - - -
spsc/rigtorp/burst_100/msg64 1781.14 1774.8 1912.8 59.8 - - - -
spsc/rigtorp/burst_1000/u64 15324.62 15165.9 16034.1 559.5 - - - -
spsc/rigtorp/burst_1000/msg48 17985.75 17715.7 19175.1 702 - - - -
spsc/rigtorp/burst_1000/msg64 33045.45 31819.5 39772.1 1149.7 - - - -
spsc/drogalis/single_item/u64 8.89 8.7 10.3 0.3 - - - -
spsc/drogalis/single_item/msg48 9.78 9.8 10.6 0.4 - - - -
spsc/drogalis/single_item/msg64 9.34 9.2 10.1 0.3 - - - -
spsc/drogalis/burst_100/u64 1491.61 1502.9 1586.6 62.9 - - - -
spsc/drogalis/burst_100/msg48 1739.46 1715.8 1837.1 56.6 - - - -
spsc/drogalis/burst_100/msg64 1706.33 1688.5 1850.6 54.9 - - - -
spsc/drogalis/burst_1000/u64 14779.29 14654.8 16269.7 506.7 - - - -
spsc/drogalis/burst_1000/msg48 18144.19 17888.8 19429.3 704.8 - - - -
spsc/drogalis/burst_1000/msg64 31121.95 30782.1 33068.1 1121.8 - - - -

Fixed-Point Arithmetic (mantis-fixed)

Linux

CPU: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz | Arch: x86_64 | Compiler: rustc 1.96.0-nightly (2972b5e59 2026-04-03)

checked_add

Variant ns/op
FixedI64_6_ 0.8
raw_i64 0.8

checked_div

Variant ns/op
trunc 5.2
round 6.1

checked_mul_trunc

Variant ns/op
D=4 3.18
D=2 3.43
D=8 3.48
D=6 3.51

display

Variant ns/op
FixedI64_6_ 42.13

mul_round_vs_trunc

Variant ns/op
trunc 3.48
round 4.26

parse

Variant ns/op
short 18.99
integer_only 25.25
full_precision 32.63

rescale

Variant ns/op
D6_to_D2_trunc 0.66
D2_to_D8_widen 0.67
macOS

CPU: Apple M1 (Virtual) | Arch: arm64 | Compiler: rustc 1.96.0-nightly (2972b5e59 2026-04-03)

checked_add

Variant ns/op
FixedI64_6_ 1.53
raw_i64 1.6

checked_div

Variant ns/op
trunc 3.45
round 4.99

checked_mul_trunc

Variant ns/op
D=6 1.87
D=8 1.88
D=4 1.93
D=2 2.05

display

Variant ns/op
FixedI64_6_ 34.06

mul_round_vs_trunc

Variant ns/op
trunc 1.96
round 2.36

parse

Variant ns/op
short 11.94
integer_only 17.23
full_precision 25.16

rescale

Variant ns/op
D6_to_D2_trunc 0.53
D2_to_D8_widen 0.54

@brohamgoham
Copy link
Copy Markdown
Collaborator Author

brohamgoham commented Apr 4, 2026

@Milerius

  1. &str vs &mut [u8] recommendation is Noted. The callback signature will change to &mut [u8] in Phase C when simd-json lands. For Phase A this is test scaffolding, not the final normalization boundary.

  2. Text PING mixing: Polymarket requires text "PING" (not WS frame Ping). Both are handled. Phase B venue adapters will own heartbeat policy per exchange. The current WsConfig::ping_interval is venue-configurable already.

  3. Registry separation: Already addressed. registry-epic plan now exists with the full design. Phase C header updated to BLOCKS on mantis-events + mantis-registry. The feed loop will do read-only registry lookups; mutation stays on the control path.

The rest (enforce hot/control split in types, finalize the normalization API) are Phase C concerns. Phase A is intentionally minimal.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

Adds two new workspace crates—mantis-transport (blocking, CPU‑pinned WebSocket ingest layer with venue adapters and feed threading) and mantis-registry (instrument registry with bindings and reverse indexes). Also updates workspace metadata, license allowlist, adds tests, a Polymarket probe script, and a .gitignore entry.

Changes

Cohort / File(s) Summary
Workspace & config
/.gitignore, Cargo.toml, deny.toml
Added .claude/datapipeline to gitignore; bumped workspace rust-version to 1.95.0 and declared mantis-transport & mantis-registry workspace deps; extended license allowlist with Zlib and CDLA-Permissive-2.0.
Transport crate manifest & docs
crates/transport/Cargo.toml, crates/transport/README.md
New mantis-transport Cargo manifest and README describing blocking pinned-thread WebSocket ingest model, features, tuning, and testing.
Transport crate root & re-exports
crates/transport/src/lib.rs
New crate root exposing binance, polymarket modules and re-exporting Feed*, BackoffConfig, SocketTuning, and Ws*.
Feed thread core
crates/transport/src/feed.rs, crates/transport/tests/feed_thread.rs
New blocking, pinned FeedThread with automatic reconnect/backoff, FeedConfig/BackoffConfig, FeedHandle (shutdown/monitoring), and integration tests for ingestion, reconnect, subscribe, and shutdown semantics.
WebSocket layer
crates/transport/src/ws.rs
WsConfig, WsConnection, and WsError implementing TLS-capable tungstenite client, optional subscribe message, ping heartbeat, read semantics, and socket option handling.
Socket tuning
crates/transport/src/tuning.rs
SocketTuning helpers for optional CPU affinity and Linux SO_BUSY_POLL (gated by tuning feature).
Venue adapters — Binance
crates/transport/src/binance/...
crates/transport/src/binance/mod.rs, crates/transport/src/binance/reference.rs
Binance reference feed adapter: URL builder for single/multi-symbol bookTicker, BinanceReferenceConfig, and spawn_reference_feed that builds FeedConfig and spawns the feed. Includes unit tests.
Venue adapters — Polymarket
crates/transport/src/polymarket/...
crates/transport/src/polymarket/mod.rs, crates/transport/src/polymarket/market.rs
Polymarket market feed adapter: WS_MARKET_URL, PolymarketMarketConfig, subscription payload construction, and spawn_market_feed spawning FeedThread.
Transport live tests
crates/transport/tests/live_feeds.rs
Feature-gated live-network tests (Binance & Polymarket) validating message flow and reconnect behavior (requires live-tests).
Registry crate manifest & docs
crates/registry/Cargo.toml, crates/registry/README.md
New mantis-registry Cargo manifest and README describing instrument registry semantics and lifecycle.
Registry types & records
crates/registry/src/types.rs, crates/registry/src/record.rs, crates/registry/src/bindings.rs
Domain enums and structs: InstrumentClass, Asset, Timeframe, OutcomeSide, InstrumentKey, CanonicalInstrument, InstrumentRecord, plus Binance/Polymarket binding types.
Registry core & errors
crates/registry/src/lib.rs, crates/registry/src/error.rs
InstrumentRegistry<const D: u8> implementation with insertion, prediction-pair insertion, Polymarket bind/promote/unbind APIs, reverse indexes, read APIs, unit tests; RegistryError enum with Display/Error impl.
Tooling script
scripts/polymarket_ws_probe.py
New async Python probe that discovers Polymarket markets (Gamma API), subscribes to CLOB WS, captures/normalizes events, and prints summary statistics and samples.

Sequence Diagram

sequenceDiagram
    participant Config as Venue Config
    participant Adapter as Venue Adapter
    participant FeedThread as FeedThread\n(Pinned)
    participant Ws as WsConnection\n(TLS / Heartbeat)
    participant Venue as Venue WebSocket
    participant Callback as User Callback

    Config->>Adapter: build FeedConfig + callback
    Adapter->>FeedThread: spawn(config, on_message)
    FeedThread->>FeedThread: apply affinity / tuning

    loop Reconnect Loop
        FeedThread->>Ws: WsConnection::connect(WsConfig)
        Ws->>Venue: TLS handshake + WS upgrade
        alt connected
            Ws->>Venue: send subscribe_msg (optional)
            loop Read Loop
                FeedThread->>Ws: read_text()
                alt Text message
                    Ws-->>FeedThread: Ok(Some(text))
                    FeedThread->>FeedThread: msg_count += 1
                    FeedThread->>Callback: on_message(&str)
                    alt Callback -> true
                        Callback-->>FeedThread: continue
                    else Callback -> false
                        Callback-->>FeedThread: stop -> exit
                    end
                else Control / PONG / Non-text
                    Ws-->>FeedThread: Ok(None)
                else Closed
                    Ws-->>FeedThread: WsError::Closed -> reconnect
                end
            end
        else connect error
            FeedThread->>FeedThread: exponential backoff + reconnects += 1
        end
    end

    FeedThread->>FeedThread: await shutdown signal / join
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hop on sockets, pin my paws, and spin,
Reading ticks and pings where market feeds begin,
Backoff beats and heartbeats in a loop so neat,
I nudge the thread to stop or help it meet,
Counters hum—happy rabbit on the beat.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(transport): add mantis-transport crate with blocking WS feed threads' clearly summarizes the main change: adding a new transport crate with blocking WebSocket feed threads. It is concise, specific, and directly related to the primary change in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ws-transport-ingest

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (3)
.gitignore (1)

50-50: Consider adding a descriptive comment for consistency.

Other ignore patterns in this file have explanatory comments (e.g., lines 1-2, 6-7, 12-13). Adding a brief comment above .claude/datapipeline would improve maintainability and keep the file consistent with the established pattern.

📋 Example
+# Claude development pipeline artifacts
 .claude/datapipeline
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore at line 50, Add a short descriptive comment above the
.claude/datapipeline ignore entry in .gitignore explaining what that directory
contains or why it's ignored (e.g., "Claude local data pipeline cache" or
"generated datapipeline artifacts for Claude - do not commit"). Locate the line
with the pattern ".claude/datapipeline" and insert a one-line comment
immediately above it to match the existing explanatory style used for other
entries.
scripts/polymarket_ws_probe.py (1)

294-312: Optional: Remove extraneous f-string prefixes.

Ruff flags several f-strings without placeholders (lines 294, 303, 306, 310). While functionally harmless, removing the f prefix improves clarity.

Example fix
-        print(f"  -> 1 Trade event")
+        print("  -> 1 Trade event")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/polymarket_ws_probe.py` around lines 294 - 312, Several print
statements in the event_type handling branches use f-strings but contain no
placeholders; remove the unnecessary f-prefix to satisfy linters. Locate the
prints inside the event_type branches (e.g., the "-> 1 Trade event" print, the
literal prints in the "best_bid_ask" branch that print labels like "  best_bid:"
and "  best_ask:", and the literal bracketed messages in the cold-path and
unknown-event branches) and change those print(...) calls from f-strings to
plain string literals (print("...")) while keeping any prints that do use
msg.get(...) as f-strings intact.
crates/transport/src/feed.rs (1)

58-72: Use an actual randomness source for reconnect jitter.

Instant::now().elapsed() on a freshly created Instant just measures call overhead, so this "jitter" is really scheduler noise rather than deliberate spread. If herd avoidance matters, feed this calculation from a real RNG or a per-thread PRNG.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/transport/src/feed.rs` around lines 58 - 72, The delay() method
currently derives jitter from Instant::now().elapsed() which is not true
randomness; replace that with a proper RNG (e.g., thread-local or SmallRng) to
compute jitter when jitter_factor > 0.0: use the feed struct fields (initial,
max, jitter_factor) and the delay(attempt: u32) function to compute the
base/capped duration as before, then draw a random value in [-jitter_range,
+jitter_range] (where jitter_range = capped.as_millis() as f64 * jitter_factor)
and add it to capped.as_millis(), finally returning
Duration::from_millis(ms.max(100.0) as u64); ensure you import and seed the RNG
appropriately (thread_rng() or per-thread PRNG) and avoid using Instant for
randomness.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.gitignore:
- Line 50: The .gitignore currently ends without a trailing newline (the last
entry ".claude/datapipeline"), which breaks POSIX text-file conventions; open
the .gitignore and add a single newline character after the final line so the
file ends with a trailing newline, then save and commit the change so tools and
diffs no longer warn about the missing newline.

In `@crates/transport/README.md`:
- Around line 114-117: Clarify the contradictory safety statement: change the
wording to say that the crate root includes #![deny(unsafe_code)] so the default
build contains no unsafe code, and explicitly note that the optional "tuning"
feature enables a single, audited unsafe block used only to call
setsockopt(SO_BUSY_POLL); i.e., state that the guarantee is "no unsafe in the
default build" and that the lone unsafe call is behind the tuning feature and
has been audited. Reference the crate root attribute (#![deny(unsafe_code)]),
the "tuning" feature, and the setsockopt(SO_BUSY_POLL) call in the updated
sentence.

In `@crates/transport/src/binance/reference.rs`:
- Around line 58-67: spawn_reference_feed currently allows an empty
config.symbols which makes book_ticker_url(&[]) produce an invalid "/ws/" URL
and spawns a worker that will reconnect forever; validate the input early in
spawn_reference_feed (taking BinanceReferenceConfig and before calling
book_ticker_url) and return an Err(std::io::Error) (or appropriate error) when
config.symbols.is_empty(), so callers fail fast instead of returning
Ok(FeedHandle) and starting the background thread.
- Around line 17-24: book_ticker_url currently concatenates multiple streams
into /ws/stream1/stream2 which is invalid for Binance combined streams; update
the book_ticker_url function to emit a single-symbol URL using
"{WS_BASE_URL}/ws/{stream}" when symbols.len()==1 and to use the combined-stream
syntax "{WS_BASE_URL}/stream?streams=stream1/stream2/..." (or
"{WS_BASE_URL}/public/stream?streams=..." if you prefer the newer endpoint) when
symbols.len()>1, joining the same "{s}@bookTicker" pieces with '/' to build the
streams parameter.

In `@crates/transport/src/feed.rs`:
- Around line 172-189: The code calls config.tuning.apply_affinity() but never
applies busy-poll; after a successful WsConnection::connect(&config.ws) return
(inside the match arm that yields conn) call the tuning method that enables busy
polling (e.g., config.tuning.apply_busy_poll(&conn.socket) or the appropriate
SocketTuning::apply_busy_poll method) before resetting attempt to 0 and using
conn; ensure you reference the same tuning struct used earlier (config.tuning /
SocketTuning) and pass the connection's underlying socket or file descriptor so
busy_poll_us is actually applied on each successful reconnection.
- Around line 190-203: The reconnect path uses thread::sleep(delay) (with delay
= config.backoff.delay(attempt)) which is uninterruptible and lets shutdown()
block; replace the single long sleep with an interruptible wait: either loop in
small chunks (e.g., compute remaining = delay and while remaining > 0 { if
is_shutdown() { break } sleep(min(chunk, remaining)); remaining -= chunk })
checking the shared shutdown flag, or use a wait primitive
(Condvar::wait_timeout or parking_lot::Condvar or an AtomicBool +
tokio::sync::Notify) to wake early; update the places using thread::sleep(delay)
(the shown Err(e) branch and the similar block around lines 237-246) to use this
interruptible-wait pattern and ensure attempt and backoff logic remain unchanged
when interrupted.
- Around line 112-130: The public FeedThread::spawn signature currently exposes
FnMut(&str) as the durable transport boundary (via FeedThread, spawn,
FeedConfig, FeedHandle), which we must not stabilize; change this by marking the
API as provisional and preventing downstream use: add a clear doc comment on
FeedThread::spawn and the FnMut(&str) parameter stating it is
provisional/internal-only, annotate the function with #[doc(hidden)] (or
#[deprecated(note = "internal/provisional API - do not depend on")] if hiding is
inappropriate), and consider restricting visibility to pub(crate) for spawn (or
introduce a sealed callback trait) so parse/normalize/HotEvent emission stays on
the IO-thread side until a stable API is designed. Ensure the doc comment
references FeedThread, spawn, FeedConfig, and FeedHandle so callers see the
warning.

In `@crates/transport/src/polymarket/market.rs`:
- Around line 29-39: The subscribe_msg function currently hand-rolls JSON by
quoting token_ids (using token_ids and format!("\"{id}\"")), which fails to
escape special characters; instead build a proper JSON value and serialize it
with serde_json (e.g., construct a struct or serde_json::json! with assets_ids =
self.token_ids, type = "market", custom_feature_enabled = true) and return
serde_json::to_string(...) so all token_ids are correctly escaped and you can
drop the manual quote mapping.

In `@crates/transport/src/tuning.rs`:
- Line 44: The call to us.cast_signed() (producing val) uses a method stabilized
in Rust 1.87, but our workspace MSRV is 1.85; replace that call with an
MSRV-safe cast (e.g., cast the unsigned value `us` to the desired signed type
with `as` — matching the intended target type used later — or use an explicit
conversion helper) or alternatively bump the workspace `rust-version` to 1.87.
Locate the occurrence `let val = us.cast_signed();` in tuning.rs and either
change it to an explicit `as <signed_type>` cast that matches subsequent uses of
`val`, or update the workspace MSRV if you intend to rely on `cast_signed()`.

In `@crates/transport/src/ws.rs`:
- Around line 36-45: WsConfig::binance() currently sets a 30s text-ping
heartbeat and send_ping() always sends a text "PING", which incorrectly applies
Polymarket's heartbeat to Binance and relies on read_timeout for scheduling;
change the design so heartbeat behavior is venue-configurable: introduce a
heartbeat mode/interval on WsConfig (e.g., enum HeartbeatMode { None,
Text(String), Frame } plus Duration ping_interval or Option<Duration>) and
update WsConfig::binance() to set HeartbeatMode::None (or no ping_interval) for
Binance, while Polymarket config sets HeartbeatMode::Text("PING") with the 10s
cadence; modify send_ping() to branch on the new HeartbeatMode to send text
messages or rely on protocol-level pongs (or do nothing for None/Frame), and
rewrite the read loop to use an independent timer (tokio::time::interval or a
separate timeout task) that enforces the ping_interval and triggers send_ping()
on schedule rather than waiting on read()/read_timeout so heartbeats occur on
their own cadence.

In `@crates/transport/tests/feed_thread.rs`:
- Around line 118-123: Stop the feed worker before comparing the two counters:
call handle.shutdown() (or the worker join method if available) before loading
received and handle.msg_count so both samples are taken after the thread is
stopped; alternatively relax the equality check to
assert!(handle.msg_count.load(Ordering::Relaxed) >=
received.load(Ordering::Relaxed)) referencing the existing symbols received,
handle.msg_count, and handle.shutdown() to locate and update the test.

In `@crates/transport/tests/live_feeds.rs`:
- Around line 49-53: The test currently only asserts handle.is_running() while
subscribing with token_ids: vec![], which can pass during a perpetual reconnect
loop; update the test in live_feeds.rs to wait for a positive success signal
instead of mere liveness: subscribe with a known-valid token id (replace
token_ids: vec![] with a concrete token id), and assert a successful
subscription acknowledgement or the arrival of the first message for that token
(e.g. wait for a subscription ack event or a first-message callback from
FeedThread), or assert an explicit successful-connect metric emitted by the
feed; replace the single handle.is_running() check with a timed wait for that
ack/message and fail the test if it does not arrive within a short timeout.

In `@scripts/polymarket_ws_probe.py`:
- Around line 354-359: The timeout passed into asyncio.wait_for can become
negative if time advances between the while check and the call; in the loop
surrounding ws.recv() compute the timeout value once (e.g., timeout = deadline -
time.time()), clamp it to a non-negative value or skip/wrap if timeout <= 0, and
then call asyncio.wait_for(ws.recv(), timeout=min(5.0, timeout)) to ensure you
never pass a negative timeout to asyncio.wait_for; update the block using
ws.recv, asyncio.wait_for, and deadline accordingly.

---

Nitpick comments:
In @.gitignore:
- Line 50: Add a short descriptive comment above the .claude/datapipeline ignore
entry in .gitignore explaining what that directory contains or why it's ignored
(e.g., "Claude local data pipeline cache" or "generated datapipeline artifacts
for Claude - do not commit"). Locate the line with the pattern
".claude/datapipeline" and insert a one-line comment immediately above it to
match the existing explanatory style used for other entries.

In `@crates/transport/src/feed.rs`:
- Around line 58-72: The delay() method currently derives jitter from
Instant::now().elapsed() which is not true randomness; replace that with a
proper RNG (e.g., thread-local or SmallRng) to compute jitter when jitter_factor
> 0.0: use the feed struct fields (initial, max, jitter_factor) and the
delay(attempt: u32) function to compute the base/capped duration as before, then
draw a random value in [-jitter_range, +jitter_range] (where jitter_range =
capped.as_millis() as f64 * jitter_factor) and add it to capped.as_millis(),
finally returning Duration::from_millis(ms.max(100.0) as u64); ensure you import
and seed the RNG appropriately (thread_rng() or per-thread PRNG) and avoid using
Instant for randomness.

In `@scripts/polymarket_ws_probe.py`:
- Around line 294-312: Several print statements in the event_type handling
branches use f-strings but contain no placeholders; remove the unnecessary
f-prefix to satisfy linters. Locate the prints inside the event_type branches
(e.g., the "-> 1 Trade event" print, the literal prints in the "best_bid_ask"
branch that print labels like "  best_bid:" and "  best_ask:", and the literal
bracketed messages in the cold-path and unknown-event branches) and change those
print(...) calls from f-strings to plain string literals (print("...")) while
keeping any prints that do use msg.get(...) as f-strings intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 30fbe313-7dce-4971-bdac-c273973ff57d

📥 Commits

Reviewing files that changed from the base of the PR and between 1c8d7e8 and 24ceed5.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • .gitignore
  • Cargo.toml
  • crates/transport/Cargo.toml
  • crates/transport/README.md
  • crates/transport/src/binance/mod.rs
  • crates/transport/src/binance/reference.rs
  • crates/transport/src/feed.rs
  • crates/transport/src/lib.rs
  • crates/transport/src/polymarket/market.rs
  • crates/transport/src/polymarket/mod.rs
  • crates/transport/src/tuning.rs
  • crates/transport/src/ws.rs
  • crates/transport/tests/feed_thread.rs
  • crates/transport/tests/live_feeds.rs
  • deny.toml
  • scripts/polymarket_ws_probe.py

Comment thread .gitignore
Comment thread crates/transport/README.md Outdated
Comment thread crates/transport/src/binance/reference.rs
Comment thread crates/transport/src/binance/reference.rs
Comment thread crates/transport/src/feed.rs
Comment thread crates/transport/src/tuning.rs
Comment thread crates/transport/src/ws.rs
Comment thread crates/transport/tests/feed_thread.rs
Comment thread crates/transport/tests/live_feeds.rs Outdated
Comment thread scripts/polymarket_ws_probe.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
crates/transport/tests/feed_thread.rs (1)

118-123: ⚠️ Potential issue | 🟡 Minor

Potential race: counter comparison while feed is still running.

The received and handle.msg_count are sampled while the feed thread is active. A message arriving between the two load() calls can cause assert_eq to fail intermittently. Either shut down before comparing, or relax the assertion.

🔧 Suggested fix: relax assertion or stop first
     tokio::time::sleep(Duration::from_millis(500)).await;
+    handle.shutdown();
     let count = received.load(Ordering::Relaxed);
     assert!(count >= 10, "expected >= 10 messages, got {count}");
-    assert_eq!(handle.msg_count.load(Ordering::Relaxed), u64::from(count));
-    handle.shutdown();
+    // After shutdown, both counters are stable
+    assert_eq!(handle.msg_count.load(Ordering::Relaxed), u64::from(count));

Alternatively, keep shutdown at the end but relax to >=:

-    assert_eq!(handle.msg_count.load(Ordering::Relaxed), u64::from(count));
+    assert!(handle.msg_count.load(Ordering::Relaxed) >= u64::from(count));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/transport/tests/feed_thread.rs` around lines 118 - 123, The test
samples two counters while the feed thread may still be running which can race:
move handle.shutdown() to before reading/comparing counters (call
handle.shutdown() then await any join if necessary), then load received and
handle.msg_count and assert equality; alternatively relax the strict equality to
assert!(handle.msg_count.load(Ordering::Relaxed) >=
u64::from(received.load(Ordering::Relaxed))) if you prefer not to stop the feed.
Ensure you reference the received atomic, handle.msg_count, and
handle.shutdown() when making the change.
🧹 Nitpick comments (8)
crates/registry/src/types.rs (2)

6-13: Minor: Extra spaces in doc comments.

Lines 6 and 10 have extra spaces before the opening parenthesis: "( BTC/USDT)" and "( Polymarket Up/Down)".

📝 Fix typos
-    /// Spot market ( BTC/USDT).
+    /// Spot market (BTC/USDT).
     Spot,
     /// Perpetual futures.
     Perp,
-    /// Binary prediction market ( Polymarket Up/Down).
+    /// Binary prediction market (Polymarket Up/Down).
     PredictionBinary,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/registry/src/types.rs` around lines 6 - 13, The doc comments for the
enum variants Spot and PredictionBinary contain stray spaces before the opening
parentheses; update the comments in types.rs for the Spot variant to "Spot
market (BTC/USDT)." and for PredictionBinary to "Binary prediction market
(Polymarket Up/Down)." by removing the extra space before "(" so the parentheses
are immediately adjacent to the preceding word.

81-91: Naming clarification: spot_reference creates OracleReference class.

The method is named spot_reference but sets class: InstrumentClass::OracleReference. This is semantically correct (reference price from a spot market acting as oracle), but the dual terminology could confuse readers. Consider either renaming to oracle_reference or adding a doc comment clarifying the relationship.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/registry/src/types.rs` around lines 81 - 91, The method spot_reference
currently constructs an Instrument with class InstrumentClass::OracleReference
which can be confusing; update the public API by adding a clarifying doc comment
to the spot_reference function explaining that it returns an OracleReference
derived from a spot market (or if you prefer a breaking change, rename the
function to oracle_reference and update all call sites). Specifically, edit the
spot_reference function's doc comment to state that spot_reference creates an
OracleReference (reference price sourced from a spot market) so readers
understand the dual terminology; include the function name spot_reference and
the enum InstrumentClass::OracleReference in the comment so maintainers can find
and review the change.
crates/registry/src/bindings.rs (1)

21-34: Consider validating window_start <= window_end invariant.

PolymarketWindowBinding accepts any Timestamp values without validation. Invalid windows where window_start > window_end could silently propagate. Consider adding a constructor that enforces this invariant, or document that validation occurs in the registry's bind methods.

🛠️ Example constructor with validation
impl PolymarketWindowBinding {
    /// Creates a new window binding, validating that start <= end.
    pub fn new(
        token_id: String,
        market_slug: String,
        window_start: Timestamp,
        window_end: Timestamp,
        condition_id: Option<String>,
    ) -> Result<Self, &'static str> {
        if window_start > window_end {
            return Err("window_start must not exceed window_end");
        }
        Ok(Self { token_id, market_slug, window_start, window_end, condition_id })
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/registry/src/bindings.rs` around lines 21 - 34,
PolymarketWindowBinding currently allows window_start > window_end, so add a
validated constructor (e.g., impl PolymarketWindowBinding::new) that checks if
window_start <= window_end and returns an Err on violation, or alternatively
update any factory functions that create PolymarketWindowBinding to perform this
check before constructing; reference the struct name PolymarketWindowBinding and
implement a new(...) method that validates the invariant and returns
Result<Self, &'static str> (or propagate a crate error type) to prevent invalid
windows from being created.
crates/transport/README.md (1)

9-16: Add language specifiers to ASCII diagram code blocks.

The architecture diagram (lines 9-16) and lifecycle diagram (lines 77-83) lack language specifiers. Adding text would satisfy markdown linters.

📝 Suggested fix
-```
+```text
 VENUE FEEDS                          MANTIS HOT PATH

Also applies to: 77-83

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/transport/README.md` around lines 9 - 16, Add a markdown language
specifier "text" to the ASCII diagram code fences so linters recognize them as
plain text; locate the fenced blocks containing the diagram lines like "VENUE
FEEDS                          MANTIS HOT PATH" and the lifecycle block around
lines showing "Polymarket Market WS", "Polymarket User WS", "Binance Reference
WS", "Timer" (and the second diagram at lines 77-83) and change the opening
triple-backtick from ``` to ```text for both diagrams.
crates/registry/README.md (1)

11-19: Add language specifiers to fenced code blocks for consistency.

The ASCII diagram and workflow code blocks lack language specifiers. While these aren't actual code, adding text or leaving empty with just triple backticks is cleaner for markdown linters.

📝 Suggested fix
-```
+```text
 token_id "72160714677..." (changes every 15 min)
     ↓ registry.by_polymarket_token_id()

Apply similar changes to the architecture diagram (line 23) and rotation workflow (line 75).

Also applies to: 23-31, 75-82

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/registry/README.md` around lines 11 - 19, Update the fenced code
blocks in the README diagrams to include a language specifier (use "text") so
markdown linters parse them consistently; specifically add ```text``` for the
ASCII workflow block containing token_id "72160714677...", the block showing
registry.by_polymarket_token_id(), registry.meta(), price_to_ticks(),
qty_to_lots() (InstrumentId/InstrumentMeta/Ticks/Lots), and likewise add
```text``` to the architecture diagram and the rotation workflow blocks
referenced around the other diagram sections.
crates/registry/src/lib.rs (1)

96-100: active_polymarket_token_ids allocates a new Vec on each call.

For hot-path subscription scenarios, this could be optimized by returning an iterator. However, for WS subscription (control path, infrequent), this is acceptable.

♻️ Optional: Return an iterator for zero-allocation access
     /// All currently active Polymarket token IDs (for WS subscription).
-    #[must_use]
-    pub fn active_polymarket_token_ids(&self) -> Vec<String> {
-        self.by_polymarket_token_id.keys().cloned().collect()
-    }
+    /// Returns an iterator over currently active Polymarket token IDs.
+    pub fn active_polymarket_token_ids(&self) -> impl Iterator<Item = &str> {
+        self.by_polymarket_token_id.keys().map(String::as_str)
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/registry/src/lib.rs` around lines 96 - 100, The current
active_polymarket_token_ids method allocates a Vec on each call; to avoid that,
add a zero-allocation iterator variant (e.g. active_polymarket_token_ids_iter)
that returns impl Iterator<Item=&String> by forwarding to
self.by_polymarket_token_id.keys() (or the concrete Keys type) so callers can
iterate without collecting; keep the existing Vec-returning
active_polymarket_token_ids for compatibility if needed.
crates/transport/src/feed.rs (1)

106-110: Drop sets shutdown flag but doesn't join the thread.

When a FeedHandle is dropped without calling shutdown(), the flag is set but the thread continues running until it checks the flag. This is intentional (non-blocking drop), but the thread may outlive the handle briefly. Consider documenting this behavior explicitly.

📝 Optional: Add doc comment clarifying drop behavior
 impl Drop for FeedHandle {
+    /// Sets the shutdown flag but does not block waiting for the thread to exit.
+    /// Call [`FeedHandle::shutdown()`] explicitly if you need to wait for termination.
     fn drop(&mut self) {
         self.shutdown.store(true, Ordering::Release);
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/transport/src/feed.rs` around lines 106 - 110, The Drop impl for
FeedHandle currently sets the shutdown flag via shutdown.store(true,
Ordering::Release) but does not join or block on the background thread, which
can let the thread briefly outlive the handle; add a clear doc comment on the
FeedHandle type (and optionally on the Drop impl) describing that dropping is
non-blocking: it flips the internal shutdown flag, does not join the worker
thread, and users should call shutdown() when they need deterministic thread
termination. Reference the FeedHandle struct, its shutdown field and method
shutdown(), and the Drop impl to ensure the documentation is placed where users
will see the intended lifetime semantics.
crates/transport/src/ws.rs (1)

79-90: Consider logging unhandled MaybeTlsStream variants instead of silently skipping TCP tuning.

The MaybeTlsStream enum is #[non_exhaustive], so while currently only Plain and Rustls variants are available with the enabled features, future versions of tungstenite or feature changes (e.g., adding native-tls) could introduce new variants. The wildcard pattern silently drops these cases, making TCP timeouts and nodelay configuration fail without visibility.

Add a debug log to track if variants are unexpectedly unhandled:

🔧 Suggested improvement
         let tcp_result = match ws.get_ref() {
             MaybeTlsStream::Plain(tcp) => Some(tcp),
             MaybeTlsStream::Rustls(tls) => Some(tls.get_ref()),
             _ => {
+                debug!("unhandled MaybeTlsStream variant, skipping TCP tuning");
                 None
             }
         };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/transport/src/ws.rs` around lines 79 - 90, The code silently ignores
unhandled MaybeTlsStream variants when matching ws.get_ref(), so add a debug log
when the match falls through to None to aid future debugging; specifically,
after computing tcp_result (from MaybeTlsStream::Plain and ::Rustls) log a debug
message (including the ws.get_ref() or a descriptive string) if tcp_result is
None before returning, so callers see that TCP tuning via set_read_timeout and
set_nodelay did not run; keep existing error handling for set_read_timeout and
set_nodelay and reference WsError::Connect and config.read_timeout in the
surrounding context to ensure logs tie to the same operation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/registry/src/error.rs`:
- Around line 39-45: The current byte-slice truncation in the Display arm for
DuplicatePolymarketTokenId (using &t[..t.len().min(20)]) can panic on multi-byte
UTF-8; replace it with a char-safe truncation: create a truncated string from t
via chars().take(20).collect::<String>() (optionally append "…" if
t.chars().count() > 20) and use that truncated value in the write! call inside
the DuplicatePolymarketTokenId match arm to avoid slicing by byte indices.

In `@crates/transport/src/ws.rs`:
- Around line 118-153: read_text can double-ping because maybe_send_ping() is
called at the top and the timeout branch unconditionally calls send_ping() when
ping_interval.is_some(); change the logic so a second ping is suppressed if a
ping was just sent — e.g. have maybe_send_ping() return a bool (or
update/send_ping to check a last_ping timestamp) and in read_text's timeout
branch only call send_ping() when maybe_send_ping() did not already send one (or
when last_ping is older than the configured interval), referencing read_text,
maybe_send_ping, send_ping and self.config.ping_interval.

---

Duplicate comments:
In `@crates/transport/tests/feed_thread.rs`:
- Around line 118-123: The test samples two counters while the feed thread may
still be running which can race: move handle.shutdown() to before
reading/comparing counters (call handle.shutdown() then await any join if
necessary), then load received and handle.msg_count and assert equality;
alternatively relax the strict equality to
assert!(handle.msg_count.load(Ordering::Relaxed) >=
u64::from(received.load(Ordering::Relaxed))) if you prefer not to stop the feed.
Ensure you reference the received atomic, handle.msg_count, and
handle.shutdown() when making the change.

---

Nitpick comments:
In `@crates/registry/README.md`:
- Around line 11-19: Update the fenced code blocks in the README diagrams to
include a language specifier (use "text") so markdown linters parse them
consistently; specifically add ```text``` for the ASCII workflow block
containing token_id "72160714677...", the block showing
registry.by_polymarket_token_id(), registry.meta(), price_to_ticks(),
qty_to_lots() (InstrumentId/InstrumentMeta/Ticks/Lots), and likewise add
```text``` to the architecture diagram and the rotation workflow blocks
referenced around the other diagram sections.

In `@crates/registry/src/bindings.rs`:
- Around line 21-34: PolymarketWindowBinding currently allows window_start >
window_end, so add a validated constructor (e.g., impl
PolymarketWindowBinding::new) that checks if window_start <= window_end and
returns an Err on violation, or alternatively update any factory functions that
create PolymarketWindowBinding to perform this check before constructing;
reference the struct name PolymarketWindowBinding and implement a new(...)
method that validates the invariant and returns Result<Self, &'static str> (or
propagate a crate error type) to prevent invalid windows from being created.

In `@crates/registry/src/lib.rs`:
- Around line 96-100: The current active_polymarket_token_ids method allocates a
Vec on each call; to avoid that, add a zero-allocation iterator variant (e.g.
active_polymarket_token_ids_iter) that returns impl Iterator<Item=&String> by
forwarding to self.by_polymarket_token_id.keys() (or the concrete Keys type) so
callers can iterate without collecting; keep the existing Vec-returning
active_polymarket_token_ids for compatibility if needed.

In `@crates/registry/src/types.rs`:
- Around line 6-13: The doc comments for the enum variants Spot and
PredictionBinary contain stray spaces before the opening parentheses; update the
comments in types.rs for the Spot variant to "Spot market (BTC/USDT)." and for
PredictionBinary to "Binary prediction market (Polymarket Up/Down)." by removing
the extra space before "(" so the parentheses are immediately adjacent to the
preceding word.
- Around line 81-91: The method spot_reference currently constructs an
Instrument with class InstrumentClass::OracleReference which can be confusing;
update the public API by adding a clarifying doc comment to the spot_reference
function explaining that it returns an OracleReference derived from a spot
market (or if you prefer a breaking change, rename the function to
oracle_reference and update all call sites). Specifically, edit the
spot_reference function's doc comment to state that spot_reference creates an
OracleReference (reference price sourced from a spot market) so readers
understand the dual terminology; include the function name spot_reference and
the enum InstrumentClass::OracleReference in the comment so maintainers can find
and review the change.

In `@crates/transport/README.md`:
- Around line 9-16: Add a markdown language specifier "text" to the ASCII
diagram code fences so linters recognize them as plain text; locate the fenced
blocks containing the diagram lines like "VENUE FEEDS                         
MANTIS HOT PATH" and the lifecycle block around lines showing "Polymarket Market
WS", "Polymarket User WS", "Binance Reference WS", "Timer" (and the second
diagram at lines 77-83) and change the opening triple-backtick from ``` to
```text for both diagrams.

In `@crates/transport/src/feed.rs`:
- Around line 106-110: The Drop impl for FeedHandle currently sets the shutdown
flag via shutdown.store(true, Ordering::Release) but does not join or block on
the background thread, which can let the thread briefly outlive the handle; add
a clear doc comment on the FeedHandle type (and optionally on the Drop impl)
describing that dropping is non-blocking: it flips the internal shutdown flag,
does not join the worker thread, and users should call shutdown() when they need
deterministic thread termination. Reference the FeedHandle struct, its shutdown
field and method shutdown(), and the Drop impl to ensure the documentation is
placed where users will see the intended lifetime semantics.

In `@crates/transport/src/ws.rs`:
- Around line 79-90: The code silently ignores unhandled MaybeTlsStream variants
when matching ws.get_ref(), so add a debug log when the match falls through to
None to aid future debugging; specifically, after computing tcp_result (from
MaybeTlsStream::Plain and ::Rustls) log a debug message (including the
ws.get_ref() or a descriptive string) if tcp_result is None before returning, so
callers see that TCP tuning via set_read_timeout and set_nodelay did not run;
keep existing error handling for set_read_timeout and set_nodelay and reference
WsError::Connect and config.read_timeout in the surrounding context to ensure
logs tie to the same operation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d99031cd-763b-44b8-9b90-679681e32afa

📥 Commits

Reviewing files that changed from the base of the PR and between 24ceed5 and 423acb1.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • Cargo.toml
  • crates/registry/Cargo.toml
  • crates/registry/README.md
  • crates/registry/src/bindings.rs
  • crates/registry/src/error.rs
  • crates/registry/src/lib.rs
  • crates/registry/src/record.rs
  • crates/registry/src/types.rs
  • crates/transport/README.md
  • crates/transport/src/binance/reference.rs
  • crates/transport/src/feed.rs
  • crates/transport/src/polymarket/market.rs
  • crates/transport/src/ws.rs
  • crates/transport/tests/feed_thread.rs
  • crates/transport/tests/live_feeds.rs
✅ Files skipped from review due to trivial changes (2)
  • crates/registry/src/record.rs
  • crates/registry/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (2)
  • Cargo.toml
  • crates/transport/src/polymarket/market.rs

Comment thread crates/registry/src/error.rs
Comment thread crates/transport/src/ws.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
crates/transport/README.md (2)

77-83: Add language specifier to the lifecycle diagram.

Same as above — use text for the ASCII lifecycle diagram.

📝 Suggested fix
-```
+```text
 spawn() → connect → subscribe → read loop ──> callback
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/transport/README.md` around lines 77 - 83, The ASCII lifecycle diagram
block lacks a language specifier; update the fenced code block that contains
"spawn() → connect → subscribe → read loop ──> callback" (and mentions on
error/backoff and shutdown()) to use the "text" language tag so it's treated as
plain text (i.e., change the opening ``` to ```text for the diagram that
includes spawn(), connect, subscribe, read loop, callback, and shutdown()).

9-16: Add language specifier to fenced code blocks.

Static analysis flagged missing language specifiers. For ASCII diagrams, use text or plaintext to satisfy linters while preserving readability.

📝 Suggested fix
-```
+```text
 VENUE FEEDS                          MANTIS HOT PATH
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/transport/README.md` around lines 9 - 16, The fenced code block
containing the ASCII diagram in the README (the triple-backtick block that
starts the VENUE FEEDS / MANTIS HOT PATH diagram) is missing a language
specifier; update the opening fence from ``` to ```text (or ```plaintext) so
linters recognize it as plain text while preserving the ASCII diagram
formatting.
crates/transport/src/ws.rs (1)

153-158: Consider documenting the bypass caveat.

Exposing inner_mut() allows callers to bypass the configured read_timeout and heartbeat scheduling if they call ws.read() directly. This is probably intentional for subscription updates, but a brief doc note could prevent misuse on the hot path.

📝 Suggested doc improvement
     /// Get a mutable reference to the underlying tungstenite socket.
     ///
     /// Useful for sending messages (e.g., dynamic subscription updates).
+    /// 
+    /// **Note:** Calling `read()` directly on the returned socket bypasses
+    /// heartbeat scheduling and timeout handling — use `read_text()` for the
+    /// normal read loop.
     pub fn inner_mut(&mut self) -> &mut WebSocket<MaybeTlsStream<TcpStream>> {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/transport/src/ws.rs` around lines 153 - 158, Add a brief doc note to
the inner_mut() method warning that returning a mutable reference to the
underlying tungstenite WebSocket (inner_mut) lets callers bypass the transport's
read_timeout and heartbeat scheduling (e.g., by calling ws.read() directly), and
recommend using provided higher-level helpers or ensuring the caller respects
timeouts/heartbeats when operating on the raw socket; reference the function
name inner_mut and the underlying field ws in the comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/transport/README.md`:
- Around line 77-83: The ASCII lifecycle diagram block lacks a language
specifier; update the fenced code block that contains "spawn() → connect →
subscribe → read loop ──> callback" (and mentions on error/backoff and
shutdown()) to use the "text" language tag so it's treated as plain text (i.e.,
change the opening ``` to ```text for the diagram that includes spawn(),
connect, subscribe, read loop, callback, and shutdown()).
- Around line 9-16: The fenced code block containing the ASCII diagram in the
README (the triple-backtick block that starts the VENUE FEEDS / MANTIS HOT PATH
diagram) is missing a language specifier; update the opening fence from ``` to
```text (or ```plaintext) so linters recognize it as plain text while preserving
the ASCII diagram formatting.

In `@crates/transport/src/ws.rs`:
- Around line 153-158: Add a brief doc note to the inner_mut() method warning
that returning a mutable reference to the underlying tungstenite WebSocket
(inner_mut) lets callers bypass the transport's read_timeout and heartbeat
scheduling (e.g., by calling ws.read() directly), and recommend using provided
higher-level helpers or ensuring the caller respects timeouts/heartbeats when
operating on the raw socket; reference the function name inner_mut and the
underlying field ws in the comment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f091b326-0630-4997-97b3-83ecc2a76cc3

📥 Commits

Reviewing files that changed from the base of the PR and between 423acb1 and 1bff8e0.

📒 Files selected for processing (3)
  • crates/transport/README.md
  • crates/transport/src/ws.rs
  • crates/transport/tests/feed_thread.rs
✅ Files skipped from review due to trivial changes (1)
  • crates/transport/tests/feed_thread.rs

@brohamgoham
Copy link
Copy Markdown
Collaborator Author

@Milerius please review and merge when ready

@Milerius Milerius merged commit ea32348 into main Apr 4, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants