Skip to content

feat: session 2 — spawn, timer, monitor, perf optimizations#29

Merged
Milerius merged 19 commits intomainfrom
feat/session-2-transport-phase-cd
Apr 11, 2026
Merged

feat: session 2 — spawn, timer, monitor, perf optimizations#29
Milerius merged 19 commits intomainfrom
feat/session-2-transport-phase-cd

Conversation

@Milerius
Copy link
Copy Markdown
Owner

@Milerius Milerius commented Apr 11, 2026

Summary

  • Spawn convenience for both venues: spawn_binance_feed / spawn_polymarket_market_feed with FnMut(HotEvent) -> bool backpressure and FeedSpawnResult (event_count + drop_count)
  • Binance multi-symbol decoder refactor: 1-8 symbols via flat lookup, combined stream detection via peek_stream
  • TimerThread: periodic Timer(Periodic) + Heartbeat events with config validation and Drop impl
  • FeedMonitor: stale feed detection via event_count comparison, first-check baseline, recovery
  • sonic-rs as default JSON parser (zero internal allocations, 1.5-2x faster than simd-json)
  • Optimized parse_decimal_bytes: single-pass, i64-only, POW10 lookup (eliminated i128 widening + triple-scan)
  • Timestamp::now(): std-gated wall-clock access
  • Book snapshot truncation warning at 64 levels

Benchmarks (Linux isolated core, criterion, target-cpu=native)

Decode path Before After Change
Binance bookTicker 307 ns 272 ns -11%
Polymarket price_change 306 ns 207 ns -32%
Polymarket trade 318 ns 216 ns -32%
Polymarket book 5 levels 627 ns 465 ns -26%

Perf profile (after optimization)

  • 0% malloc/free (sonic-rs zero-alloc)
  • parse_decimal_bytes reduced from 20% → 10.5% of total decode time
  • peek_stream eliminated for single-symbol feeds (was 16%)

Test plan

  • 187 mantis-fixed tests (optimized parser, MAX/MIN roundtrip)
  • 13 mantis-binance unit tests + 7 e2e/stress tests
  • 21 mantis-polymarket unit tests + 11 e2e/stress tests
  • 7 timer thread tests (periodic, heartbeat, seq, shutdown, validation)
  • 8 feed monitor tests (baseline, stale, recovery, multiple feeds)
  • 2 bolero property tests (arbitrary bytes → never panic)
  • 1M message stress tests on both decoders (release mode)
  • Backpressure accounting verified exact (50%/33% drop rates)
  • Full workspace cargo test --features alloc,std green
  • Perf profiled on Linux isolated core (AMD Ryzen 7 PRO 8700GE)

Review

Spec and plan reviewed by GPT-5.4 (gpt-5.4 via Codex CLI). All high/medium issues addressed.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Multi-symbol Binance decoder with combined-stream support
    • Feed spawn helpers for Binance and Polymarket exposing event/drop counters
    • Feed liveness monitor and configurable timer thread emitting periodic heartbeats
    • Timestamp::now() convenience API (std feature)
  • Improvements

    • Faster single-pass fixed-point decimal parser
    • Default JSON backend switched to sonic-rs with adjusted feature gating
    • Polymarket snapshot truncation now logs a warning
    • Expanded crate docs and README guidance
  • Tests & Quality

    • Extensive end-to-end, stress, unit, and property-based tests added

Milerius and others added 9 commits April 11, 2026 08:14
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add build_callback() and spawn_binance_feed() with Arc<AtomicU64>
counters for event_count and drop_count. Fix clippy lints in
Timestamp::now() (missing panics doc, cast truncation).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ssure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Passive utility struct that tracks event emission counters from feed
spawn wrappers and detects staleness. Designed for the engine to call
check_all() on each Timer(Periodic) event.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add property-based fuzz tests ensuring BinanceDecoder and
PolymarketMarketDecoder never panic on arbitrary byte input.
Critical for HFT resilience against malformed market data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nused fields

- Add sonic-rs as default JSON parser (1.5-2x faster than simd-json by
  skipping tape intermediate stage). Three-tier fallback: sonic-rs >
  simd-json > serde_json, controlled by feature flags.
- Replace peek_stream() byte scan with combined_stream bool field on
  BinanceDecoder, set at construction based on mapping count. Eliminates
  16% hotspot on single-symbol feeds.
- Remove unused T (trade_time) and E (event_time) fields from
  BinanceBookTicker schema — serde skips unknown fields, saving ~3.6%
  parse time per message.
- Update e2e tests to use combined-stream JSON format for multi-symbol
  decoders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Transport Phase C/D completion:
- Timestamp::now() (std-gated wall-clock access)
- spawn_binance_feed / spawn_polymarket_market_feed with backpressure
  (FnMut(HotEvent) -> bool, drop_count tracking, FeedSpawnResult)
- TimerThread — periodic Timer(Periodic) + Heartbeat events
  (config validation, Drop impl, interruptible shutdown)
- FeedMonitor — stale feed detection via event_count comparison
  (first-check baseline, recovery, MAX_FEEDS=8)
- Book snapshot truncation warning (tracing::warn at 64 levels)

Performance optimizations:
- sonic-rs as default JSON parser (1.5-2x faster than simd-json,
  zero internal allocations)
- Optimized parse_decimal_bytes: single-pass, i64-only, POW10 lookup
  (eliminated i128 widening + triple-scan + multiply loop)
- Eliminated peek_stream overhead for single-symbol feeds
- Removed unused T/E fields from Binance schema

Benchmarks (Linux isolated core, criterion):
- Binance bookTicker: 272ns/msg (was 307ns, -11%)
- Polymarket price_change: 207ns/msg (was 306ns, -32%)

Testing:
- 18 e2e + stress tests (1M messages, backpressure, multi-symbol)
- 7 timer tests, 8 monitor tests
- Bolero property tests (arbitrary bytes → never panic)
- READMEs for mantis-binance, mantis-polymarket
- PROGRESS.md updated with section 1.13

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 11, 2026

Warning

Rate limit exceeded

@Milerius has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 minutes and 15 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 1 minutes and 15 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 926b4e50-eccf-4edc-bda3-3bb9510cb080

📥 Commits

Reviewing files that changed from the base of the PR and between 623cf93 and 60ac37f.

📒 Files selected for processing (5)
  • crates/binance/src/spawn.rs
  • crates/polymarket/src/market/decoder.rs
  • crates/polymarket/src/market/spawn.rs
  • crates/transport/src/monitor.rs
  • crates/transport/src/timer.rs
📝 Walkthrough

Walkthrough

Adds workspace sonic-rs and integrates multi-symbol, zero-allocation Binance and Polymarket decoders with spawn helpers exposing event/drop counters; introduces transport timer and feed monitor, single-pass fixed-point parser, expanded tests/benches/READMEs, and related Cargo feature changes across several crates.

Changes

