Skip to main content

BoundedMpscQueue

Struct BoundedMpscQueue 

Source
pub struct BoundedMpscQueue<T> { /* private fields */ }
Expand description

Bounded MPSC ring queue. Capacity is rounded up to the next power of two (minimum 2) so the modulo can be a bitmask.

Implementations§

Source§

impl<T> BoundedMpscQueue<T>

Source

pub fn new(capacity: usize) -> Self

New empty queue. capacity is rounded up to a power of two, minimum 2.

Examples found in repository?
examples/perf_features.rs (line 263)
195fn main() -> io::Result<()> {
196    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
197        .join("..")
198        .join(".subms")
199        .join("features")
200        .join("rust.json");
201    let existing = std::fs::read_to_string(&path).unwrap_or_default();
202    let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
203    // Stamp the box these numbers came from. The bench runs wherever it is
204    // invoked, so an unstamped manifest is indistinguishable from a fleet
205    // capture; the renderer will not publish one it cannot attribute.
206    let (source, instance) = SubMsP99Source::from_env();
207    manifest.set_p99_source(source, instance.as_deref());
208
209    // Optional, off by default: pin this thread to one core for the whole run.
210    // On a heterogeneous laptop the scheduler moves the bench between core
211    // clusters and every measurement lands in one of two clock states 1.31x
212    // apart - a spread three times wider than the deltas being classified, and
213    // large enough on its own to flip a feature between auxiliary and hot-path.
214    // Pinned, the same sweep repeats to within 1%. Left OFF by default because a
215    // fleet box isolates cores outside the process, and pinning from in here
216    // would override that placement with a core the orchestrator did not choose.
217    #[cfg(feature = "affinity")]
218    if let Some(core) = std::env::var("SUBMS_PIN").ok().and_then(|v| v.parse().ok()) {
219        let _ = subms_mpsc_queue::set_affinity(&[core]);
220    }
221
222    // Burn before the first measurement, not just before each one. Every
223    // `batched` call warms itself, but the FIRST measurement in the process pays
224    // a ramp the per-measurement warm sits inside rather than absorbs, and the
225    // sweep runs smallest-first: without this the base curve read 71800 / 45300 /
226    // 46500 ns, a 1.6x fall with size that is the process settling, not the
227    // queue.
228    {
229        let mut q = filled(CANON);
230        let start = std::time::Instant::now();
231        while (start.elapsed().as_nanos() as u64) < BURN_NANOS {
232            for i in 0..ITEMS_PER_SAMPLE {
233                q.push(i as u64);
234                black_box(q.try_pop());
235            }
236        }
237    }
238
239    // The baseline: the base queue's push + try_pop round trip. Swept as well as
240    // sampled, because whether queue depth moves the BASE op is the context
241    // every feature curve is read against.
242    let base_sweep = sweep("base/push+pop", |n| {
243        let mut q = filled(n);
244        batched(ITEMS_PER_SAMPLE, |i| {
245            q.push(i as u64);
246            black_box(q.try_pop());
247        })
248    });
249    let base_p50 = base_sweep
250        .iter()
251        .find(|(n, _)| *n == CANON)
252        .map_or(0, |(_, v)| *v);
253    eprintln!("base push+pop p50 per {ITEMS_PER_SAMPLE}-item sample: {base_p50}ns");
254
255    // ---------- bounded: fixed-capacity ring, backpressure on enqueue ----------
256    #[cfg(feature = "bounded")]
257    {
258        use subms_mpsc_queue::BoundedMpscQueue;
259        // Half full at every sweep point. Filled to a FIXED element count
260        // instead, the big rings would sit 98% empty and the enqueue would be
261        // measuring the fill fraction rather than the footprint.
262        fn ring(n: usize) -> BoundedMpscQueue<u64> {
263            let q = BoundedMpscQueue::new(n);
264            for i in 0..n / 2 {
265                let _ = q.try_enqueue(i as u64);
266            }
267            q
268        }
269        let sw = sweep("bounded/enqueue+dequeue", |n| {
270            let mut q = ring(n);
271            batched(ITEMS_PER_SAMPLE, |i| {
272                let _ = q.try_enqueue(i as u64);
273                black_box(q.try_dequeue());
274            })
275        });
276        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
277
278        let mut q = ring(CANON);
279        let mut p99 = BTreeMap::new();
280        p99.insert(
281            "enqueue".to_string(),
282            single(|st, i| {
283                st.time(|| {
284                    let _ = q.try_enqueue(i as u64);
285                });
286                black_box(q.try_dequeue());
287            }),
288        );
289        p99.insert(
290            "dequeue".to_string(),
291            single(|st, i| {
292                let _ = q.try_enqueue(i as u64);
293                st.time(|| black_box(q.try_dequeue()));
294            }),
295        );
296        // The reject path, which is the reason the feature exists. The ring is
297        // filled to capacity once, OUTSIDE the timed region; every timed call
298        // then takes the full branch and hands the value back to the caller.
299        let full: BoundedMpscQueue<u64> = BoundedMpscQueue::new(CANON);
300        while full.try_enqueue(0).is_ok() {}
301        p99.insert(
302            "enqueue_full".to_string(),
303            single(|st, i| {
304                st.time(|| {
305                    let _ = full.try_enqueue(i as u64);
306                });
307            }),
308        );
309        manifest.set_feature("bounded", cat, &p99, &reason);
310    }
311
312    // ---------- mpmc: bounded ring, sequence CAS on both ends ----------
313    #[cfg(feature = "mpmc")]
314    {
315        use subms_mpsc_queue::MpmcQueue;
316        fn ring(n: usize) -> MpmcQueue<u64> {
317            let q = MpmcQueue::new(n);
318            for i in 0..n / 2 {
319                let _ = q.try_enqueue(i as u64);
320            }
321            q
322        }
323        // Uncontended, so every CAS succeeds first try. That is the figure the
324        // category is about: what the multi-consumer claim costs a queue that is
325        // NOT contended, which is the state a well-sized pipeline runs in.
326        let sw = sweep("mpmc/enqueue+dequeue", |n| {
327            let q = ring(n);
328            batched(ITEMS_PER_SAMPLE, |i| {
329                let _ = q.try_enqueue(i as u64);
330                black_box(q.try_dequeue());
331            })
332        });
333        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
334
335        let q = ring(CANON);
336        let mut p99 = BTreeMap::new();
337        p99.insert(
338            "enqueue".to_string(),
339            single(|st, i| {
340                st.time(|| {
341                    let _ = q.try_enqueue(i as u64);
342                });
343                black_box(q.try_dequeue());
344            }),
345        );
346        p99.insert(
347            "dequeue".to_string(),
348            single(|st, i| {
349                let _ = q.try_enqueue(i as u64);
350                st.time(|| black_box(q.try_dequeue()));
351            }),
352        );
353        manifest.set_feature("mpmc", cat, &p99, &reason);
354    }
355
356    // ---------- batch: drain up to BATCH items behind one acquire fence ----------
357    #[cfg(feature = "batch")]
358    {
359        use subms_mpsc_queue::BatchMpscQueue;
360        fn filled_batch(n: usize) -> BatchMpscQueue<u64> {
361            let q = BatchMpscQueue::new();
362            for i in 0..n {
363                q.push(i as u64);
364            }
365            q
366        }
367        // A sample moves ITEMS_PER_SAMPLE items either way; only the call width
368        // differs. That is why the reps count is divided rather than the batch
369        // grown - growing it would sweep the batch size, and the number would
370        // stop being comparable to the base round trip.
371        let sw = sweep("batch/push+dequeue_batch", |n| {
372            let mut q = filled_batch(n);
373            let mut buf: Vec<Option<u64>> = (0..BATCH).map(|_| None).collect();
374            batched(ITEMS_PER_SAMPLE / BATCH, |i| {
375                for j in 0..BATCH {
376                    q.push((i + j) as u64);
377                }
378                black_box(q.try_dequeue_batch(&mut buf));
379            })
380        });
381        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
382
383        let mut q = filled_batch(CANON);
384        let mut buf: Vec<Option<u64>> = (0..BATCH).map(|_| None).collect();
385        let mut p99 = BTreeMap::new();
386        // The refill is outside the timed region: timing it would put a BATCH of
387        // pushes inside the drain's number and the stage would stop being a
388        // drain figure at all.
389        p99.insert(
390            "dequeue_batch".to_string(),
391            single(|st, i| {
392                st.time(|| black_box(q.try_dequeue_batch(&mut buf)));
393                for j in 0..BATCH {
394                    q.push((i + j) as u64);
395                }
396            }),
397        );
398        p99.insert(
399            "enqueue".to_string(),
400            single(|st, i| {
401                st.time(|| q.push(i as u64));
402                let _ = q.try_dequeue_batch(&mut buf[..1]);
403            }),
404        );
405        // The producer mirror: BATCH items published behind one head swap. The
406        // drain that puts the queue back is outside the timed region for the
407        // same reason the refill is above.
408        p99.insert(
409            "enqueue_batch".to_string(),
410            single(|st, i| {
411                let base = i as u64;
412                st.time(|| black_box(q.push_batch(base..base + BATCH as u64)));
413                let _ = q.try_dequeue_batch(&mut buf);
414            }),
415        );
416        manifest.set_feature("batch", cat, &p99, &reason);
417    }
418
419    // ---------- metrics: relaxed atomic counters around each op ----------
420    #[cfg(feature = "metrics")]
421    {
422        use subms_mpsc_queue::MetricsMpscQueue;
423        fn filled_metrics(n: usize) -> MetricsMpscQueue<u64> {
424            let q = MetricsMpscQueue::new();
425            for i in 0..n {
426                q.push(i as u64);
427            }
428            q
429        }
430        let sw = sweep("metrics/push+pop", |n| {
431            let mut q = filled_metrics(n);
432            batched(ITEMS_PER_SAMPLE, |i| {
433                q.push(i as u64);
434                black_box(q.try_pop());
435            })
436        });
437        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
438
439        let mut q = filled_metrics(CANON);
440        let mut p99 = BTreeMap::new();
441        p99.insert(
442            "enqueue".to_string(),
443            single(|st, i| {
444                st.time(|| q.push(i as u64));
445                black_box(q.try_pop());
446            }),
447        );
448        p99.insert(
449            "dequeue".to_string(),
450            single(|st, i| {
451                q.push(i as u64);
452                st.time(|| black_box(q.try_pop()));
453            }),
454        );
455        p99.insert(
456            "snapshot".to_string(),
457            single(|st, _| {
458                st.time(|| black_box(q.snapshot()));
459            }),
460        );
461        manifest.set_feature("metrics", cat, &p99, &reason);
462    }
463
464    // ---------- affinity: pin the calling thread, once, at startup ----------
465    // Runs LAST because measuring it pins THIS process to core 0, and every
466    // number taken afterwards would be a number taken on one core.
467    #[cfg(feature = "affinity")]
468    {
469        use subms_mpsc_queue::set_affinity;
470        // Swept over the same axis to show what it is: a call that touches no
471        // queue state and cannot move with queue size. PINNED auxiliary rather
472        // than left to the base-delta test, which would see a syscall costing
473        // more than an enqueue and call it hot-path. It is not on the hot path at
474        // any price - `set_affinity` is called once per thread at startup and
475        // appears in neither `push` nor `try_pop`. The two ports are not even
476        // measuring the same thing: Rust issues a real `SetThreadAffinityMask` /
477        // `sched_setaffinity`, while the Java sibling validates its argument and
478        // returns UNSUPPORTED because the stock JDK has no pinning API. The
479        // per-call figure, not the sample, is the interpretable one and it is in
480        // `p99ByStage`.
481        let sw = sweep("affinity/set_affinity", |_| {
482            batched(ITEMS_PER_SAMPLE, |_| {
483                let _ = set_affinity(&[0]);
484            })
485        });
486        let (cat, reason) = classify_feature(
487            &sw,
488            Some(base_p50),
489            Some(subms::SubMsFeatureCategory::Auxiliary),
490        );
491
492        let mut p99 = BTreeMap::new();
493        p99.insert(
494            "set_affinity".to_string(),
495            single(|st, _| {
496                st.time(|| {
497                    let _ = set_affinity(&[0]);
498                });
499            }),
500        );
501        manifest.set_feature("affinity", cat, &p99, &reason);
502
503        let cores: Vec<usize> = (0..std::thread::available_parallelism()
504            .map_or(1, std::num::NonZeroUsize::get))
505            .collect();
506        let _ = set_affinity(&cores);
507    }
508
509    std::fs::create_dir_all(path.parent().unwrap())?;
510    std::fs::write(&path, manifest.to_json())?;
511    io::stdout().write_all(manifest.to_json().as_bytes())?;
512    Ok(())
513}
More examples
Hide additional examples
examples/sample_app.rs (line 203)
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}
Source

