Skip to main content

sample_app/
sample_app.rs

1//! Sample app: a tour of `subms-spsc-ring-buffer`, base API first, then each
2//! optional feature. Run the base with `cargo run --example sample_app`; add
3//! `--all-features` (or a subset like `--features bulk`) to see the feature
4//! sections light up.
5//!
6//! Domain: a market-data feed handler thread hands ticks to a strategy thread
7//! over the wait-free ring. A full ring makes the feed handler drop the tick
8//! (backpressure) rather than block the hot path.
9//!
10//! * base            - feed-handler -> strategy handoff, drop-on-full
11//! * bulk            - drain a NIC receive batch in one fenced call
12//! * wait-strategies - a blocking handoff when a wakeup cost is affordable
13//! * mpsc-fan-in     - many venue feeds into one strategy, still wait-free per feed
14//! * mpmc-disruptor  - broadcast one tick stream to strategy + risk monitor
15//! * metrics         - per-instance enqueue/dequeue + max-depth counters
16
17use std::thread;
18
19use subms_spsc_ring_buffer::SpscRingBuffer;
20
21#[derive(Clone, Copy, Debug, PartialEq)]
22struct Tick {
23    seq: u64,
24    price_cents: u32,
25}
26
27fn main() {
28    base_feed_to_strategy();
29
30    #[cfg(feature = "bulk")]
31    bulk_batch_ingest();
32
33    #[cfg(feature = "wait-strategies")]
34    blocking_handoff();
35
36    #[cfg(feature = "mpsc-fan-in")]
37    many_venue_fan_in();
38
39    #[cfg(feature = "mpmc-disruptor")]
40    broadcast_to_strategy_and_risk();
41
42    #[cfg(feature = "metrics")]
43    instrumented_handoff();
44}
45
46/// Base API: a feed-handler thread pushes ticks to a strategy thread. Both ends
47/// are wait-free; when the ring fills, `try_push` hands the tick back so the
48/// feed handler drops it instead of stalling the hot path.
49fn base_feed_to_strategy() {
50    println!("== base: feed-handler -> strategy handoff ==");
51
52    // Drop-on-full is the caller's decision. Shown deterministically on a
53    // small ring: capacity 4, six ticks offered, the last two are dropped.
54    let (mut tx, _rx) = SpscRingBuffer::with_capacity::<Tick>(4);
55    let mut dropped = 0usize;
56    for seq in 0..6u64 {
57        let tick = Tick {
58            seq,
59            price_cents: 10_000 + seq as u32,
60        };
61        if tx.try_push(tick).is_err() {
62            dropped += 1;
63        }
64    }
65    println!("  cap-4 ring, 6 offered -> {dropped} dropped under backpressure");
66    assert_eq!(
67        dropped, 2,
68        "two ticks past capacity are dropped, not blocked"
69    );
70
71    // Steady state: a drained ring loses nothing and preserves feed order.
72    let n = 50_000u64;
73    let (mut tx, mut rx) = SpscRingBuffer::with_capacity::<Tick>(1024);
74    let feed = thread::spawn(move || {
75        for seq in 0..n {
76            let tick = Tick {
77                seq,
78                price_cents: 10_000 + (seq % 500) as u32,
79            };
80            while tx.try_push(tick).is_err() {
81                std::hint::spin_loop();
82            }
83        }
84    });
85    let strategy = thread::spawn(move || {
86        let mut expected = 0u64;
87        while expected < n {
88            if let Some(tick) = rx.try_pop() {
89                assert_eq!(tick.seq, expected, "ticks arrive in feed order");
90                expected += 1;
91            }
92        }
93        expected
94    });
95    feed.join().unwrap();
96    let received = strategy.join().unwrap();
97    println!("  streamed {received} ticks in order, zero loss when drained");
98    assert_eq!(received, n);
99}
100
101/// `bulk` feature: a feed handler often lifts a whole batch of ticks off one
102/// NIC receive. `try_enqueue_bulk` copies the run in behind a single release
103/// fence instead of one per tick; the strategy drains behind one acquire fence.
104#[cfg(feature = "bulk")]
105fn bulk_batch_ingest() {
106    println!("\n== bulk: batch a NIC receive into the ring ==");
107    let (mut tx, mut rx) = SpscRingBuffer::with_capacity::<Tick>(16);
108    let batch: Vec<Tick> = (0..10)
109        .map(|seq| Tick {
110            seq,
111            price_cents: 20_000 + seq as u32,
112        })
113        .collect();
114
115    let pushed = tx.try_enqueue_bulk(&batch);
116    println!(
117        "  offered {} ticks, took {pushed} in one fenced call",
118        batch.len()
119    );
120    assert_eq!(pushed, 10);
121
122    let mut out = [Tick {
123        seq: 0,
124        price_cents: 0,
125    }; 10];
126    let drained = rx.try_dequeue_bulk(&mut out);
127    println!("  drained {drained} in one fenced call");
128    assert_eq!(drained, 10);
129    assert_eq!(out.as_slice(), batch.as_slice(), "bulk preserves order");
130}
131
132/// `wait-strategies` feature: wrap the non-blocking ends in a blocking handle
133/// with a backoff policy. `YieldStrategy` lets other threads run between
134/// retries, a sensible default when the feed and strategy share cores.
135#[cfg(feature = "wait-strategies")]
136fn blocking_handoff() {
137    use subms_spsc_ring_buffer::{BlockingSpscConsumer, BlockingSpscProducer, YieldStrategy};
138
139    println!("\n== wait-strategies: blocking handoff (yield backoff) ==");
140    let (tx, rx) = SpscRingBuffer::with_capacity::<Tick>(8);
141    let mut producer = BlockingSpscProducer::new(tx, YieldStrategy);
142    let mut consumer = BlockingSpscConsumer::new(rx, YieldStrategy);
143
144    let n = 5_000u64;
145    let feed = thread::spawn(move || {
146        for seq in 0..n {
147            producer.push(Tick {
148                seq,
149                price_cents: 30_000,
150            });
151        }
152    });
153    let strategy = thread::spawn(move || {
154        for expected in 0..n {
155            let tick = consumer.pop();
156            assert_eq!(tick.seq, expected, "blocking pop keeps feed order");
157        }
158        n
159    });
160    feed.join().unwrap();
161    let got = strategy.join().unwrap();
162    println!("  handed off {got} ticks, producer blocked on full instead of dropping");
163    assert_eq!(got, n);
164}
165
166/// `mpsc-fan-in` feature: several venue feeds, one strategy consumer. Each feed
167/// owns an independent SPSC ring (still wait-free against its own counter); the
168/// consumer round-robins so a quiet venue never starves a busy one.
169#[cfg(feature = "mpsc-fan-in")]
170fn many_venue_fan_in() {
171    use subms_spsc_ring_buffer::MpscFanIn;
172
173    println!("\n== mpsc-fan-in: three venue feeds -> one strategy ==");
174    let venues = 3usize;
175    let per_venue = 20_000u64;
176    let (mut producers, mut consumer) = MpscFanIn::with_capacity::<Tick>(venues, 256);
177
178    let mut feeds = Vec::new();
179    for venue in 0..venues {
180        let mut p = producers.remove(0);
181        feeds.push(thread::spawn(move || {
182            for seq in 0..per_venue {
183                let tick = Tick {
184                    seq,
185                    price_cents: 40_000 + venue as u32,
186                };
187                while p.try_push(tick).is_err() {
188                    std::hint::spin_loop();
189                }
190            }
191        }));
192    }
193    let total = per_venue * venues as u64;
194    let strategy = thread::spawn(move || {
195        let mut got = 0u64;
196        while got < total {
197            if consumer.try_pop().is_some() {
198                got += 1;
199            }
200        }
201        got
202    });
203    for f in feeds {
204        f.join().unwrap();
205    }
206    let got = strategy.join().unwrap();
207    println!("  {venues} feeds x {per_venue} ticks -> consumer drained {got}");
208    assert_eq!(got, total);
209}
210
211/// `mpmc-disruptor` feature: broadcast one tick stream to independent readers,
212/// a strategy and a risk monitor, each of which sees every published tick. (To
213/// have each tick handled by exactly one reader, reach for `mpsc-fan-in`.)
214#[cfg(feature = "mpmc-disruptor")]
215fn broadcast_to_strategy_and_risk() {
216    use subms_spsc_ring_buffer::MpmcDisruptor;
217
218    println!("\n== mpmc-disruptor: broadcast to strategy + risk ==");
219    let n = 8u64;
220    let (producer, mut consumers) = MpmcDisruptor::with_consumers::<Tick>(16, 2);
221    let (strategy, rest) = consumers.split_at_mut(1);
222    let strategy = &mut strategy[0];
223    let risk = &mut rest[0];
224
225    // Small and single-threaded so the tour self-verifies; the threaded
226    // broadcast path is pinned in the tests.
227    let mut published = 0u64;
228    let mut strat_seen = Vec::new();
229    let mut risk_seen = Vec::new();
230    while published < n {
231        while published < n
232            && producer
233                .try_publish(Tick {
234                    seq: published,
235                    price_cents: 50_000,
236                })
237                .is_ok()
238        {
239            published += 1;
240        }
241        while let Some(t) = strategy.try_consume() {
242            strat_seen.push(t.seq);
243        }
244        while let Some(t) = risk.try_consume() {
245            risk_seen.push(t.seq);
246        }
247    }
248    println!(
249        "  published {published}; strategy saw {}, risk saw {}",
250        strat_seen.len(),
251        risk_seen.len()
252    );
253    let expected: Vec<u64> = (0..n).collect();
254    assert_eq!(strat_seen, expected, "strategy sees every tick");
255    assert_eq!(risk_seen, expected, "risk monitor sees every tick too");
256}
257
258/// `metrics` feature: wrap a base pair to count enqueue/dequeue success + fail
259/// and track the high-water depth - operational stats for a feed, at the cost
260/// of one atomic increment per op.
261#[cfg(feature = "metrics")]
262fn instrumented_handoff() {
263    use subms_spsc_ring_buffer::InstrumentedSpsc;
264
265    println!("\n== metrics: instrumented feed handoff ==");
266    let (tx, rx) = SpscRingBuffer::with_capacity::<Tick>(4);
267    let (mut tx, mut rx, metrics) = InstrumentedSpsc::wrap(tx, rx);
268
269    // Fill to capacity, one overflow drop, then drain plus one empty poll.
270    for seq in 0..4u64 {
271        tx.try_push(Tick {
272            seq,
273            price_cents: 60_000,
274        })
275        .unwrap();
276    }
277    assert!(
278        tx.try_push(Tick {
279            seq: 99,
280            price_cents: 0
281        })
282        .is_err()
283    );
284    for _ in 0..4 {
285        rx.try_pop().unwrap();
286    }
287    assert!(rx.try_pop().is_none());
288
289    let snap = metrics.snapshot();
290    println!(
291        "  enqueued {} (dropped {}), dequeued {} (empty {}), peak depth {}",
292        snap.enqueue_success,
293        snap.enqueue_fail,
294        snap.dequeue_success,
295        snap.dequeue_fail,
296        snap.max_depth_observed
297    );
298    assert_eq!(snap.enqueue_success, 4);
299    assert_eq!(snap.enqueue_fail, 1);
300    assert_eq!(snap.dequeue_success, 4);
301    assert_eq!(snap.dequeue_fail, 1);
302    assert_eq!(snap.max_depth_observed, 4);
303}