Cohort / File(s) Summary
Workspace & Manifests
Cargo.toml, crates/bench/Cargo.toml, crates/binance/Cargo.toml, crates/polymarket/Cargo.toml, crates/verify/Cargo.toml
Add workspace sonic-rs = "0.5"; propagate optional sonic-rs deps and features to crates; update mantis-types std feature and add mantis-* deps to verify.
Binance Decoder & API
crates/binance/src/decoder.rs, crates/binance/src/schema.rs, crates/binance/src/lib.rs, crates/binance/README.md
Convert decoder to fixed-capacity multi-symbol mapping, make constructor fallible (DecoderError), add combined-stream handling and symbol lookup routing, add sonic-rs parse backend, update exports and README.
Binance Spawn, Bench & Tests
crates/binance/src/spawn.rs, crates/bench/benches/decode.rs, crates/binance/tests/e2e_decode.rs
Implement build_callback and spawn_binance_feed returning FeedSpawnResult with event_count/drop_count; update bench to multi-symbol mapping; add extensive e2e and stress tests.
Polymarket Decoder & API
crates/polymarket/src/market/decoder.rs, crates/polymarket/src/market/spawn.rs, crates/polymarket/src/market/mod.rs, crates/polymarket/Cargo.toml, crates/polymarket/README.md
Add sonic-rs backend/feature, adjust cfg gating, implement spawn utilities with counters and re-exports, add truncation warning for >64 book levels, add README and e2e tests.
Transport: timer & monitor
crates/transport/src/timer.rs, crates/transport/src/monitor.rs, crates/transport/src/lib.rs, crates/transport/src/feed.rs, crates/transport/README.md
Add TimerThread (tick/heartbeat emission, config validation, spawn/shutdown), add FeedMonitor (register/check_all/stale tracking, MAX_FEEDS), make interruptible_sleep pub(crate), re-export monitor/timer API, and update docs.
Fixed-point parsing & Types
crates/fixed/src/parse.rs, crates/types/src/timestamp.rs
Rewrite decimal parser to a single-pass accumulator with overflow checks and add Timestamp::now() (std feature) plus tests.
Verification & Property Tests
crates/verify/src/decoder_props.rs, crates/verify/src/lib.rs, docs/PROGRESS.md
Add bolero property tests to ensure decoders never panic on arbitrary bytes, register new test module, and update project progress documentation.

Sequence Diagram(s)

sequenceDiagram
    participant WS as WebSocket Thread
    participant CB as Transport Callback
    participant Dec as Decoder
    participant Q as SPSC Queue
    participant Monitor as FeedMonitor

    WS->>CB: read_bytes(&mut buf)
    CB->>Dec: decode(&mut buf, timestamp, &mut out)
    Dec->>Dec: parse JSON (sonic-rs/simd-json/serde) 
    Dec->>Dec: symbol/token lookup & convert ticks/lots
    Dec-->>CB: return n HotEvent(s)
    CB->>CB: event_count += n
    loop for each HotEvent
        CB->>Q: push(event)
        alt push succeeds
            Q->>Engine: event available
        else push fails
            CB->>CB: drop_count += 1
        end
    end
    CB->>Monitor: Monitor observes event_count (periodic)
    CB-->>WS: return true
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • brohamgoham

Poem

🐰 Hopping through bytes with whiskers bright,

Multi-symbol maps guide every flight.
Timers tick, monitors peep, counters keep score,
Sonic parses fast, events leap from the core.
A tiny rabbit cheers — decode, decode, encore!

🚥 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 title 'feat: session 2 — spawn, timer, monitor, perf optimizations' directly summarizes the main changes: adding spawn helpers, timer thread, feed monitor, and performance optimizations across the codebase.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/session-2-transport-phase-cd

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 11, 2026

Benchmark Report

Commit: b3caf534afd5451292173a2ed1bd2380068eb7bc

Sequence Lock (mantis-seqlock)

Linux

CPU: AMD EPYC 7763 64-Core Processor | Arch: x86_64 | Compiler: rustc 1.96.0-nightly (02c7f9bec 2026-04-10)

seqlock_read_contended

Variant ns/op
u64 25.16
msg64 327.19

seqlock_read_uncontended

Variant ns/op
u64 0.63
msg64 1.43
msg128 2.51

seqlock_write

Variant ns/op
u64 1.56
msg64 1.87
msg128 3.12
macOS

CPU: Apple M1 (Virtual) | Arch: arm64 | Compiler: rustc 1.96.0-nightly (02c7f9bec 2026-04-10)

seqlock_read_contended

Variant ns/op
msg64 99.23
u64 145.14

seqlock_read_uncontended

Variant ns/op
u64 1.37
msg64 2.09
msg128 4.3

seqlock_write

Variant ns/op
msg64 7.04
u64 7.23
msg128 7.81

Fixed-Point Arithmetic (mantis-fixed)

Linux

CPU: AMD EPYC 7763 64-Core Processor | Arch: x86_64 | Compiler: rustc 1.96.0-nightly (02c7f9bec 2026-04-10)

fixed_checked_add

Variant ns/op
FixedI64_6_ 1.25
raw_i64 1.25

fixed_checked_div

Variant ns/op
trunc 4.99
round 5.61

fixed_checked_mul_trunc

Variant ns/op
D=6 4.14
D=2 4.2
D=4 4.28
D=8 4.35

fixed_decimal_parse

Variant ns/op
f64_roundtrip_short_0.53 12.23
mantis_str_short_0.53 12.49
mantis_bytes_short_0.53 12.51
mantis_bytes_integer_67396 14.18
f64_roundtrip_medium_67396.70 15.62
mantis_bytes_long_0.00012345 16.76
mantis_str_medium_67396.70 17.49
mantis_bytes_medium_67396.70 17.51

fixed_display

Variant ns/op
FixedI64_6_ 49.3

fixed_mul_round_vs_trunc

Variant ns/op
trunc 4.21
round 4.84

fixed_parse

Variant ns/op
short 11.78
integer_only 15.12
full_precision 19.4

fixed_rescale

Variant ns/op
D2_to_D8_widen 0.63
D6_to_D2_trunc 0.91
macOS

CPU: Apple M1 (Virtual) | Arch: arm64 | Compiler: rustc 1.96.0-nightly (02c7f9bec 2026-04-10)

fixed_checked_add

Variant ns/op
raw_i64 1.3
FixedI64_6_ 1.39

fixed_checked_div

Variant ns/op
trunc 3.39
round 4.57

fixed_checked_mul_trunc

Variant ns/op
D=8 2
D=6 2.03
D=4 2.06
D=2 2.13

fixed_decimal_parse

Variant ns/op
f64_roundtrip_short_0.53 7.78
mantis_bytes_short_0.53 8.33
mantis_str_short_0.53 8.83
mantis_bytes_integer_67396 9.83
f64_roundtrip_medium_67396.70 10.52
mantis_bytes_medium_67396.70 15.92
mantis_str_medium_67396.70 16.22
mantis_bytes_long_0.00012345 16.52

fixed_display

Variant ns/op
FixedI64_6_ 38.41

fixed_mul_round_vs_trunc

Variant ns/op
trunc 2.05
round 2.36

fixed_parse

Variant ns/op
short 8.52
integer_only 10.83
full_precision 19.89

fixed_rescale

Variant ns/op
D6_to_D2_trunc 0.52
D2_to_D8_widen 0.53

Market-State Engine (mantis-market-state)

Linux