pub fn capacity(&self) -> usize

Capacity (power-of-two; possibly larger than requested).

Examples found in repository?
examples/sample_app.rs (line 204)
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}
Source

pub fn producer_index(&self) -> usize

Monotonic count of slots ever claimed by producers. Safe to read from any thread; pair it with Self::consumer_index to get lag, or sample it twice to get throughput without disturbing either end.

Examples found in repository?
examples/sample_app.rs (line 233)
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}
Source

pub fn consumer_index(&self) -> usize

Monotonic count of slots ever consumed. Safe to read from any thread.

Examples found in repository?
examples/sample_app.rs (line 234)
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}
Source

pub fn try_enqueue(&self, value: T) -> Result<(), T>

Multi-producer push. Returns Err(value) when the ring is full so the caller can retry, drop, or apply backpressure.

Examples found in repository?
examples/perf_features.rs (line 265)
195fn main() -> io::Result<()> {
196    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
197        .join("..")
198        .join(".subms")
199        .join("features")
200        .join("rust.json");
201    let existing = std::fs::read_to_string(&path).unwrap_or_default();
202    let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
203    // Stamp the box these numbers came from. The bench runs wherever it is
204    // invoked, so an unstamped manifest is indistinguishable from a fleet
205    // capture; the renderer will not publish one it cannot attribute.
206    let (source, instance) = SubMsP99Source::from_env();
207    manifest.set_p99_source(source, instance.as_deref());
208
209    // Optional, off by default: pin this thread to one core for the whole run.
210    // On a heterogeneous laptop the scheduler moves the bench between core
211    // clusters and every measurement lands in one of two clock states 1.31x
212    // apart - a spread three times wider than the deltas being classified, and
213    // large enough on its own to flip a feature between auxiliary and hot-path.
214    // Pinned, the same sweep repeats to within 1%. Left OFF by default because a
215    // fleet box isolates cores outside the process, and pinning from in here
216    // would override that placement with a core the orchestrator did not choose.
217    #[cfg(feature = "affinity")]
218    if let Some(core) = std::env::var("SUBMS_PIN").ok().and_then(|v| v.parse().ok()) {
219        let _ = subms_mpsc_queue::set_affinity(&[core]);
220    }
221
222    // Burn before the first measurement, not just before each one. Every
223    // `batched` call warms itself, but the FIRST measurement in the process pays
224    // a ramp the per-measurement warm sits inside rather than absorbs, and the
225    // sweep runs smallest-first: without this the base curve read 71800 / 45300 /
226    // 46500 ns, a 1.6x fall with size that is the process settling, not the
227    // queue.
228    {
229        let mut q = filled(CANON);
230        let start = std::time::Instant::now();
231        while (start.elapsed().as_nanos() as u64) < BURN_NANOS {
232            for i in 0..ITEMS_PER_SAMPLE {
233                q.push(i as u64);
234                black_box(q.try_pop());
235            }
236        }
237    }
238
239    // The baseline: the base queue's push + try_pop round trip. Swept as well as
240    // sampled, because whether queue depth moves the BASE op is the context
241    // every feature curve is read against.
242    let base_sweep = sweep("base/push+pop", |n| {
243        let mut q = filled(n);
244        batched(ITEMS_PER_SAMPLE, |i| {
245            q.push(i as u64);
246            black_box(q.try_pop());
247        })
248    });
249    let base_p50 = base_sweep
250        .iter()
251        .find(|(n, _)| *n == CANON)
252        .map_or(0, |(_, v)| *v);
253    eprintln!("base push+pop p50 per {ITEMS_PER_SAMPLE}-item sample: {base_p50}ns");
254
255    // ---------- bounded: fixed-capacity ring, backpressure on enqueue ----------
256    #[cfg(feature = "bounded")]
257    {
258        use subms_mpsc_queue::BoundedMpscQueue;
259        // Half full at every sweep point. Filled to a FIXED element count
260        // instead, the big rings would sit 98% empty and the enqueue would be
261        // measuring the fill fraction rather than the footprint.
262        fn ring(n: usize) -> BoundedMpscQueue<u64> {
263            let q = BoundedMpscQueue::new(n);
264            for i in 0..n / 2 {
265                let _ = q.try_enqueue(i as u64);
266            }
267            q
268        }
269        let sw = sweep("bounded/enqueue+dequeue", |n| {
270            let mut q = ring(n);
271            batched(ITEMS_PER_SAMPLE, |i| {
272                let _ = q.try_enqueue(i as u64);
273                black_box(q.try_dequeue());
274            })
275        });
276        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
277
278        let mut q = ring(CANON);
279        let mut p99 = BTreeMap::new();
280        p99.insert(
281            "enqueue".to_string(),
282            single(|st, i| {
283                st.time(|| {
284                    let _ = q.try_enqueue(i as u64);
285                });
286                black_box(q.try_dequeue());
287            }),
288        );
289        p99.insert(
290            "dequeue".to_string(),
291            single(|st, i| {
292                let _ = q.try_enqueue(i as u64);
293                st.time(|| black_box(q.try_dequeue()));
294            }),
295        );
296        // The reject path, which is the reason the feature exists. The ring is
297        // filled to capacity once, OUTSIDE the timed region; every timed call
298        // then takes the full branch and hands the value back to the caller.
299        let full: BoundedMpscQueue<u64> = BoundedMpscQueue::new(CANON);
300        while full.try_enqueue(0).is_ok() {}
301        p99.insert(
302            "enqueue_full".to_string(),
303            single(|st, i| {
304                st.time(|| {
305                    let _ = full.try_enqueue(i as u64);
306                });
307            }),
308        );
309        manifest.set_feature("bounded", cat, &p99, &reason);
310    }
311
312    // ---------- mpmc: bounded ring, sequence CAS on both ends ----------
313    #[cfg(feature = "mpmc")]
314    {
315        use subms_mpsc_queue::MpmcQueue;
316        fn ring(n: usize) -> MpmcQueue<u64> {
317            let q = MpmcQueue::new(n);
318            for i in 0..n / 2 {
319                let _ = q.try_enqueue(i as u64);
320            }
321            q
322        }
323        // Uncontended, so every CAS succeeds first try. That is the figure the
324        // category is about: what the multi-consumer claim costs a queue that is
325        // NOT contended, which is the state a well-sized pipeline runs in.
326        let sw = sweep("mpmc/enqueue+dequeue", |n| {
327            let q = ring(n);
328            batched(ITEMS_PER_SAMPLE, |i| {
329                let _ = q.try_enqueue(i as u64);
330                black_box(q.try_dequeue());
331            })
332        });
333        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
334
335        let q = ring(CANON);
336        let mut p99 = BTreeMap::new();
337        p99.insert(
338            "enqueue".to_string(),
339            single(|st, i| {
340                st.time(|| {
341                    let _ = q.try_enqueue(i as u64);
342                });
343                black_box(q.try_dequeue());
344            }),
345        );
346        p99.insert(
347            "dequeue".to_string(),
348            single(|st, i| {
349                let _ = q.try_enqueue(i as u64);
350                st.time(|| black_box(q.try_dequeue()));
351            }),
352        );
353        manifest.set_feature("mpmc", cat, &p99, &reason);
354    }
355
356    // ---------- batch: drain up to BATCH items behind one acquire fence ----------
357    #[cfg(feature = "batch")]
358    {
359        use subms_mpsc_queue::BatchMpscQueue;
360        fn filled_batch(n: usize) -> BatchMpscQueue<u64> {
361            let q = BatchMpscQueue::new();
362            for i in 0..n {
363                q.push(i as u64);
364            }
365            q
366        }
367        // A sample moves ITEMS_PER_SAMPLE items either way; only the call width
368        // differs. That is why the reps count is divided rather than the batch
369        // grown - growing it would sweep the batch size, and the number would
370        // stop being comparable to the base round trip.
371        let sw = sweep("batch/push+dequeue_batch", |n| {
372            let mut q = filled_batch(n);
373            let mut buf: Vec<Option<u64>> = (0..BATCH).map(|_| None).collect();
374            batched(ITEMS_PER_SAMPLE / BATCH, |i| {
375                for j in 0..BATCH {
376                    q.push((i + j) as u64);
377                }
378                black_box(q.try_dequeue_batch(&mut buf));
379            })
380        });
381        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
382
383        let mut q = filled_batch(CANON);
384        let mut buf: Vec<Option<u64>> = (0..BATCH).map(|_| None).collect();
385        let mut p99 = BTreeMap::new();
386        // The refill is outside the timed region: timing it would put a BATCH of
387        // pushes inside the drain's number and the stage would stop being a
388        // drain figure at all.
389        p99.insert(
390            "dequeue_batch".to_string(),
391            single(|st, i| {
392                st.time(|| black_box(q.try_dequeue_batch(&mut buf)));
393                for j in 0..BATCH {
394                    q.push((i + j) as u64);
395                }
396            }),
397        );
398        p99.insert(
399            "enqueue".to_string(),
400            single(|st, i| {
401                st.time(|| q.push(i as u64));
402                let _ = q.try_dequeue_batch(&mut buf[..1]);
403            }),
404        );
405        // The producer mirror: BATCH items published behind one head swap. The
406        // drain that puts the queue back is outside the timed region for the
407        // same reason the refill is above.
408        p99.insert(
409            "enqueue_batch".to_string(),
410            single(|st, i| {
411                let base = i as u64;
412                st.time(|| black_box(q.push_batch(base..base + BATCH as u64)));
413                let _ = q.try_dequeue_batch(&mut buf);
414            }),
415        );
416        manifest.set_feature("batch", cat, &p99, &reason);
417    }
418
419    // ---------- metrics: relaxed atomic counters around each op ----------
420    #[cfg(feature = "metrics")]
421    {
422        use subms_mpsc_queue::MetricsMpscQueue;
423        fn filled_metrics(n: usize) -> MetricsMpscQueue<u64> {
424            let q = MetricsMpscQueue::new();
425            for i in 0..n {
426                q.push(i as u64);
427            }
428            q
429        }
430        let sw = sweep("metrics/push+pop", |n| {
431            let mut q = filled_metrics(n);
432            batched(ITEMS_PER_SAMPLE, |i| {
433                q.push(i as u64);
434                black_box(q.try_pop());
435            })
436        });
437        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
438
439        let mut q = filled_metrics(CANON);
440        let mut p99 = BTreeMap::new();
441        p99.insert(
442            "enqueue".to_string(),
443            single(|st, i| {
444                st.time(|| q.push(i as u64));
445                black_box(q.try_pop());
446            }),
447        );
448        p99.insert(
449            "dequeue".to_string(),
450            single(|st, i| {
451                q.push(i as u64);
452                st.time(|| black_box(q.try_pop()));
453            }),
454        );
455        p99.insert(
456            "snapshot".to_string(),
457            single(|st, _| {
458                st.time(|| black_box(q.snapshot()));
459            }),
460        );
461        manifest.set_feature("metrics", cat, &p99, &reason);
462    }
463
464    // ---------- affinity: pin the calling thread, once, at startup ----------
465    // Runs LAST because measuring it pins THIS process to core 0, and every
466    // number taken afterwards would be a number taken on one core.
467    #[cfg(feature = "affinity")]
468    {
469        use subms_mpsc_queue::set_affinity;
470        // Swept over the same axis to show what it is: a call that touches no
471        // queue state and cannot move with queue size. PINNED auxiliary rather
472        // than left to the base-delta test, which would see a syscall costing
473        // more than an enqueue and call it hot-path. It is not on the hot path at
474        // any price - `set_affinity` is called once per thread at startup and
475        // appears in neither `push` nor `try_pop`. The two ports are not even
476        // measuring the same thing: Rust issues a real `SetThreadAffinityMask` /
477        // `sched_setaffinity`, while the Java sibling validates its argument and
478        // returns UNSUPPORTED because the stock JDK has no pinning API. The
479        // per-call figure, not the sample, is the interpretable one and it is in
480        // `p99ByStage`.
481        let sw = sweep("affinity/set_affinity", |_| {
482            batched(ITEMS_PER_SAMPLE, |_| {
483                let _ = set_affinity(&[0]);
484            })
485        });
486        let (cat, reason) = classify_feature(
487            &sw,
488            Some(base_p50),
489            Some(subms::SubMsFeatureCategory::Auxiliary),
490        );
491
492        let mut p99 = BTreeMap::new();
493        p99.insert(
494            "set_affinity".to_string(),
495            single(|st, _| {
496                st.time(|| {
497                    let _ = set_affinity(&[0]);
498                });
499            }),
500        );
501        manifest.set_feature("affinity", cat, &p99, &reason);
502
503        let cores: Vec<usize> = (0..std::thread::available_parallelism()
504            .map_or(1, std::num::NonZeroUsize::get))
505            .collect();
506        let _ = set_affinity(&cores);
507    }
508
509    std::fs::create_dir_all(path.parent().unwrap())?;
510    std::fs::write(&path, manifest.to_json())?;
511    io::stdout().write_all(manifest.to_json().as_bytes())?;
512    Ok(())
513}
More examples
Hide additional examples
examples/sample_app.rs (line 209)
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}
Source

