Skip to main content

SpscRingBuffer

Struct SpscRingBuffer 

Source
pub struct SpscRingBuffer;
Expand description

Constructor namespace. Use SpscRingBuffer::with_capacity.

Implementations§

Source§

impl SpscRingBuffer

Source

pub fn with_capacity<T>(requested_capacity: usize) -> (Producer<T>, Consumer<T>)

Build a (producer, consumer) pair backed by a buffer of at least requested_capacity slots, rounded up to the next power of two. A floor of 2 is enforced.

Examples found in repository?
examples/perf_features.rs (line 182)
181fn pair(cap: usize) -> (Producer<u64>, Consumer<u64>) {
182    let (mut tx, rx) = SpscRingBuffer::with_capacity::<u64>(cap);
183    for i in 0..cap / 2 {
184        let _ = tx.try_push(i as u64);
185    }
186    (tx, rx)
187}
More examples
Hide additional examples
examples/demo.rs (line 5)
4fn main() {
5    let (mut tx, mut rx) = SpscRingBuffer::with_capacity::<u32>(16);
6
7    let producer = thread::spawn(move || {
8        for i in 0..10u32 {
9            while tx.try_push(i).is_err() {}
10        }
11    });
12
13    let consumer = thread::spawn(move || {
14        let mut seen = Vec::new();
15        while seen.len() < 10 {
16            if let Some(v) = rx.try_pop() {
17                seen.push(v);
18            }
19        }
20        seen
21    });
22
23    producer.join().unwrap();
24    let seen = consumer.join().unwrap();
25    println!("consumed: {seen:?}");
26}
examples/sample_app.rs (line 54)
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, mut 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    // Occupancy is what a queue-depth alarm reads, and peek lets the strategy
72    // inspect the oldest tick before deciding to consume it.
73    let oldest = rx.peek().expect("ring is full").seq;
74    println!(
75        "  depth {}/{} full={}, oldest queued seq {oldest}",
76        rx.len(),
77        rx.capacity(),
78        tx.is_full()
79    );
80    println!("  dropped {} stale ticks on resync", rx.clear());
81
82    // Steady state: a drained ring loses nothing and preserves feed order.
83    let n = 50_000u64;
84    let (mut tx, mut rx) = SpscRingBuffer::with_capacity::<Tick>(1024);
85    let feed = thread::spawn(move || {
86        for seq in 0..n {
87            let tick = Tick {
88                seq,
89                price_cents: 10_000 + (seq % 500) as u32,
90            };
91            while tx.try_push(tick).is_err() {
92                std::hint::spin_loop();
93            }
94        }
95    });
96    let strategy = thread::spawn(move || {
97        let mut expected = 0u64;
98        while expected < n {
99            if let Some(tick) = rx.try_pop() {
100                assert_eq!(tick.seq, expected, "ticks arrive in feed order");
101                expected += 1;
102            }
103        }
104        expected
105    });
106    feed.join().unwrap();
107    let received = strategy.join().unwrap();
108    println!("  streamed {received} ticks in order, zero loss when drained");
109    assert_eq!(received, n);
110}
111
112/// `bulk` feature: a feed handler often lifts a whole batch of ticks off one
113/// NIC receive. `try_enqueue_bulk` copies the run in behind a single release
114/// fence instead of one per tick; the strategy drains behind one acquire fence.
115#[cfg(feature = "bulk")]
116fn bulk_batch_ingest() {
117    println!("\n== bulk: batch a NIC receive into the ring ==");
118    let (mut tx, mut rx) = SpscRingBuffer::with_capacity::<Tick>(16);
119    let batch: Vec<Tick> = (0..10)
120        .map(|seq| Tick {
121            seq,
122            price_cents: 20_000 + seq as u32,
123        })
124        .collect();
125
126    let pushed = tx.try_enqueue_bulk(&batch);
127    println!(
128        "  offered {} ticks, took {pushed} in one fenced call",
129        batch.len()
130    );
131    assert_eq!(pushed, 10);
132
133    let mut out = [Tick {
134        seq: 0,
135        price_cents: 0,
136    }; 10];
137    let drained = rx.try_dequeue_bulk(&mut out);
138    println!("  drained {drained} in one fenced call");
139    assert_eq!(drained, 10);
140    assert_eq!(out.as_slice(), batch.as_slice(), "bulk preserves order");
141}
142
143/// `wait-strategies` feature: wrap the non-blocking ends in a blocking handle
144/// with a backoff policy. `YieldStrategy` lets other threads run between
145/// retries, a sensible default when the feed and strategy share cores.
146#[cfg(feature = "wait-strategies")]
147fn blocking_handoff() {
148    use subms_spsc_ring_buffer::{BlockingSpscConsumer, BlockingSpscProducer, YieldStrategy};
149
150    println!("\n== wait-strategies: blocking handoff (yield backoff) ==");
151    let (tx, rx) = SpscRingBuffer::with_capacity::<Tick>(8);
152    let mut producer = BlockingSpscProducer::new(tx, YieldStrategy);
153    let mut consumer = BlockingSpscConsumer::new(rx, YieldStrategy);
154
155    let n = 5_000u64;
156    let feed = thread::spawn(move || {
157        for seq in 0..n {
158            producer.push(Tick {
159                seq,
160                price_cents: 30_000,
161            });
162        }
163    });
164    let strategy = thread::spawn(move || {
165        for expected in 0..n {
166            let tick = consumer.pop();
167            assert_eq!(tick.seq, expected, "blocking pop keeps feed order");
168        }
169        n
170    });
171    feed.join().unwrap();
172    let got = strategy.join().unwrap();
173    println!("  handed off {got} ticks, producer blocked on full instead of dropping");
174    assert_eq!(got, n);
175}
176
177/// `mpsc-fan-in` feature: several venue feeds, one strategy consumer. Each feed
178/// owns an independent SPSC ring (still wait-free against its own counter); the
179/// consumer round-robins so a quiet venue never starves a busy one.
180#[cfg(feature = "mpsc-fan-in")]
181fn many_venue_fan_in() {
182    use subms_spsc_ring_buffer::MpscFanIn;
183
184    println!("\n== mpsc-fan-in: three venue feeds -> one strategy ==");
185    let venues = 3usize;
186    let per_venue = 20_000u64;
187    let (mut producers, mut consumer) = MpscFanIn::with_capacity::<Tick>(venues, 256);
188
189    let mut feeds = Vec::new();
190    for venue in 0..venues {
191        let mut p = producers.remove(0);
192        feeds.push(thread::spawn(move || {
193            for seq in 0..per_venue {
194                let tick = Tick {
195                    seq,
196                    price_cents: 40_000 + venue as u32,
197                };
198                while p.try_push(tick).is_err() {
199                    std::hint::spin_loop();
200                }
201            }
202        }));
203    }
204    let total = per_venue * venues as u64;
205    let strategy = thread::spawn(move || {
206        let mut got = 0u64;
207        while got < total {
208            if consumer.try_pop().is_some() {
209                got += 1;
210            }
211        }
212        got
213    });
214    for f in feeds {
215        f.join().unwrap();
216    }
217    let got = strategy.join().unwrap();
218    println!("  {venues} feeds x {per_venue} ticks -> consumer drained {got}");
219    assert_eq!(got, total);
220}
221
222/// `mpmc-disruptor` feature: broadcast one tick stream to independent readers,
223/// a strategy and a risk monitor, each of which sees every published tick. (To
224/// have each tick handled by exactly one reader, reach for `mpsc-fan-in`.)
225#[cfg(feature = "mpmc-disruptor")]
226fn broadcast_to_strategy_and_risk() {
227    use subms_spsc_ring_buffer::MpmcDisruptor;
228
229    println!("\n== mpmc-disruptor: broadcast to strategy + risk ==");
230    let n = 8u64;
231    let (producer, mut consumers) = MpmcDisruptor::with_consumers::<Tick>(16, 2);
232    let (strategy, rest) = consumers.split_at_mut(1);
233    let strategy = &mut strategy[0];
234    let risk = &mut rest[0];
235
236    // Small and single-threaded so the tour self-verifies; the threaded
237    // broadcast path is pinned in the tests.
238    let mut published = 0u64;
239    let mut strat_seen = Vec::new();
240    let mut risk_seen = Vec::new();
241    while published < n {
242        while published < n
243            && producer
244                .try_publish(Tick {
245                    seq: published,
246                    price_cents: 50_000,
247                })
248                .is_ok()
249        {
250            published += 1;
251        }
252        while let Some(t) = strategy.try_consume() {
253            strat_seen.push(t.seq);
254        }
255        while let Some(t) = risk.try_consume() {
256            risk_seen.push(t.seq);
257        }
258    }
259    println!(
260        "  published {published}; strategy saw {}, risk saw {}",
261        strat_seen.len(),
262        risk_seen.len()
263    );
264    let expected: Vec<u64> = (0..n).collect();
265    assert_eq!(strat_seen, expected, "strategy sees every tick");
266    assert_eq!(risk_seen, expected, "risk monitor sees every tick too");
267}
268
269/// `metrics` feature: wrap a base pair to count enqueue/dequeue success + fail
270/// and track the high-water depth - operational stats for a feed, at the cost
271/// of one atomic increment per op.
272#[cfg(feature = "metrics")]
273fn instrumented_handoff() {
274    use subms_spsc_ring_buffer::InstrumentedSpsc;
275
276    println!("\n== metrics: instrumented feed handoff ==");
277    let (tx, rx) = SpscRingBuffer::with_capacity::<Tick>(4);
278    let (mut tx, mut rx, metrics) = InstrumentedSpsc::wrap(tx, rx);
279
280    // Fill to capacity, one overflow drop, then drain plus one empty poll.
281    for seq in 0..4u64 {
282        tx.try_push(Tick {
283            seq,
284            price_cents: 60_000,
285        })
286        .unwrap();
287    }
288    assert!(
289        tx.try_push(Tick {
290            seq: 99,
291            price_cents: 0
292        })
293        .is_err()
294    );
295    for _ in 0..4 {
296        rx.try_pop().unwrap();
297    }
298    assert!(rx.try_pop().is_none());
299
300    let snap = metrics.snapshot();
301    println!(
302        "  enqueued {} (dropped {}), dequeued {} (empty {}), peak depth {}",
303        snap.enqueue_success,
304        snap.enqueue_fail,
305        snap.dequeue_success,
306        snap.dequeue_fail,
307        snap.max_depth_observed
308    );
309    assert_eq!(snap.enqueue_success, 4);
310    assert_eq!(snap.enqueue_fail, 1);
311    assert_eq!(snap.dequeue_success, 4);
312    assert_eq!(snap.dequeue_fail, 1);
313    assert_eq!(snap.max_depth_observed, 4);
314}

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.