CPU: AMD EPYC 7763 64-Core Processor | Arch: x86_64 | Compiler: rustc 1.96.0-nightly (02c7f9bec 2026-04-10)

market_state_array_book

Variant ns/op
best_bid 0.94
apply_delta 1.87

market_state_engine

Variant ns/op
micro_price 3.12
process_delta_mid_batch 4.08
process_delta_batch_end 19.63
book_imbalance_5 163.05
macOS

CPU: Apple M1 (Virtual) | Arch: arm64 | Compiler: rustc 1.96.0-nightly (02c7f9bec 2026-04-10)

market_state_array_book

Variant ns/op
best_bid 0.73
apply_delta 1.22

market_state_engine

Variant ns/op
micro_price 2.22
process_delta_mid_batch 2.93
process_delta_batch_end 26.9
book_imbalance_5 141.46

- Fixed parse.rs: backtick doc_markdown, let..else, merged match arms,
  missing #Errors section on from_str_decimal
- Fixed e2e tests: expect_used annotations, unused imports, doc backticks,
  print_stderr/cast_precision_loss file-level expects

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/transport/README.md (1)

135-140: ⚠️ Potential issue | 🟡 Minor

Remove the stale transport-level JSON feature entry.

Earlier sections say transport only forwards raw bytes and venue crates own JSON parsing, so the simd-json row is misleading here. If parsing has moved out of this crate, the feature table should only document transport features.

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

In `@crates/transport/README.md` around lines 135 - 140, Remove the stale
transport-level JSON feature entry by deleting the `simd-json` row from the
Features table in the README and leaving only transport-relevant features (e.g.,
the `tuning` row); search for and remove any other references to the `simd-json`
feature in this crate's README or feature documentation so the docs reflect that
JSON parsing is handled by venue crates, keeping references to `tuning` and any
real transport features intact.
🧹 Nitpick comments (2)
crates/types/src/timestamp.rs (1)

103-109: Consider renaming test to now_is_non_decreasing.

SystemTime is wall-clock time and not guaranteed to be monotonic (NTP adjustments can cause backward jumps). The test assertion b >= a is correct for a quick sanity check, but the name now_is_monotonic implies a stronger guarantee than SystemTime provides. Consider renaming to now_is_non_decreasing_in_quick_succession or similar to avoid confusion.

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

In `@crates/types/src/timestamp.rs` around lines 103 - 109, Rename the test
function now_is_monotonic to a name that does not imply a monotonic clock (e.g.,
now_is_non_decreasing or now_is_non_decreasing_in_quick_succession) to reflect
that Timestamp::now() uses wall-clock SystemTime; update the test declaration fn
now_is_monotonic() -> fn now_is_non_decreasing() (or chosen name) and keep the
body using Timestamp::now() and the same assert to preserve behavior.
crates/polymarket/src/market/decoder.rs (1)

175-185: Avoid double truncation warnings for a single snapshot.

If bids already hit the 64 cap, the asks loop currently emits the same warning again. Consider logging once per message to reduce noise.

Diff suggestion
         let mut count: usize = 0;
         let total_levels = msg.bids.len() + msg.asks.len();