pub fn try_dequeue(&mut self) -> Option<T>

Single-consumer pop. Returns None when the ring is empty.

Examples found in repository?
examples/sample_app.rs (line 221)
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}
More examples
Hide additional examples
examples/perf_features.rs (line 273)
195fn main() -> io::Result<()> {
196    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
197        .join("..")
198        .join(".subms")
199        .join("features")
200        .join("rust.json");
201    let existing = std::fs::read_to_string(&path).unwrap_or_default();
202    let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
203    // Stamp the box these numbers came from. The bench runs wherever it is
204    // invoked, so an unstamped manifest is indistinguishable from a fleet
205    // capture; the renderer will not publish one it cannot attribute.
206    let (source, instance) = SubMsP99Source::from_env();
207    manifest.set_p99_source(source, instance.as_deref());
208
209    // Optional, off by default: pin this thread to one core for the whole run.
210    // On a heterogeneous laptop the scheduler moves the bench between core
211    // clusters and every measurement lands in one of two clock states 1.31x
212    // apart - a spread three times wider than the deltas being classified, and
213    // large enough on its own to flip a feature between auxiliary and hot-path.
214    // Pinned, the same sweep repeats to within 1%. Left OFF by default because a
215    // fleet box isolates cores outside the process, and pinning from in here
216    // would override that placement with a core the orchestrator did not choose.
217    #[cfg(feature = "affinity")]
218    if let Some(core) = std::env::var("SUBMS_PIN").ok().and_then(|v| v.parse().ok()) {
219        let _ = subms_mpsc_queue::set_affinity(&[core]);
220    }
221
222    // Burn before the first measurement, not just before each one. Every
223    // `batched` call warms itself, but the FIRST measurement in the process pays
224    // a ramp the per-measurement warm sits inside rather than absorbs, and the
225    // sweep runs smallest-first: without this the base curve read 71800 / 45300 /
226    // 46500 ns, a 1.6x fall with size that is the process settling, not the
227    // queue.
228    {
229        let mut q = filled(CANON);
230        let start = std::time::Instant::now();
231        while (start.elapsed().as_nanos() as u64) < BURN_NANOS {
232            for i in 0..ITEMS_PER_SAMPLE {
233                q.push(i as u64);
234                black_box(q.try_pop());
235            }
236        }
237    }
238
239    // The baseline: the base queue's push + try_pop round trip. Swept as well as
240    // sampled, because whether queue depth moves the BASE op is the context
241    // every feature curve is read against.
242    let base_sweep = sweep("base/push+pop", |n| {
243        let mut q = filled(n);
244        batched(ITEMS_PER_SAMPLE, |i| {
245            q.push(i as u64);
246            black_box(q.try_pop());
247        })
248    });
249    let base_p50 = base_sweep
250        .iter()
251        .find(|(n, _)| *n == CANON)
252        .map_or(0, |(_, v)| *v);
253    eprintln!("base push+pop p50 per {ITEMS_PER_SAMPLE}-item sample: {base_p50}ns");
254
255    // ---------- bounded: fixed-capacity ring, backpressure on enqueue ----------
256    #[cfg(feature = "bounded")]
257    {
258        use subms_mpsc_queue::BoundedMpscQueue;
259        // Half full at every sweep point. Filled to a FIXED element count
260        // instead, the big rings would sit 98% empty and the enqueue would be
261        // measuring the fill fraction rather than the footprint.
262        fn ring(n: usize) -> BoundedMpscQueue<u64> {
263            let q = BoundedMpscQueue::new(n);
264            for i in 0..n / 2 {
265                let _ = q.try_enqueue(i as u64);
266            }
267            q
268        }
269        let sw = sweep("bounded/enqueue+dequeue", |n| {
270            let mut q = ring(n);
271            batched(ITEMS_PER_SAMPLE, |i| {
272                let _ = q.try_enqueue(i as u64);
273                black_box(q.try_dequeue());
274            })
275        });
276        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
277
278        let mut q = ring(CANON);
279        let mut p99 = BTreeMap::new();
280        p99.insert(
281            "enqueue".to_string(),
282            single(|st, i| {
283                st.time(|| {
284                    let _ = q.try_enqueue(i as u64);
285                });
286                black_box(q.try_dequeue());
287            }),
288        );
289        p99.insert(
290            "dequeue".to_string(),
291            single(|st, i| {
292                let _ = q.try_enqueue(i as u64);
293                st.time(|| black_box(q.try_dequeue()));
294            }),
295        );
296        // The reject path, which is the reason the feature exists. The ring is
297        // filled to capacity once, OUTSIDE the timed region; every timed call
298        // then takes the full branch and hands the value back to the caller.
299        let full: BoundedMpscQueue<u64> = BoundedMpscQueue::new(CANON);
300        while full.try_enqueue(0).is_ok() {}
301        p99.insert(
302            "enqueue_full".to_string(),
303            single(|st, i| {
304                st.time(|| {
305                    let _ = full.try_enqueue(i as u64);
306                });
307            }),
308        );
309        manifest.set_feature("bounded", cat, &p99, &reason);
310    }
311
312    // ---------- mpmc: bounded ring, sequence CAS on both ends ----------
313    #[cfg(feature = "mpmc")]
314    {
315        use subms_mpsc_queue::MpmcQueue;
316        fn ring(n: usize) -> MpmcQueue<u64> {
317            let q = MpmcQueue::new(n);
318            for i in 0..n / 2 {
319                let _ = q.try_enqueue(i as u64);
320            }
321            q
322        }
323        // Uncontended, so every CAS succeeds first try. That is the figure the
324        // category is about: what the multi-consumer claim costs a queue that is
325        // NOT contended, which is the state a well-sized pipeline runs in.
326        let sw = sweep("mpmc/enqueue+dequeue", |n| {
327            let q = ring(n);
328            batched(ITEMS_PER_SAMPLE, |i| {
329                let _ = q.try_enqueue(i as u64);
330                black_box(q.try_dequeue());
331            })
332        });
333        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
334
335        let q = ring(CANON);
336        let mut p99 = BTreeMap::new();
337        p99.insert(
338            "enqueue".to_string(),
339            single(|st, i| {
340                st.time(|| {
341                    let _ = q.try_enqueue(i as u64);
342                });
343                black_box(q.try_dequeue());
344            }),
345        );
346        p99.insert(
347            "dequeue".to_string(),
348            single(|st, i| {
349                let _ = q.try_enqueue(i as u64);
350                st.time(|| black_box(q.try_dequeue()));
351            }),
352        );
353        manifest.set_feature("mpmc", cat, &p99, &reason);
354    }
355
356    // ---------- batch: drain up to BATCH items behind one acquire fence ----------
357    #[cfg(feature = "batch")]
358    {
359        use subms_mpsc_queue::BatchMpscQueue;
360        fn filled_batch(n: usize) -> BatchMpscQueue<u64> {
361            let q = BatchMpscQueue::new();
362            for i in 0..n {
363                q.push(i as u64);
364            }
365            q
366        }
367        // A sample moves ITEMS_PER_SAMPLE items either way; only the call width
368        // differs. That is why the reps count is divided rather than the batch
369        // grown - growing it would sweep the batch size, and the number would
370        // stop being comparable to the base round trip.
371        let sw = sweep("batch/push+dequeue_batch", |n| {
372            let mut q = filled_batch(n);
373            let mut buf: Vec<Option<u64>> = (0..BATCH).map(|_| None).collect();
374            batched(ITEMS_PER_SAMPLE / BATCH, |i| {
375                for j in 0..BATCH {
376                    q.push((i + j) as u64);
377                }
378                black_box(q.try_dequeue_batch(&mut buf));
379            })
380        });
381        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
382
383        let mut q = filled_batch(CANON);
384        let mut buf: Vec<Option<u64>> = (0..BATCH).map(|_| None).collect();
385        let mut p99 = BTreeMap::new();
386        // The refill is outside the timed region: timing it would put a BATCH of
387        // pushes inside the drain's number and the stage would stop being a
388        // drain figure at all.
389        p99.insert(
390            "dequeue_batch".to_string(),
391            single(|st, i| {
392                st.time(|| black_box(q.try_dequeue_batch(&mut buf)));
393                for j in 0..BATCH {
394                    q.push((i + j) as u64);
395                }
396            }),
397        );
398        p99.insert(
399            "enqueue".to_string(),
400            single(|st, i| {
401                st.time(|| q.push(i as u64));
402                let _ = q.try_dequeue_batch(&mut buf[..1]);
403            }),
404        );
405        // The producer mirror: BATCH items published behind one head swap. The
406        // drain that puts the queue back is outside the timed region for the
407        // same reason the refill is above.
408        p99.insert(
409            "enqueue_batch".to_string(),
410            single(|st, i| {
411                let base = i as u64;
412                st.time(|| black_box(q.push_batch(base..base + BATCH as u64)));
413                let _ = q.try_dequeue_batch(&mut buf);
414            }),
415        );
416        manifest.set_feature("batch", cat, &p99, &reason);
417    }
418
419    // ---------- metrics: relaxed atomic counters around each op ----------
420    #[cfg(feature = "metrics")]
421    {
422        use subms_mpsc_queue::MetricsMpscQueue;
423        fn filled_metrics(n: usize) -> MetricsMpscQueue<u64> {
424            let q = MetricsMpscQueue::new();
425            for i in 0..n {
426                q.push(i as u64);
427            }
428            q
429        }
430        let sw = sweep("metrics/push+pop", |n| {
431            let mut q = filled_metrics(n);
432            batched(ITEMS_PER_SAMPLE, |i| {
433                q.push(i as u64);
434                black_box(q.try_pop());
435            })
436        });
437        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
438
439        let mut q = filled_metrics(CANON);
440        let mut p99 = BTreeMap::new();
441        p99.insert(
442            "enqueue".to_string(),
443            single(|st, i| {
444                st.time(|| q.push(i as u64));
445                black_box(q.try_pop());
446            }),
447        );
448        p99.insert(
449            "dequeue".to_string(),
450            single(|st, i| {
451                q.push(i as u64);
452                st.time(|| black_box(q.try_pop()));
453            }),
454        );
455        p99.insert(
456            "snapshot".to_string(),
457            single(|st, _| {
458                st.time(|| black_box(q.snapshot()));
459            }),
460        );
461        manifest.set_feature("metrics", cat, &p99, &reason);
462    }
463
464    // ---------- affinity: pin the calling thread, once, at startup ----------
465    // Runs LAST because measuring it pins THIS process to core 0, and every
466    // number taken afterwards would be a number taken on one core.
467    #[cfg(feature = "affinity")]
468    {
469        use subms_mpsc_queue::set_affinity;
470        // Swept over the same axis to show what it is: a call that touches no
471        // queue state and cannot move with queue size. PINNED auxiliary rather
472        // than left to the base-delta test, which would see a syscall costing
473        // more than an enqueue and call it hot-path. It is not on the hot path at
474        // any price - `set_affinity` is called once per thread at startup and
475        // appears in neither `push` nor `try_pop`. The two ports are not even
476        // measuring the same thing: Rust issues a real `SetThreadAffinityMask` /
477        // `sched_setaffinity`, while the Java sibling validates its argument and
478        // returns UNSUPPORTED because the stock JDK has no pinning API. The
479        // per-call figure, not the sample, is the interpretable one and it is in
480        // `p99ByStage`.
481        let sw = sweep("affinity/set_affinity", |_| {
482            batched(ITEMS_PER_SAMPLE, |_| {
483                let _ = set_affinity(&[0]);
484            })
485        });
486        let (cat, reason) = classify_feature(
487            &sw,
488            Some(base_p50),
489            Some(subms::SubMsFeatureCategory::Auxiliary),
490        );
491
492        let mut p99 = BTreeMap::new();
493        p99.insert(
494            "set_affinity".to_string(),
495            single(|st, _| {
496                st.time(|| {
497                    let _ = set_affinity(&[0]);
498                });
499            }),
500        );
501        manifest.set_feature("affinity", cat, &p99, &reason);
502
503        let cores: Vec<usize> = (0..std::thread::available_parallelism()
504            .map_or(1, std::num::NonZeroUsize::get))
505            .collect();
506        let _ = set_affinity(&cores);
507    }
508
509    std::fs::create_dir_all(path.parent().unwrap())?;
510    std::fs::write(&path, manifest.to_json())?;
511    io::stdout().write_all(manifest.to_json().as_bytes())?;
512    Ok(())
513}
Source

pub fn peek(&mut self) -> Option<&T>

Borrow the next value without consuming it. None when the ring is empty. Consumer-side only.

Source

pub fn clear(&mut self) -> usize

Drop everything currently readable and return the count. Producers keep publishing throughout, so the ring is not guaranteed empty on return. Consumer-side only.

Source

pub fn len(&self) -> usize

Best-effort length. Approximate under producer contention.

Examples found in repository?
examples/sample_app.rs (line 235)
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}
Source

pub fn is_empty(&self) -> bool

Source

pub fn is_full(&self) -> bool

Best-effort fullness. A true can go stale the instant the consumer drains a slot, so branch on Self::try_enqueue instead of this when the answer decides whether a push lands.

Examples found in repository?
examples/sample_app.rs (line 217)
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}

Trait Implementations§

Source§

impl<T> Drop for BoundedMpscQueue<T>

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more
Source§

impl<T: Send> Send for BoundedMpscQueue<T>

Source§

impl<T: Send> Sync for BoundedMpscQueue<T>

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.