Skip to main content

sample_app/
sample_app.rs

1//! Sample app: a tour of `subms-mpsc-queue` in an order-entry setting - N
2//! gateway threads funnel orders into one matching-engine consumer. Run the
3//! base with `cargo run --example sample_app`; add `--all-features` (or a
4//! subset like `--features bounded`) to light up the feature sections.
5//!
6//! * base     - N order-entry gateways fan in to one matching-engine consumer
7//! * mpmc     - shard the match loop across several consumers on one ring
8//! * bounded  - a fixed-capacity inbox that sheds load rather than the heap
9//! * batch    - the match loop drains one tick's orders in a single fenced pass
10//! * metrics  - a health snapshot of enqueue / dequeue counts
11//! * affinity - pin the match loop to a core so it stops migrating
12
13use std::sync::Arc;
14use std::thread;
15
16use subms_mpsc_queue::{MpscQueue, PopResult};
17
18const GATEWAYS: usize = 4;
19const ORDERS_PER_GATEWAY: usize = 1_000;
20
21fn order_id(gateway: usize, seq: usize) -> u64 {
22    ((gateway as u64) << 32) | seq as u64
23}
24
25fn main() {
26    base_order_fan_in();
27
28    #[cfg(feature = "mpmc")]
29    mpmc_sharded_match();
30
31    #[cfg(feature = "bounded")]
32    bounded_inbox_backpressure();
33
34    #[cfg(feature = "batch")]
35    batch_drain_per_tick();
36
37    #[cfg(feature = "metrics")]
38    metrics_health_snapshot();
39
40    #[cfg(feature = "affinity")]
41    affinity_pin_match_loop();
42}
43
44/// Base API: every order-entry gateway pushes onto one shared queue and a
45/// single matching-engine thread drains it. `push` is wait-free per producer;
46/// the consumer tolerates the dangling-tail window by retrying on
47/// `Inconsistent` rather than mistaking a mid-publish gateway for an empty
48/// queue.
49fn base_order_fan_in() {
50    println!("== base: order-entry gateways fan in to one matching engine ==");
51    let q: Arc<MpscQueue<u64>> = Arc::new(MpscQueue::new());
52
53    let gateways: Vec<_> = (0..GATEWAYS)
54        .map(|g| {
55            let q = Arc::clone(&q);
56            thread::spawn(move || {
57                for seq in 0..ORDERS_PER_GATEWAY {
58                    q.push(order_id(g, seq));
59                }
60            })
61        })
62        .collect();
63    for h in gateways {
64        h.join().unwrap();
65    }
66
67    // Every gateway handle is joined, so the queue is uniquely owned again and
68    // the single consumer can take it back out (try_pop needs &mut self).
69    let mut q = Arc::into_inner(q).expect("all gateway handles dropped");
70
71    let total = GATEWAYS * ORDERS_PER_GATEWAY;
72    println!("  inbox depth before the match loop starts: {}", q.len());
73    let first = *q.peek().expect("the inbox is not empty");
74    println!(
75        "  head of book: gateway {} seq {}",
76        first >> 32,
77        first & 0xffff_ffff
78    );
79
80    let mut per_gateway = [0usize; GATEWAYS];
81    let mut last_seq = [None::<u64>; GATEWAYS];
82    let mut matched = 0usize;
83    loop {
84        match q.try_pop() {
85            PopResult::Some(order) => {
86                let g = (order >> 32) as usize;
87                let seq = order & 0xffff_ffff;
88                if let Some(prev) = last_seq[g] {
89                    assert!(seq > prev, "orders from one gateway stay in FIFO order");
90                }
91                last_seq[g] = Some(seq);
92                per_gateway[g] += 1;
93                matched += 1;
94            }
95            PopResult::Inconsistent => continue,
96            PopResult::Empty => break,
97        }
98    }
99
100    println!("  {GATEWAYS} gateways x {ORDERS_PER_GATEWAY} orders -> matched {matched}");
101    println!("  per-gateway tally: {per_gateway:?}");
102    assert_eq!(matched, total, "no order dropped, none duplicated");
103    for count in per_gateway {
104        assert_eq!(count, ORDERS_PER_GATEWAY, "every gateway fully drained");
105    }
106
107    // Kill-switch: a venue disconnect voids everything still queued rather than
108    // matching it against a book that has moved on.
109    for seq in 0..32 {
110        q.push(order_id(0, seq));
111    }
112    let voided = q.clear();
113    println!(
114        "  kill switch voided {voided} queued orders, inbox empty: {}",
115        q.is_empty()
116    );
117    assert_eq!(voided, 32);
118    assert!(q.is_empty());
119}
120
121/// `mpmc` feature: the base queue allows one consumer. When one match loop
122/// cannot keep up, `MpmcQueue` is a bounded Disruptor-style ring where several
123/// consumer shards race the head; the loser of a CAS refreshes and retries.
124/// Producers race the tail the same way.
125#[cfg(feature = "mpmc")]
126fn mpmc_sharded_match() {
127    use std::sync::atomic::{AtomicUsize, Ordering};
128
129    use subms_mpsc_queue::MpmcQueue;
130
131    println!("\n== mpmc: shard the match loop across several consumers ==");
132    let shards = 3usize;
133    let ring: Arc<MpmcQueue<u64>> = Arc::new(MpmcQueue::new(1_024));
134    let total = GATEWAYS * ORDERS_PER_GATEWAY;
135
136    let gateways: Vec<_> = (0..GATEWAYS)
137        .map(|g| {
138            let ring = Arc::clone(&ring);
139            thread::spawn(move || {
140                for seq in 0..ORDERS_PER_GATEWAY {
141                    let mut order = order_id(g, seq);
142                    while let Err(rejected) = ring.try_enqueue(order) {
143                        order = rejected;
144                        std::hint::spin_loop();
145                    }
146                }
147            })
148        })
149        .collect();
150
151    let matched = Arc::new(AtomicUsize::new(0));
152    let consumers: Vec<_> = (0..shards)
153        .map(|_| {
154            let ring = Arc::clone(&ring);
155            let matched = Arc::clone(&matched);
156            thread::spawn(move || {
157                let mut local = 0usize;
158                loop {
159                    if ring.try_dequeue().is_some() {
160                        local += 1;
161                        matched.fetch_add(1, Ordering::Relaxed);
162                    } else if matched.load(Ordering::Relaxed) >= total {
163                        break;
164                    } else {
165                        std::hint::spin_loop();
166                    }
167                }
168                local
169            })
170        })
171        .collect();
172
173    for h in gateways {
174        h.join().unwrap();
175    }
176    let drained: usize = consumers.into_iter().map(|c| c.join().unwrap()).sum();
177    // cas_retries() is the contention read-out, deliberately not printed: it is
178    // a property of how the OS scheduled these threads on this run.
179    println!(
180        "  {shards} shards drained {drained} orders, ring empty: {}",
181        ring.is_empty()
182    );
183    assert_eq!(
184        drained, total,
185        "shards together drain every order exactly once"
186    );
187    assert_eq!(
188        ring.producer_index(),
189        ring.consumer_index(),
190        "every claimed slot was consumed"
191    );
192}
193
194/// `bounded` feature: a fixed-capacity ring gives the gateway backpressure.
195/// On the base queue a slow match loop turns into unbounded heap growth; the
196/// bounded inbox returns the rejected order so the gateway can retry or shed
197/// load instead of the backlog landing on the heap.
198#[cfg(feature = "bounded")]
199fn bounded_inbox_backpressure() {
200    use subms_mpsc_queue::BoundedMpscQueue;
201
202    println!("\n== bounded: a fixed-capacity inbox that pushes back ==");
203    let mut inbox: BoundedMpscQueue<u64> = BoundedMpscQueue::new(4);
204    let cap = inbox.capacity();
205
206    let mut accepted = 0usize;
207    let mut rejected = 0usize;
208    for seq in 0..cap + 2 {
209        match inbox.try_enqueue(order_id(0, seq)) {
210            Ok(()) => accepted += 1,
211            Err(_order) => rejected += 1,
212        }
213    }
214    println!("  capacity {cap}: accepted {accepted}, shed {rejected} while full");
215    assert_eq!(accepted, cap, "accepts exactly one full ring");
216    assert_eq!(rejected, 2, "the overflow is handed back, not queued");
217    assert!(inbox.is_full());
218
219    // Drain one, and a previously-rejected order now fits.
220    assert!(
221        inbox.try_dequeue().is_some(),
222        "match loop consumes one order"
223    );
224    assert!(
225        inbox.try_enqueue(order_id(0, 99)).is_ok(),
226        "a freed slot reopens the inbox"
227    );
228
229    // The two monotonic cursors are what a health check scrapes: their
230    // difference is inbox lag, and each on its own gives a rate between polls.
231    println!(
232        "  producer index {} - consumer index {} = lag {}",
233        inbox.producer_index(),
234        inbox.consumer_index(),
235        inbox.len()
236    );
237    assert_eq!(inbox.producer_index() - inbox.consumer_index(), inbox.len());
238}
239
240/// `batch` feature: a match loop that runs on a tick drains a whole tick's
241/// worth of orders in one fenced pass. `try_dequeue_batch` pays one acquire
242/// per call instead of one per order and stops early on empty or a mid-publish
243/// gateway.
244#[cfg(feature = "batch")]
245fn batch_drain_per_tick() {
246    use subms_mpsc_queue::BatchMpscQueue;
247
248    println!("\n== batch: publish and drain a whole tick in one pass ==");
249    const TICK: usize = 256;
250    const BURST: usize = 50;
251    let mut q: BatchMpscQueue<u64> = BatchMpscQueue::new();
252    let total = 1_000usize;
253
254    // A gateway that decodes a wire frame already holds a run of orders. One
255    // head swap publishes the whole run instead of BURST of them.
256    let mut published = 0usize;
257    while published < total {
258        let base = published;
259        published += q.push_batch((base..base + BURST).map(|seq| order_id(0, seq)));
260    }
261    println!("  {published} orders published in {} swaps", total / BURST);
262    assert_eq!(published, total);
263
264    let mut buf: Vec<Option<u64>> = (0..TICK).map(|_| None).collect();
265    let mut ticks = 0usize;
266    let mut matched = 0usize;
267    loop {
268        let n = q.try_dequeue_batch(&mut buf);
269        if n == 0 {
270            break;
271        }
272        ticks += 1;
273        for slot in buf.iter_mut().take(n) {
274            let _ = slot.take();
275            matched += 1;
276        }
277    }
278    println!("  drained {matched} orders across {ticks} ticks of up to {TICK}");
279    assert_eq!(matched, total, "every queued order is drained");
280    assert_eq!(
281        ticks,
282        total.div_ceil(TICK),
283        "each tick drains a full buffer until the tail"
284    );
285
286    // The callback form skips the buffer entirely when the match loop's work
287    // is per-order anyway. Here it accumulates notional.
288    q.push_batch((0..64).map(|seq| order_id(1, seq)));
289    let mut notional = 0u64;
290    let handled = q.drain(TICK, |order| notional += order & 0xffff_ffff);
291    println!("  drain callback handled {handled} orders, notional {notional}");
292    assert_eq!(handled, 64);
293    assert_eq!(notional, (0..64u64).sum::<u64>());
294    assert!(q.is_empty());
295}
296
297/// `metrics` feature: wrap the queue in per-instance counters to answer "is
298/// this inbox actually contended" from a health snapshot rather than a guess.
299/// The counters are relaxed - advisory, not part of the queue's correctness.
300#[cfg(feature = "metrics")]
301fn metrics_health_snapshot() {
302    use subms_mpsc_queue::MetricsMpscQueue;
303
304    println!("\n== metrics: a health snapshot of the inbox ==");
305    let mut q: MetricsMpscQueue<u64> = MetricsMpscQueue::new();
306    for seq in 0..500 {
307        q.push(order_id(0, seq));
308    }
309    let mut matched = 0usize;
310    while matched < 500 {
311        match q.try_pop() {
312            PopResult::Some(_) => matched += 1,
313            PopResult::Inconsistent => continue,
314            PopResult::Empty => break,
315        }
316    }
317    // One extra pop on the drained queue to register a dequeue miss.
318    let _ = q.try_pop();
319
320    let snap = q.snapshot();
321    println!(
322        "  enqueue_ok={} dequeue_ok={} dequeue_fail={}",
323        snap.enqueue_ok, snap.dequeue_ok, snap.dequeue_fail
324    );
325    assert_eq!(snap.enqueue_ok, 500, "every push is counted");
326    assert_eq!(snap.dequeue_ok, 500, "every matched order is counted");
327    assert!(
328        snap.dequeue_fail >= 1,
329        "the miss on the drained queue is counted"
330    );
331}
332
333/// `affinity` feature: pin the match loop to a core so it stops migrating and
334/// trashing the producers' cache lines. Real on Linux and Windows; a documented
335/// `Unsupported` no-op elsewhere. An empty core set is always rejected.
336#[cfg(feature = "affinity")]
337fn affinity_pin_match_loop() {
338    use subms_mpsc_queue::{AffinityError, set_affinity};
339
340    println!("\n== affinity: pin the match loop to a core ==");
341    match set_affinity(&[0]) {
342        Ok(()) => println!("  match loop pinned to core 0"),
343        Err(e) => println!("  pinning unavailable: {e}"),
344    }
345    assert!(
346        matches!(set_affinity(&[]), Err(AffinityError::InvalidCore(0))),
347        "an empty core set is always rejected"
348    );
349}