+        let mut truncation_warned = false;

         for (depth_idx, level) in msg.bids.iter().enumerate() {
             if count >= 64 {
-                tracing::warn!(
-                    asset_id = msg.asset_id,
-                    total_levels,
-                    emitted = 64,
-                    "book snapshot truncated at 64 events"
-                );
+                if !truncation_warned {
+                    tracing::warn!(
+                        asset_id = msg.asset_id,
+                        total_levels,
+                        emitted = 64,
+                        "book snapshot truncated at 64 events"
+                    );
+                    truncation_warned = true;
+                }
                 break;
             }
@@
         for (depth_idx, level) in msg.asks.iter().enumerate() {
             if count >= 64 {
-                tracing::warn!(
-                    asset_id = msg.asset_id,
-                    total_levels,
-                    emitted = 64,
-                    "book snapshot truncated at 64 events"
-                );
+                if !truncation_warned {
+                    tracing::warn!(
+                        asset_id = msg.asset_id,
+                        total_levels,
+                        emitted = 64,
+                        "book snapshot truncated at 64 events"
+                    );
+                    truncation_warned = true;
+                }
                 break;
             }

Also applies to: 210-218

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

In `@crates/polymarket/src/market/decoder.rs` around lines 175 - 185, The snapshot
truncation warning is emitted twice when both bids and asks iterate after count
hits 64; modify the logic in the loops that iterate msg.bids and msg.asks inside
decoder.rs (the code blocks around the bids loop starting with "for (depth_idx,
level) in msg.bids.iter().enumerate()" and the corresponding asks loop) to emit
the warning only once per message by introducing a per-message boolean flag
(e.g., warned_truncated or emitted_truncation_warn) initialized before the
loops, check it before calling tracing::warn!, set it to true when you log, and
reuse that flag in both loops so the warning is suppressed on the second
truncation attempt.
🤖 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/binance/README.md`:
- Around line 9-19: The markdown fence that begins with the ASCII diagram
starting "COLD SIDE (transport thread)              HOT SIDE (SPSC consumer)" is
missing a language tag; update the opening ``` to ```text so the block is
labeled (e.g., change the fence before that diagram to ```text) to satisfy
markdownlint MD040.
- Around line 54-90: Update the README wording to reflect the current
implementation: remove/replace claims that the decoder calls peek_stream() for
combined-stream detection and that simd-json is the default; instead state that
combined-stream detection is performed outside the decoder (so decoders consume
already-unwrapped events) and that the crate now defaults to the non-simd parser
(list the actual default parser), and update the Features and Combined Stream
Detection sections to reference BinanceDecoder<D>, BinanceSymbolMapping<'a, D>,
and DecoderError only for what they currently do (stateful decoding and error
types) and to note that peek_stream() is no longer part of the decoder path and
the simd-json feature is opt-in rather than the default.

In `@crates/binance/src/decoder.rs`:
- Around line 94-105: The code currently silently truncates m.symbol when its
byte length exceeds MAX_SYMBOL_LEN (16), which can cause lookup failures; update
the decoding logic that handles symbol_names/symbol_lens to first check if
m.symbol.as_bytes().len() > MAX_SYMBOL_LEN and return an
Err(DecoderError::SymbolTooLong) instead of truncating, and add a new
DecoderError::SymbolTooLong variant (with the provided doc comment) to the
DecoderError enum so callers can handle oversized symbols explicitly.

In `@crates/binance/tests/e2e_decode.rs`:
- Around line 168-204: The stress tests (e.g., the test function
stress_1m_messages) should not run as part of regular cargo test; either mark
them to be skipped by default or move them to a proper benchmark harness: add a
skip attribute like #[ignore] or gate them behind a feature (e.g.,
#[cfg_attr(not(feature = "perf"), ignore)] or #[cfg(feature = "perf")]) so
normal tests run fast, or relocate the large-loop/perf logic into a
Criterion/benches benchmark crate and keep only small functional assertions in
the unit tests (apply the same change to the other large/stress tests referenced
in the diff).

In `@crates/transport/src/timer.rs`:
- Around line 118-129: Add a new TimerConfigError::HeartbeatTooLarge variant
and, after computing tick_ns and hb_ns and after the divisibility check (where
TimerConfigError::HeartbeatNotDivisible is returned), check that (hb_ns /
tick_ns) <= u64::MAX; if it exceeds u64::MAX return
TimerConfigError::HeartbeatTooLarge instead of casting; only then perform the
safe cast into heartbeat_every (the variable currently set via (hb_ns / tick_ns)
as u64) so you avoid silent truncation and the potential zero divisor panic when
calling methods like tick_counter.is_multiple_of(heartbeat_every).

---

Outside diff comments:
In `@crates/transport/README.md`:
- Around line 135-140: Remove the stale transport-level JSON feature entry by
deleting the `simd-json` row from the Features table in the README and leaving
only transport-relevant features (e.g., the `tuning` row); search for and remove
any other references to the `simd-json` feature in this crate's README or
feature documentation so the docs reflect that JSON parsing is handled by venue
crates, keeping references to `tuning` and any real transport features intact.

---

Nitpick comments:
In `@crates/polymarket/src/market/decoder.rs`:
- Around line 175-185: The snapshot truncation warning is emitted twice when
both bids and asks iterate after count hits 64; modify the logic in the loops
that iterate msg.bids and msg.asks inside decoder.rs (the code blocks around the
bids loop starting with "for (depth_idx, level) in msg.bids.iter().enumerate()"
and the corresponding asks loop) to emit the warning only once per message by
introducing a per-message boolean flag (e.g., warned_truncated or
emitted_truncation_warn) initialized before the loops, check it before calling
tracing::warn!, set it to true when you log, and reuse that flag in both loops
so the warning is suppressed on the second truncation attempt.

In `@crates/types/src/timestamp.rs`:
- Around line 103-109: Rename the test function now_is_monotonic to a name that
does not imply a monotonic clock (e.g., now_is_non_decreasing or
now_is_non_decreasing_in_quick_succession) to reflect that Timestamp::now() uses
wall-clock SystemTime; update the test declaration fn now_is_monotonic() -> fn
now_is_non_decreasing() (or chosen name) and keep the body using
Timestamp::now() and the same assert to preserve behavior.
🪄 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: 36454b01-efd3-45b3-8424-e2b945b7f603

📥 Commits

Reviewing files that changed from the base of the PR and between 04d93a4 and 741595e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • Cargo.toml
  • crates/bench/Cargo.toml
  • crates/bench/benches/decode.rs
  • crates/binance/Cargo.toml
  • crates/binance/README.md
  • crates/binance/src/decoder.rs
  • crates/binance/src/lib.rs
  • crates/binance/src/schema.rs
  • crates/binance/src/spawn.rs
  • crates/binance/tests/e2e_decode.rs
  • crates/fixed/src/parse.rs
  • crates/polymarket/Cargo.toml
  • crates/polymarket/README.md
  • crates/polymarket/src/market/decoder.rs
  • crates/polymarket/src/market/mod.rs
  • crates/polymarket/src/market/spawn.rs
  • crates/polymarket/tests/e2e_decode.rs
  • crates/transport/README.md
  • crates/transport/src/feed.rs
  • crates/transport/src/lib.rs
  • crates/transport/src/monitor.rs
  • crates/transport/src/timer.rs
  • crates/types/src/timestamp.rs
  • crates/verify/Cargo.toml
  • crates/verify/src/decoder_props.rs
  • crates/verify/src/lib.rs
  • docs/PROGRESS.md

Comment thread crates/binance/README.md Outdated
Comment thread crates/binance/README.md Outdated
Comment thread crates/binance/src/decoder.rs
Comment thread crates/binance/tests/e2e_decode.rs
Comment thread crates/transport/src/timer.rs
- Add SymbolTooLong error to BinanceDecoder, reject oversized symbols
- Add HeartbeatTooLarge bounds check to TimerConfig validation
- Mark all stress tests #[ignore = "stress test"] in both venue crates
- Deduplicate truncation warning in Polymarket book decoder
- Rename now_is_monotonic test to now_is_non_decreasing
- Fix binance README: text fence, sonic-rs default, combined_stream docs
- Remove stale simd-json entry from transport README features table

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Milerius
Copy link
Copy Markdown
Owner Author

@coderabbitai All 5 actionable issues + 2 nitpicks addressed in commit 03e5175:

  1. SymbolTooLongDecoderError::SymbolTooLong added, constructor rejects symbols > 16 bytes
  2. Stress tests #[ignore] — All 7 stress tests gated with #[ignore = "stress test"]
  3. HeartbeatTooLargeTimerConfigError::HeartbeatTooLarge added with u64 bounds check before cast
  4. Double truncation warningtruncation_warned flag ensures single warn per snapshot
  5. Test renamenow_is_monotonicnow_is_non_decreasing
  6. README fixestext fence tag, sonic-rs as default, combined_stream bool field, removed stale simd-json from transport features table

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 11, 2026

Tip

For best results, initiate chat on the files or code changes.

@Milerius: All six items confirmed — thank you for the thorough follow-through! Here's a quick summary of what landed:

  1. DecoderError::SymbolTooLong — constructor now enforces the 16-byte symbol length limit at the API boundary.
  2. #[ignore = "stress test"] — all 7 stress tests are gated, keeping cargo test fast by default.
  3. TimerConfigError::HeartbeatTooLarge — u64 overflow is caught before the cast, closing the potential panic path.
  4. truncation_warned flag — single-warn-per-snapshot semantics are now guaranteed.
  5. now_is_non_decreasing — test name accurately reflects the weaker (and correct) monotonicity contract.
  6. README fixestext fence, sonic-rs as default, combined_stream bool field, and the stale simd-json transport row are all corrected.

Everything looks solid. I'll go ahead and resolve the open review comments.

[resolve]

(∩`-´)⊃━☆゚.*・。゚

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 11, 2026

Codecov Report

❌ Patch coverage is 93.69787% with 68 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.35%. Comparing base (1c472ad) to head (60ac37f).
⚠️ Report is 21 commits behind head on main.

Files with missing lines Patch % Lines
crates/polymarket/src/market/spawn.rs 84.40% 17 Missing ⚠️
crates/binance/src/spawn.rs 84.76% 16 Missing ⚠️
crates/transport/src/timer.rs 94.98% 8 Missing and 7 partials ⚠️
crates/binance/src/decoder.rs 96.19% 7 Missing ⚠️
crates/transport/src/monitor.rs 96.27% 5 Missing and 1 partial ⚠️
crates/polymarket/src/market/decoder.rs 87.50% 5 Missing ⚠️
crates/fixed/src/parse.rs 98.18% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #29      +/-   ##
==========================================
- Coverage   95.57%   95.35%   -0.22%     
==========================================
  Files          77       82       +5     
  Lines       10009    10893     +884     
  Branches    10009    10893     +884     
==========================================
+ Hits         9566    10387     +821     
- Misses        393      447      +54     
- Partials       50       59       +9     
Files with missing lines Coverage Δ
crates/transport/src/feed.rs 89.14% <100.00%> (ø)
crates/types/src/timestamp.rs 100.00% <100.00%> (ø)
crates/verify/src/decoder_props.rs 100.00% <100.00%> (ø)
crates/fixed/src/parse.rs 96.89% <98.18%> (+2.67%) ⬆️
crates/polymarket/src/market/decoder.rs 94.74% <87.50%> (-0.24%) ⬇️
crates/transport/src/monitor.rs 96.27% <96.27%> (ø)
crates/binance/src/decoder.rs 94.20% <96.19%> (-3.11%) ⬇️
crates/transport/src/timer.rs 94.98% <94.98%> (ø)
crates/binance/src/spawn.rs 84.76% <84.76%> (ø)
crates/polymarket/src/market/spawn.rs 84.40% <84.40%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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: 4

🧹 Nitpick comments (4)
crates/transport/src/timer.rs (1)

299-301: Replace wildcard matches! patterns with explicit destructuring.

At Line 300 and Line 334, EventBody::Timer(_) / EventBody::Heartbeat(_) violates the project’s Rust pattern-matching rule.

Suggested refactor
-        let timer_count = collected
-            .iter()
-            .filter(|e| matches!(e.body, EventBody::Timer(_)))
-            .count();
+        let timer_count = collected
+            .iter()
+            .filter(|e| {
+                if let EventBody::Timer(timer_payload) = &e.body {
+                    let _explicit_binding = timer_payload;
+                    true
+                } else {
+                    false
+                }
+            })
+            .count();

-        let hb_count = collected
-            .iter()
-            .filter(|e| matches!(e.body, EventBody::Heartbeat(_)))
-            .count();
+        let hb_count = collected
+            .iter()
+            .filter(|e| {
+                if let EventBody::Heartbeat(heartbeat_payload) = &e.body {
+                    let _explicit_binding = heartbeat_payload;
+                    true
+                } else {
+                    false
+                }
+            })
+            .count();

As per coding guidelines: "No wildcard matches — use explicit destructuring in pattern matching".

Also applies to: 333-335

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

In `@crates/transport/src/timer.rs` around lines 299 - 301, The matches! wildcards
should be replaced with explicit destructuring: change uses of matches!(e.body,
EventBody::Timer(_)) and matches!(e.body, EventBody::Heartbeat(_)) to
pattern-match the inner binding (e.g., match e.body { EventBody::Timer(timer) =>
true, _ => false } or use if let EventBody::Timer(timer) = &e.body { … }) so the
inner value is explicitly named; update both occurrences referencing
EventBody::Timer and EventBody::Heartbeat and replace the anonymous `_` with a
concrete binding name (e.g., timer, hb) wherever e.body is tested in
iterator/filter chains.
crates/fixed/src/parse.rs (1)

158-165: Use let...else for the feed fast path.

This match is only handling the early-return case, so let Ok(next) = acc.feed(bytes[i]) else { return Err(e); }; keeps the happy path flatter and matches the repo style.

As per coding guidelines "Use let...else for early returns, keep happy path unindented".

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

In `@crates/fixed/src/parse.rs` around lines 158 - 165, The loop uses a match to
handle the early-return case when calling acc.feed(bytes[i]); replace it with a
let...else to keep the happy path flat: call acc.feed(bytes[i]) with let
Ok(next) = acc.feed(bytes[i]) else { return Err(e); }; then set acc = next.
Update the block surrounding Accumulator::new, acc.feed, bytes, i, and start
accordingly so the early error returns via else and the successful path is
unindented.
crates/polymarket/tests/e2e_decode.rs (2)

35-78: Return id_up and id_down from test_registry().

The later assertions are coupled to InstrumentId::from_raw(1/2) even though this helper already knows the actual inserted IDs. Returning them would make the tests resilient to registry ID allocation changes, and it would also prevent stress_500k_alternating_tokens from treating any non-up ID as “down”.

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

In `@crates/polymarket/tests/e2e_decode.rs` around lines 35 - 78, The helper
test_registry currently returns only a leaked InstrumentRegistry but knows the
inserted IDs (id_up, id_down); change test_registry to return the two inserted
InstrumentId values (e.g., (id_up, id_down) or a small struct) along with or
instead of the leaked registry so callers can use the actual ids; update all
call sites (tests like stress_500k_alternating_tokens and any assertions that
assume InstrumentId::from_raw(1/2)) to consume the returned ids rather than
hard-coding raw ids; keep identifiers id_up, id_down, test_registry, and any
bindings (bind_polymarket_current / insert) as reference points when making the
change.

171-180: Avoid wildcard fallback arms in these assertions.

Bind the unexpected variant instead of using _ so failures report what came back and the match stays aligned with the repo rule.

As per coding guidelines "No wildcard matches — use explicit destructuring in pattern matching".

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

In `@crates/polymarket/tests/e2e_decode.rs` around lines 171 - 180, The assertions
use a wildcard fallback arm which hides the actual unexpected variant; change
the match arms on out[0].body and out[2].body to bind the unexpected variant
(e.g., name it `other`) instead of `_` and include that bound value in the panic
message so failures display what was returned; keep the successful arm as
`EventBody::BookDelta(bd)` and continue to assert `bd.side == Side::Bid`.
🤖 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/binance/README.md`:
- Around line 83-84: Update the DecoderError documentation to include the
missing enum variant SymbolTooLong: find the table row that lists DecoderError
(currently showing TooManySymbols and EmptyMappings) and add SymbolTooLong to
the list of possible errors so the README matches the variants exposed by the
DecoderError enum in decoder.rs; ensure the entry format matches the existing
style and, if there is an explanatory paragraph elsewhere for DecoderError, add
a short sentence describing when SymbolTooLong is returned.

In `@crates/fixed/src/parse.rs`:
- Around line 76-92: The code currently increments self.total_digits for every
byte which rejects strings of many leading zeros; change the logic to count
significant magnitude instead of raw digits by only incrementing the digit count
once the mantissa has become non-zero or the current digit is non-zero (i.e.
start counting after the first non-zero digit or once you've already started
counting significant digits). Update the overflow guard to use that significant
count (or a new significant_digits field) when deciding to return
ParseFixedError::Overflow, and keep the existing checked_mul/checked_sub updates
to self.mantissa (in the block around self.total_digits, self.mantissa, and
ParseFixedError::Overflow) so numeric overflow is still detected from arithmetic
growth rather than raw byte length.

In `@crates/transport/README.md`:
- Around line 116-122: The fenced diagram block currently unlabeled (the block
showing "spawn() -> connect -> subscribe -> read loop --> callback" and related
lines) should have a language tag to satisfy MD040; change the opening fence
from ``` to ```text so the block is labeled (e.g., use ```text before the lines
describing spawn(), reconnect/backoff, and shutdown()) to improve markdown
tooling and linting.
- Around line 86-95: The example passed a closure that returns a bool (|event|
ring.try_push(event).is_ok()) to TimerThread::spawn, but spawn expects an
FnMut(HotEvent) -> () unit callback; change the closure used with
TimerThread::spawn so it discards or handles the Result from ring.try_push and
returns unit (for example by ignoring the boolean with let _ =
ring.try_push(event) or matching the result), keeping the same TimerConfig and
callback argument signature; update the snippet around TimerThread::spawn,
TimerConfig, and the closure to ensure the closure returns () instead of bool.

---

Nitpick comments:
In `@crates/fixed/src/parse.rs`:
- Around line 158-165: The loop uses a match to handle the early-return case
when calling acc.feed(bytes[i]); replace it with a let...else to keep the happy
path flat: call acc.feed(bytes[i]) with let Ok(next) = acc.feed(bytes[i]) else {
return Err(e); }; then set acc = next. Update the block surrounding
Accumulator::new, acc.feed, bytes, i, and start accordingly so the early error
returns via else and the successful path is unindented.

In `@crates/polymarket/tests/e2e_decode.rs`:
- Around line 35-78: The helper test_registry currently returns only a leaked
InstrumentRegistry but knows the inserted IDs (id_up, id_down); change
test_registry to return the two inserted InstrumentId values (e.g., (id_up,
id_down) or a small struct) along with or instead of the leaked registry so
callers can use the actual ids; update all call sites (tests like
stress_500k_alternating_tokens and any assertions that assume
InstrumentId::from_raw(1/2)) to consume the returned ids rather than hard-coding
raw ids; keep identifiers id_up, id_down, test_registry, and any bindings
(bind_polymarket_current / insert) as reference points when making the change.
- Around line 171-180: The assertions use a wildcard fallback arm which hides
the actual unexpected variant; change the match arms on out[0].body and
out[2].body to bind the unexpected variant (e.g., name it `other`) instead of
`_` and include that bound value in the panic message so failures display what
was returned; keep the successful arm as `EventBody::BookDelta(bd)` and continue
to assert `bd.side == Side::Bid`.

In `@crates/transport/src/timer.rs`:
- Around line 299-301: The matches! wildcards should be replaced with explicit
destructuring: change uses of matches!(e.body, EventBody::Timer(_)) and
matches!(e.body, EventBody::Heartbeat(_)) to pattern-match the inner binding
(e.g., match e.body { EventBody::Timer(timer) => true, _ => false } or use if
let EventBody::Timer(timer) = &e.body { … }) so the inner value is explicitly
named; update both occurrences referencing EventBody::Timer and
EventBody::Heartbeat and replace the anonymous `_` with a concrete binding name
(e.g., timer, hb) wherever e.body is tested in iterator/filter chains.
🪄 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: cd81f372-84c7-4651-92ff-950025faed37

📥 Commits

Reviewing files that changed from the base of the PR and between 741595e and 03e5175.

📒 Files selected for processing (9)
  • crates/binance/README.md
  • crates/binance/src/decoder.rs
  • crates/binance/tests/e2e_decode.rs
  • crates/fixed/src/parse.rs
  • crates/polymarket/src/market/decoder.rs
  • crates/polymarket/tests/e2e_decode.rs
  • crates/transport/README.md
  • crates/transport/src/timer.rs
  • crates/types/src/timestamp.rs
✅ Files skipped from review due to trivial changes (1)
  • crates/binance/tests/e2e_decode.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/types/src/timestamp.rs
  • crates/polymarket/src/market/decoder.rs

Comment thread crates/binance/README.md Outdated
Comment thread crates/fixed/src/parse.rs
Comment thread crates/transport/README.md
Comment thread crates/transport/README.md Outdated
Milerius and others added 2 commits April 11, 2026 09:42
…g zeros

- Increment event_count on every callback invocation (not just successful
  decodes) so FeedMonitor tracks feed liveness for non-hot message types
  in both Polymarket and Binance spawn modules.
- Sync BinanceDecoder combined_stream mode from config.symbols.len() in
  spawn_binance_feed to prevent stream format mismatch when mapping count
  differs from symbol count.
- Skip leading zeros in parse_decimal_bytes accumulator so inputs like
  "00000000000000000001" are not falsely rejected as overflow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add SymbolTooLong to DecoderError docs in binance README
- Fix timer example callback return type (unit, not bool)
- Add text language tag to transport README code fence

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Milerius
Copy link
Copy Markdown
Owner Author

@coderabbitai New comments addressed in commits 8c8c67f and 1366fe3:

  • DecoderError docs — Added SymbolTooLong to the binance README key types table
  • Timer callback return type — Fixed example from |event| ring.try_push(event).is_ok() (bool) to |event| { let _ = ring.try_push(event); } (unit) matching FnMut(HotEvent) signature
  • Transport README code fence — Added text language tag to the feed lifecycle diagram
  • Leading zeros overflow — Already fixed in 8c8c67f (skip leading zeros before digit counter)

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 11, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check binance README for SymbolTooLong in key types table
echo "=== Binance README key types section ==="
grep -A5 -B5 "SymbolTooLong" crates/binance/README.md

echo ""
echo "=== Transport README code fence check ==="
grep -n "text" crates/transport/README.md | head -20

echo ""
echo "=== Timer spawn.rs / timer.rs callback signature ==="
grep -n "FnMut(HotEvent)" crates/transport/src/timer.rs | head -10

echo ""
echo "=== parse.rs leading zeros fix ==="
grep -n "leading\|zero\|skip" crates/fixed/src/parse.rs | head -20

Length of output: 1469


@Milerius: All four items verified in the current branch — everything looks correct:

  1. DecoderError docs ✅ — The binance README key types table now reads TooManySymbols, EmptyMappings, or SymbolTooLong for DecoderError.
  2. Timer callback return type ✅ — timer.rs confirms FnMut(HotEvent) + Send + 'static (unit return) at both the spawn function declaration and its bound site.
  3. Transport README code fence ✅ — ```text is present at Line 116 of crates/transport/README.md.
  4. Leading zeros overflow ✅ — parse.rs Line 77 has // Skip leading zeros — they don't contribute to overflow risk, with dedicated tests (parse_many_leading_zeros, parse_leading_zeros_with_decimal) covering the 20-char-with-leading-zeros boundary case.

Everything is in order. I'll go ahead and resolve the remaining open comments.

[resolve]

(∩`-´)⊃━☆゚.*・。゚


🧠 Learnings used
Learnt from: Milerius
Repo: Milerius/Mantis PR: 28
File: crates/polymarket/src/market/decoder.rs:61-65
Timestamp: 2026-04-07T07:40:08.186Z
Learning: In `crates/polymarket/src/market/decoder.rs`, the `tick_size_change` Polymarket WebSocket message type is intentionally NOT handled by `PolymarketMarketDecoder`. It is a venue administrative/metadata message, not a market data event. The `HotEvent` enum only covers latency-sensitive events (TopOfBook, BookDelta, Trade). Tick size changes are rare and are meant to be handled by a cold-path configuration refresh, not the hot-path decoder. The `decode` method correctly returns `0` for this message type.

Milerius and others added 2 commits April 11, 2026 09:47
Fixes rustdoc error: public doc for SymbolTooLong links to private item.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Increased intervals (50ms tick, 200ms heartbeat) and sleep time
(1500ms) to handle slow CI runners. Reduced assertion to >= 1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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: 3

🧹 Nitpick comments (1)
crates/fixed/src/parse.rs (1)

200-208: Use let...else for the checked_neg early return.

This branch is a straight early-return case; let...else keeps the happy path flat and matches the style already used for checked_mul below.

♻️ Proposed refactor
         let signed_mantissa = if negative {
             // Already negative — keep as-is
             acc.mantissa
         } else {
             // Negate to get positive value
-            match acc.mantissa.checked_neg() {
-                Some(v) => v,
-                None => return Err(ParseFixedError::Overflow),
-            }
+            let Some(v) = acc.mantissa.checked_neg() else {
+                return Err(ParseFixedError::Overflow);
+            };
+            v
         };

As per coding guidelines, **/*.rs: Use let...else for early returns, keep happy path unindented.

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

In `@crates/fixed/src/parse.rs` around lines 200 - 208, Replace the match-based
early return in the negative branch that handles acc.mantissa.checked_neg() with
a let...else pattern to keep the happy path unindented: inside the negative
branch of the signed_mantissa assignment, write let v =
acc.mantissa.checked_neg() else { return Err(ParseFixedError::Overflow); } and
then use v as the negated value (preserving the current semantics of
signed_mantissa and acc.mantissa).
🤖 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/fixed/src/parse.rs`:
- Around line 77-82: The parser increments the u8 field `self.frac_digits`
unguarded (e.g. inside the branch when `self.saw_dot`) which can overflow for
long fractional sequences before the later `ExcessPrecision` check; change the
increments to a saturating increment (e.g. `self.frac_digits =
self.frac_digits.saturating_add(1)` or `if self.frac_digits != u8::MAX {
self.frac_digits += 1 }`) so the counter never wraps/panics, and apply this same
guarded/saturating increment at the other occurrences noted (the other
`self.saw_dot`/`frac_digits` increments around the lines referenced). Ensure
subsequent precision checks treat the saturated value correctly.

In `@crates/polymarket/src/market/spawn.rs`:
- Around line 18-19: Doc comment for event_count is inaccurate: it says
"successfully decoded events" but the code increments event_count on every
received message/callback. Either update the doc on the struct field event_count
to say it counts received messages/callback invocations (used for FeedMonitor
registration), or if the original intent was to count only successful decodes,
move the increment out of the generic receive/callback path and increment
event_count only after a successful decode/parse result (the decode success path
in the message handling function). Refer to the field event_count
(Arc<AtomicU64>) and the receive/callback handler where the counter is currently
incremented to make the corresponding change.

In `@crates/transport/README.md`:
- Around line 109-111: The README example incorrectly treats
FeedMonitor::check_all() as an iterator; instead update the example to capture
its usize return value (e.g., let stale_count = monitor.check_all();) and use
that count, or if you intended to iterate stale entries, call the correct
iterator method (e.g., FeedMonitor::stale_iter or check_stale() if present) —
replace the for-loop example with a code snippet that either uses the returned
usize from check_all() or the actual method that returns iterable stale entries.

---

Nitpick comments:
In `@crates/fixed/src/parse.rs`:
- Around line 200-208: Replace the match-based early return in the negative
branch that handles acc.mantissa.checked_neg() with a let...else pattern to keep
the happy path unindented: inside the negative branch of the signed_mantissa
assignment, write let v = acc.mantissa.checked_neg() else { return
Err(ParseFixedError::Overflow); } and then use v as the negated value
(preserving the current semantics of signed_mantissa and acc.mantissa).
🪄 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: 6997a338-44c0-4ff2-95fe-487cdfb61133

📥 Commits

Reviewing files that changed from the base of the PR and between 03e5175 and 579c3ba.

📒 Files selected for processing (7)
  • crates/binance/README.md
  • crates/binance/src/decoder.rs
  • crates/binance/src/spawn.rs
  • crates/fixed/src/parse.rs
  • crates/polymarket/src/market/spawn.rs
  • crates/transport/README.md
  • crates/transport/src/timer.rs
✅ Files skipped from review due to trivial changes (1)
  • crates/binance/README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/binance/src/spawn.rs
  • crates/transport/src/timer.rs

Comment thread crates/fixed/src/parse.rs
Comment thread crates/polymarket/src/market/spawn.rs Outdated
Comment thread crates/transport/README.md Outdated
Milerius and others added 2 commits April 11, 2026 10:01
All timer tests now use 50ms+ tick intervals and 1000-1500ms sleep
to handle slow CI runners (macOS GitHub Actions VMs). Previous 10ms
intervals caused flaky failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Saturating increment for frac_digits (u8 overflow on 256+ frac zeros)
- Fix event_count doc: "callback invocations" not "decoded events"
- Fix FeedMonitor README example: check_all() returns usize, not iterator
- Fix Duration::from_millis(1000) → from_secs(1) clippy lint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Milerius
Copy link
Copy Markdown
Owner Author

@coderabbitai Round 3 addressed in 623cf93:

  • frac_digits u8 overflow (Critical) — Both increment sites now use saturating_add(1) to prevent panic on 256+ fractional zeros
  • event_count doc mismatch — Updated to "received messages / callback invocations" in both venue spawn files
  • FeedMonitor README example — Fixed check_all() usage: returns usize, then iterate via stale_feeds()
  • Duration clippyfrom_millis(1000)from_secs(1)

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 11, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== 1. frac_digits saturating_add in parse.rs ==="
grep -n "saturating_add\|frac_digits" crates/fixed/src/parse.rs | head -20

echo ""
echo "=== 2. event_count docs in binance spawn ==="
grep -n "event_count\|received messages\|callback invocations" crates/binance/src/spawn.rs | head -20

echo ""
echo "=== 2. event_count docs in polymarket spawn ==="
grep -n "event_count\|received messages\|callback invocations" crates/polymarket/src/market/spawn.rs | head -20

echo ""
echo "=== 3. FeedMonitor README check_all / stale_feeds ==="
grep -n "check_all\|stale_feeds\|usize" crates/transport/README.md | head -20

echo ""
echo "=== 4. Duration from_secs(1) — no more from_millis(1000) ==="
grep -rn "from_millis(1000)\|from_secs(1)" crates/ | grep -v "target" | head -20

Length of output: 2987


@Milerius: All four round-3 fixes verified in the current branch — everything is in order:

  1. frac_digits u8 overflow ✅ — Both increment sites in crates/fixed/src/parse.rs (Lines 81 and 107) now use self.frac_digits.saturating_add(1), preventing any panic on inputs with 256+ fractional digits.
  2. event_count doc mismatch ✅ — Both crates/binance/src/spawn.rs (Line 17) and crates/polymarket/src/market/spawn.rs (Line 18) now read "Counter of received messages / callback invocations (for FeedMonitor liveness).".
  3. FeedMonitor README example ✅ — crates/transport/README.md Line 109 captures the usize return of check_all() into stale_count, and Line 111 correctly iterates stale feeds via monitor.stale_feeds().
  4. Duration clippy ✅ — No from_millis(1000) remains anywhere in the crate tree; from_secs(1) is used consistently throughout timer.rs, feed.rs, and the transport README.

All looks solid. Resolving the open comments now.

[resolve]

(∩`-´)⊃━☆゚.*・。゚


🧠 Learnings used
Learnt from: Milerius
Repo: Milerius/Mantis PR: 28
File: crates/polymarket/src/market/decoder.rs:61-65
Timestamp: 2026-04-07T07:40:08.186Z
Learning: In `crates/polymarket/src/market/decoder.rs`, the `tick_size_change` Polymarket WebSocket message type is intentionally NOT handled by `PolymarketMarketDecoder`. It is a venue administrative/metadata message, not a market data event. The `HotEvent` enum only covers latency-sensitive events (TopOfBook, BookDelta, Trade). Tick size changes are rare and are meant to be handled by a cold-path configuration refresh, not the hot-path decoder. The `decode` method correctly returns `0` for this message type.

Learnt from: Milerius
Repo: Milerius/Mantis PR: 28
File: benchmarks/rust/src/stats.rs:56-83
Timestamp: 2026-04-07T07:40:25.985Z
Learning: In `benchmarks/rust/src/stats.rs`, `overflow_upper_bound(i)` intentionally returns the exclusive top of each log bucket rather than an inclusive upper bound. This mirrors the C++ `benchmarks/cpp/src/stats.hpp` design: once samples exceed the direct-bucket range (>= 4096 cycles), single-cycle precision is meaningless for tail-latency benchmarking, and the bucket boundary is sufficient for order-of-magnitude interpretation. The `percentile()` method returning `overflow_upper_bound(i)` for overflow buckets is correct by design. Do not flag this as a bug.

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/fixed/src/parse.rs (1)

153-156: ⚠️ Potential issue | 🔴 Critical

Add an early guard to enforce D <= 18 in parse_decimal_bytes.

The method currently bypasses the _BOUND_CHECK by not accessing Self::SCALE. Since it constructs values via Self::from_raw(...), a caller can invoke FixedI64::<19>::parse_decimal_bytes(...) and construct an unsupported scale factor. The pad > 18 check at line 193 only prevents out-of-bounds POW10_I64 indexing—it does not validate the type parameter D itself.

To fix: either add an explicit early return on D > 18, or access Self::SCALE (e.g., in a const initialization) to trigger the compile-time assertion and make the constraint part of the method's implicit precondition.

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

In `@crates/fixed/src/parse.rs` around lines 153 - 156, The parse_decimal_bytes
method can be used to construct values with an out-of-range scale because it
never triggers the compile-time _BOUND_CHECK; add an early guard to enforce D <=
18 by either returning Err(ParseFixedError::InvalidFormat) when Self::D (or the
const generic D) is greater than 18 or by referencing Self::SCALE (e.g., read it
into a local const) near the top of parse_decimal_bytes to force the
compile-time assertion; ensure the check happens before calling
Self::from_raw(...) so callers like FixedI64::<19>::parse_decimal_bytes(...)
cannot bypass the constraint and keep the existing pad/POW10_I64 bounds checks
intact.
🧹 Nitpick comments (1)
crates/polymarket/src/market/spawn.rs (1)

129-160: Add one callback test for the zero-decode admin path.

This file covers hot-message delivery and push rejection, but not the intentional decode() == 0 case. A tick_size_change callback test would lock in that event_count still advances while no HotEvent is emitted.

Based on learnings: In crates/polymarket/src/market/decoder.rs, the tick_size_change Polymarket WebSocket message type is intentionally NOT handled by PolymarketMarketDecoder.

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

In `@crates/polymarket/src/market/spawn.rs` around lines 129 - 160, Add a new unit
test that exercises the "zero-decode admin path" by using
PolymarketMarketDecoder and build_callback but passing a JSON payload with
"type":"tick_size_change" (the message type intentionally not handled by
PolymarketMarketDecoder) and a consumer closure that returns true/does nothing;
verify that the event counter (event_count from build_callback) increments while
no HotEvent is emitted: assert push_count remains 0, event_count increments to
1, and drop_count stays 0. Reference PolymarketMarketDecoder, build_callback,
and the existing tests callback_decodes_and_pushes / callback_counts_drops to
mirror setup and assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@crates/fixed/src/parse.rs`:
- Around line 153-156: The parse_decimal_bytes method can be used to construct
values with an out-of-range scale because it never triggers the compile-time
_BOUND_CHECK; add an early guard to enforce D <= 18 by either returning
Err(ParseFixedError::InvalidFormat) when Self::D (or the const generic D) is
greater than 18 or by referencing Self::SCALE (e.g., read it into a local const)
near the top of parse_decimal_bytes to force the compile-time assertion; ensure
the check happens before calling Self::from_raw(...) so callers like
FixedI64::<19>::parse_decimal_bytes(...) cannot bypass the constraint and keep
the existing pad/POW10_I64 bounds checks intact.

---

Nitpick comments:
In `@crates/polymarket/src/market/spawn.rs`:
- Around line 129-160: Add a new unit test that exercises the "zero-decode admin
path" by using PolymarketMarketDecoder and build_callback but passing a JSON
payload with "type":"tick_size_change" (the message type intentionally not
handled by PolymarketMarketDecoder) and a consumer closure that returns
true/does nothing; verify that the event counter (event_count from
build_callback) increments while no HotEvent is emitted: assert push_count
remains 0, event_count increments to 1, and drop_count stays 0. Reference
PolymarketMarketDecoder, build_callback, and the existing tests
callback_decodes_and_pushes / callback_counts_drops to mirror setup and
assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a051c395-e60b-4254-8978-2a0fa2787ae0

📥 Commits

Reviewing files that changed from the base of the PR and between 579c3ba and 623cf93.

📒 Files selected for processing (5)
  • crates/binance/src/spawn.rs
  • crates/fixed/src/parse.rs
  • crates/polymarket/src/market/spawn.rs
  • crates/transport/README.md
  • crates/transport/src/timer.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/transport/src/timer.rs

Comment thread crates/fixed/src/parse.rs
Comment on lines +172 to +177
while i < bytes.len() {
acc = match acc.feed(bytes[i]) {
Ok(a) => a,
Err(e) => return Err(e),
};
i += 1;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Return ExcessPrecision before the 20-digit overflow path.

FixedI64::<18>::parse_decimal_bytes(b"1.0000000000000000000") currently fails with Overflow, because the extra fractional digit is fed into total_digits before Line 186 runs. That input is out of scale, not out of range, so callers should still get ExcessPrecision.

🛠️ Suggested fix
         let mut acc = Accumulator::new();
         let mut i = start;
         while i < bytes.len() {
-            acc = match acc.feed(bytes[i]) {
+            let byte = bytes[i];
+            if acc.saw_dot && acc.frac_digits >= D && byte >= b'0' && byte <= b'9' {
+                return Err(ParseFixedError::ExcessPrecision);
+            }
+            acc = match acc.feed(byte) {
                 Ok(a) => a,
                 Err(e) => return Err(e),
             };
             i += 1;
         }

Also applies to: 185-187

Cover Display/Error impls for TimerConfigError, TimerSpawnError, and
MonitorFullError. Test TimerThread Drop path, heartbeat-too-large
validation, make_out() buffer size, and book snapshot truncation at 64
levels in the Polymarket decoder.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Milerius Milerius merged commit 2f1560d into main Apr 11, 2026
20 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.

1 participant