mod access;
mod buffer;
mod consumer;
mod desc;
mod event;
pub mod msg;
mod pool;
mod producer;
mod ring;
#[cfg(all(test, loom))]
mod concurrency;
use core::num::NonZeroU16;
pub use access::*;
pub use buffer::*;
pub use consumer::*;
pub use desc::*;
pub use event::*;
pub use pool::*;
pub use producer::*;
pub use ring::*;
use thiserror::Error;
pub trait Notifier {
fn notify(&self, stats: QueueStats);
}
#[derive(Error, Debug)]
pub enum VirtqError {
#[error("Ring error: {0}")]
RingError(RingError),
#[error("Allocation error: {0}")]
Alloc(AllocError),
#[error("Ring or pool temporarily full")]
Backpressure,
#[error("Allocation exceeds pool capacity")]
OutOfMemory,
#[error("Invalid chain received")]
BadChain,
#[error("Payload data too large: received {recv} bytes, limit {limit} bytes")]
PayloadTooLarge { recv: usize, limit: usize },
#[error("Reply data too large for allocated buffer")]
ReplyTooLarge,
#[error("Internal state error")]
InvalidState,
#[error("Memory write error")]
MemoryWriteError,
#[error("Memory read error")]
MemoryReadError,
#[error("No payload segment in this chain")]
NoPayloadSegment,
}
impl VirtqError {
#[inline(always)]
pub fn is_transient(&self) -> bool {
matches!(self, Self::Backpressure)
}
}
impl From<RingError> for VirtqError {
fn from(e: RingError) -> Self {
match e {
RingError::WouldBlock => Self::Backpressure,
other => Self::RingError(other),
}
}
}
impl From<AllocError> for VirtqError {
fn from(e: AllocError) -> Self {
match e {
AllocError::NoSpace => Self::Backpressure,
AllocError::OutOfMemory => Self::OutOfMemory,
other => Self::Alloc(other),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Layout {
desc_table_addr: u64,
desc_table_len: u16,
drv_evt_addr: u64,
dev_evt_addr: u64,
}
#[inline]
const fn align_up(val: usize, align: usize) -> usize {
val.next_multiple_of(align)
}
impl Layout {
pub const unsafe fn from_base(base: u64, num_descs: NonZeroU16) -> Result<Self, RingError> {
let num_descs = num_descs.get() as usize;
if !num_descs.is_power_of_two() {
return Err(RingError::InvalidLayout);
}
if !base.is_multiple_of(Descriptor::ALIGN as u64) {
return Err(RingError::InvalidLayout);
}
if base
.checked_add(Layout::query_size(num_descs) as u64)
.is_none()
{
return Err(RingError::InvalidLayout);
}
let desc_size = num_descs * Descriptor::SIZE;
let event_size = EventSuppression::SIZE;
let event_align = EventSuppression::ALIGN;
let drv_evt_offset = align_up(desc_size, event_align);
let dev_evt_offset = align_up(drv_evt_offset + event_size, event_align);
Ok(Self {
desc_table_addr: base,
desc_table_len: num_descs as u16,
drv_evt_addr: base + drv_evt_offset as u64,
dev_evt_addr: base + dev_evt_offset as u64,
})
}
pub const fn desc_table_addr(&self) -> u64 {
self.desc_table_addr
}
pub const fn desc_table_len(&self) -> u16 {
self.desc_table_len
}
pub const fn drv_evt_addr(&self) -> u64 {
self.drv_evt_addr
}
pub const fn dev_evt_addr(&self) -> u64 {
self.dev_evt_addr
}
pub const fn query_size(num_descs: usize) -> usize {
let desc_size = num_descs * Descriptor::SIZE;
let event_size = EventSuppression::SIZE;
let event_align = EventSuppression::ALIGN;
let drv_evt_offset = align_up(desc_size, event_align);
let dev_evt_offset = align_up(drv_evt_offset + event_size, event_align);
dev_evt_offset + event_size
}
}
#[derive(Debug, Clone, Copy)]
pub struct QueueStats {
pub num_free: usize,
pub num_inflight: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SuppressionKind {
Enable,
Disable,
Descriptor(RingCursor),
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Token {
pub seq: u32,
pub id: u16,
}
impl From<BufferElement> for Allocation {
fn from(value: BufferElement) -> Self {
Allocation {
addr: value.addr,
len: value.len as usize,
}
}
}
const _: () = {
#[allow(clippy::panic)]
#[allow(clippy::unwrap_used)]
const fn verify_layout(num_descs: usize) {
let base = 0x1000u64;
let layout =
match unsafe { Layout::from_base(base, NonZeroU16::new(num_descs as u16).unwrap()) } {
Ok(l) => l,
Err(_) => panic!("from_base failed"),
};
let expected_size = Layout::query_size(num_descs);
assert!(layout.desc_table_addr() == base);
assert!(layout.desc_table_len() as usize == num_descs);
assert!(
layout
.drv_evt_addr()
.is_multiple_of(EventSuppression::ALIGN as u64)
);
assert!(
layout
.dev_evt_addr()
.is_multiple_of(EventSuppression::ALIGN as u64)
);
let desc_end = base + (num_descs * Descriptor::SIZE) as u64;
assert!(layout.drv_evt_addr() >= desc_end);
assert!(layout.dev_evt_addr() >= layout.drv_evt_addr() + EventSuppression::SIZE as u64);
let layout_end = layout.dev_evt_addr() + EventSuppression::SIZE as u64;
assert!(base + expected_size as u64 == layout_end);
}
unsafe {
assert!(Layout::from_base(u64::MAX, NonZeroU16::new(1).unwrap()).is_err());
}
verify_layout(1);
verify_layout(2);
verify_layout(4);
verify_layout(8);
verify_layout(16);
verify_layout(32);
verify_layout(64);
verify_layout(128);
verify_layout(256);
verify_layout(512);
verify_layout(1024);
};
#[cfg(test)]
pub(crate) mod test_utils {
use alloc::collections::BTreeMap;
use alloc::sync::Arc;
use core::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Mutex;
use super::*;
use crate::virtq::ring::tests::{OwnedRing, TestMem};
#[derive(Debug, Clone)]
pub(crate) struct TestNotifier {
pub(crate) count: Arc<AtomicUsize>,
}
impl TestNotifier {
pub(crate) fn new() -> Self {
Self {
count: Arc::new(AtomicUsize::new(0)),
}
}
pub(crate) fn notification_count(&self) -> usize {
self.count.load(Ordering::Relaxed)
}
}
impl Notifier for TestNotifier {
fn notify(&self, _stats: QueueStats) {
self.count.fetch_add(1, Ordering::Relaxed);
}
}
#[derive(Clone)]
pub(crate) struct TestPool {
base: u64,
next: Arc<AtomicU64>,
size: usize,
max_alloc_len: usize,
allocations: Arc<Mutex<BTreeMap<u64, usize>>>,
}
impl TestPool {
pub(crate) fn new(base: u64, size: usize) -> Self {
Self {
base,
next: Arc::new(AtomicU64::new(base)),
size,
max_alloc_len: usize::MAX,
allocations: Arc::new(Mutex::new(BTreeMap::new())),
}
}
pub(crate) fn new_with_max_alloc_len(base: u64, size: usize, max_alloc_len: usize) -> Self {
Self {
base,
next: Arc::new(AtomicU64::new(base)),
size,
max_alloc_len,
allocations: Arc::new(Mutex::new(BTreeMap::new())),
}
}
}
impl BufferProvider for TestPool {
fn max_alloc_len(&self) -> usize {
self.max_alloc_len
}
fn alloc(&self, len: usize) -> Result<Allocation, AllocError> {
if len == 0 {
return Err(AllocError::InvalidArg);
}
let addr = self.next.fetch_add(len as u64, Ordering::Relaxed);
let end = addr + len as u64;
if end > self.base + self.size as u64 {
return Err(AllocError::NoSpace);
}
self.allocations
.lock()
.expect("poisoned mutex")
.insert(addr, len);
Ok(Allocation { addr, len })
}
fn dealloc(&self, addr: u64) -> Result<(), AllocError> {
self.allocations
.lock()
.expect("poisoned mutex")
.remove(&addr)
.map(|_| ())
.ok_or(AllocError::InvalidFree(addr, 0))
}
}
type TestProducer = VirtqProducer<TestMem, TestNotifier, TestPool>;
type TestConsumer = VirtqConsumer<TestMem, TestNotifier>;
pub(crate) fn make_test_producer(
ring: &OwnedRing,
) -> (TestProducer, TestConsumer, TestNotifier) {
let layout = ring.layout();
let mem = ring.mem();
let pool_base = mem.base_addr() + Layout::query_size(ring.len()) as u64 + 0x100;
let pool = TestPool::new(pool_base, 0x8000);
let notifier = TestNotifier::new();
let producer = VirtqProducer::new(layout, mem.clone(), notifier.clone(), pool);
let consumer = VirtqConsumer::new(layout, mem, notifier.clone());
(producer, consumer, notifier)
}
}
#[cfg(test)]
mod tests {
use alloc::sync::Arc;
use core::sync::atomic::{AtomicUsize, Ordering};
use super::*;
use crate::virtq::ring::tests::{TestMem, make_ring};
use crate::virtq::test_utils::*;
fn send_readwrite(
producer: &mut VirtqProducer<TestMem, TestNotifier, TestPool>,
entry_data: &[u8],
used_cap: usize,
) -> Token {
let mut se = producer
.chain()
.readable(entry_data.len())
.writable(used_cap)
.build()
.unwrap();
se.write_all(entry_data).unwrap();
producer.submit(se).unwrap()
}
fn poll_received(
consumer: &mut VirtqConsumer<TestMem, TestNotifier>,
) -> (RecvChain, ReplyChain<TestMem>) {
consumer.poll(1024).unwrap().unwrap()
}
#[test]
fn test_submit_notifies() {
let ring = make_ring(16);
let (mut producer, mut consumer, notifier) = make_test_producer(&ring);
let initial_count = notifier.notification_count();
let token = send_readwrite(&mut producer, b"hello", 64);
assert!(notifier.notification_count() > initial_count);
let (recv, _reply) = poll_received(&mut consumer);
assert_eq!(recv.token(), token);
}
#[test]
fn test_multiple_submits() {
let ring = make_ring(16);
let (mut producer, mut consumer, _notifier) = make_test_producer(&ring);
let tok1 = send_readwrite(&mut producer, b"request1", 64);
let tok2 = send_readwrite(&mut producer, b"request2", 64);
let tok3 = send_readwrite(&mut producer, b"request3", 64);
for _ in 0..3 {
let (_recv, reply) = poll_received(&mut consumer);
consumer.complete(reply).unwrap();
}
let used1 = producer.poll().unwrap().unwrap();
let used2 = producer.poll().unwrap().unwrap();
let used3 = producer.poll().unwrap().unwrap();
assert!(
[used1.token(), used2.token(), used3.token()].contains(&tok1)
&& [used1.token(), used2.token(), used3.token()].contains(&tok2)
&& [used1.token(), used2.token(), used3.token()].contains(&tok3)
);
}
#[test]
fn test_completion_batching_with_suppression() {
let ring = make_ring(16);
let (mut producer, mut consumer, _notifier) = make_test_producer(&ring);
let tok1 = send_readwrite(&mut producer, b"req1", 64);
let tok2 = send_readwrite(&mut producer, b"req2", 64);
let tok3 = send_readwrite(&mut producer, b"req3", 64);
let cursor = producer.used_cursor();
producer
.set_used_suppression(SuppressionKind::Descriptor(cursor))
.unwrap();
for _ in 0..3 {
let (_recv, reply) = poll_received(&mut consumer);
let ReplyChain::Writable(mut wc) = reply else {
panic!("expected writable reply");
};
wc.write_all(b"used-data").unwrap();
consumer.complete(wc).unwrap();
}
let mut responses = Vec::new();
producer
.drain(|reply| {
responses.push(reply.token());
})
.unwrap();
assert_eq!(responses.len(), 3);
assert!(responses.contains(&tok1));
assert!(responses.contains(&tok2));
assert!(responses.contains(&tok3));
}
#[test]
fn test_notifier_receives_context() {
#[derive(Debug, Clone)]
struct CtxNotifier {
last_num_free: Arc<AtomicUsize>,
last_num_inflight: Arc<AtomicUsize>,
count: Arc<AtomicUsize>,
}
impl Notifier for CtxNotifier {
fn notify(&self, stats: QueueStats) {
self.last_num_free.store(stats.num_free, Ordering::Relaxed);
self.last_num_inflight
.store(stats.num_inflight, Ordering::Relaxed);
self.count.fetch_add(1, Ordering::Relaxed);
}
}
let ring = make_ring(16);
let layout = ring.layout();
let mem = ring.mem();
let pool_base = mem.base_addr() + Layout::query_size(ring.len()) as u64 + 0x100;
let pool = TestPool::new(pool_base, 0x8000);
let notifier = CtxNotifier {
last_num_free: Arc::new(AtomicUsize::new(0)),
last_num_inflight: Arc::new(AtomicUsize::new(0)),
count: Arc::new(AtomicUsize::new(0)),
};
let mut producer = VirtqProducer::new(layout, mem, notifier.clone(), pool);
let mut se = producer.chain().readable(4).writable(32).build().unwrap();
se.write_all(b"test").unwrap();
producer.submit(se).unwrap();
assert_eq!(notifier.count.load(Ordering::Relaxed), 1);
assert!(notifier.last_num_inflight.load(Ordering::Relaxed) > 0);
}
#[test]
fn test_chain_batch() {
let ring = make_ring(16);
let (mut producer, mut consumer, notifier) = make_test_producer(&ring);
let initial_count = notifier.notification_count();
let mut se1 = producer.chain().readable(64).writable(128).build().unwrap();
se1.write_all(b"first-ent").unwrap();
let _tok1 = producer.submit(se1).unwrap();
let mut se2 = producer.chain().readable(64).writable(64).build().unwrap();
se2.write_all(b"copy-ent").unwrap();
let _tok2 = producer.submit(se2).unwrap();
let se3 = producer.chain().writable(32).build().unwrap();
let tok3 = producer.submit(se3).unwrap();
assert!(notifier.notification_count() > initial_count);
let (recv1, reply1) = poll_received(&mut consumer);
assert_eq!(recv1.to_bytes().as_ref(), b"first-ent");
consumer.complete(reply1).unwrap();
let (recv2, reply2) = poll_received(&mut consumer);
assert_eq!(recv2.to_bytes().as_ref(), b"copy-ent");
consumer.complete(reply2).unwrap();
let (_recv3, reply3) = poll_received(&mut consumer);
let ReplyChain::Writable(mut wc) = reply3 else {
panic!("expected writable reply");
};
wc.write_all(b"resp").unwrap();
consumer.complete(wc).unwrap();
let _ = producer.poll().unwrap().unwrap();
let _ = producer.poll().unwrap().unwrap();
let used = producer.poll().unwrap().unwrap();
assert_eq!(used.token(), tok3);
assert_eq!(used.to_bytes().unwrap().as_ref(), b"resp");
}
#[test]
fn test_chain_write_send() {
let ring = make_ring(16);
let (mut producer, mut consumer, _notifier) = make_test_producer(&ring);
let mut se = producer.chain().readable(64).writable(128).build().unwrap();
se.write_all(b"hello").unwrap();
let token = producer.submit(se).unwrap();
let (recv, reply) = poll_received(&mut consumer);
assert_eq!(recv.token(), token);
assert_eq!(recv.to_bytes().as_ref(), b"hello");
let ReplyChain::Writable(mut wc) = reply else {
panic!("expected writable reply");
};
wc.write_all(b"world").unwrap();
consumer.complete(wc).unwrap();
let used = producer.poll().unwrap().unwrap();
assert_eq!(used.to_bytes().unwrap().as_ref(), b"world");
}
#[test]
fn test_full_round_trip() {
let ring = make_ring(16);
let (mut producer, mut consumer, _notifier) = make_test_producer(&ring);
let token = send_readwrite(&mut producer, b"round-trip-recv", 128);
let (recv, reply) = poll_received(&mut consumer);
assert_eq!(recv.token(), token);
assert_eq!(recv.to_bytes().as_ref(), b"round-trip-recv");
let ReplyChain::Writable(mut wc) = reply else {
panic!("expected writable reply");
};
assert!(wc.capacity() >= 128);
wc.write_all(b"round-trip-rsp").unwrap();
consumer.complete(wc).unwrap();
let used = producer.poll().unwrap().unwrap();
assert_eq!(used.token(), token);
assert_eq!(used.to_bytes().unwrap().as_ref(), b"round-trip-rsp");
}
#[test]
fn test_cancel_submits_zero_length() {
let ring = make_ring(16);
let (mut producer, mut consumer, _notifier) = make_test_producer(&ring);
let token = send_readwrite(&mut producer, b"recv-data", 64);
let (_recv, reply) = poll_received(&mut consumer);
consumer.complete(reply).unwrap();
let used = producer.poll().unwrap().unwrap();
assert_eq!(used.token(), token);
assert_eq!(used.to_bytes().unwrap().len(), 0);
assert!(used.to_bytes().unwrap().is_empty());
}
#[test]
fn test_hold_reply_and_complete() {
let ring = make_ring(16);
let (mut producer, mut consumer, _notifier) = make_test_producer(&ring);
let token = send_readwrite(&mut producer, b"deferred", 64);
let (recv, reply) = poll_received(&mut consumer);
assert_eq!(recv.token(), token);
assert_eq!(recv.to_bytes().as_ref(), b"deferred");
let ReplyChain::Writable(mut wc) = reply else {
panic!("expected writable reply");
};
wc.write_all(b"deferred-used").unwrap();
consumer.complete(wc).unwrap();
let used = producer.poll().unwrap().unwrap();
assert_eq!(used.token(), token);
assert_eq!(used.to_bytes().unwrap().as_ref(), b"deferred-used");
}
#[test]
fn test_concurrent_pending_replies() {
let ring = make_ring(16);
let (mut producer, mut consumer, _notifier) = make_test_producer(&ring);
let tok1 = send_readwrite(&mut producer, b"first", 64);
let tok2 = send_readwrite(&mut producer, b"second", 64);
let (recv1, reply1) = poll_received(&mut consumer);
assert_eq!(recv1.token(), tok1);
assert_eq!(recv1.to_bytes().as_ref(), b"first");
let (recv2, reply2) = poll_received(&mut consumer);
assert_eq!(recv2.token(), tok2);
assert_eq!(recv2.to_bytes().as_ref(), b"second");
let ReplyChain::Writable(mut wc2) = reply2 else {
panic!("expected writable");
};
wc2.write_all(b"resp2").unwrap();
consumer.complete(wc2).unwrap();
let ReplyChain::Writable(mut wc1) = reply1 else {
panic!("expected writable");
};
wc1.write_all(b"resp1").unwrap();
consumer.complete(wc1).unwrap();
let used1 = producer.poll().unwrap().unwrap();
let used2 = producer.poll().unwrap().unwrap();
let mut responses: Vec<_> = vec![
(used1.token(), used1.to_bytes().unwrap().to_vec()),
(used2.token(), used2.to_bytes().unwrap().to_vec()),
];
responses.sort_by_key(|(t, _)| t.seq);
let expected_first = responses.iter().find(|(t, _)| *t == tok1).unwrap();
let expected_second = responses.iter().find(|(t, _)| *t == tok2).unwrap();
assert_eq!(&expected_first.1[..], b"resp1");
assert_eq!(&expected_second.1[..], b"resp2");
}
fn send_readonly(
producer: &mut VirtqProducer<TestMem, TestNotifier, TestPool>,
entry_data: &[u8],
) -> Token {
let mut se = producer.chain().readable(entry_data.len()).build().unwrap();
se.write_all(entry_data).unwrap();
producer.submit(se).unwrap()
}
#[test]
fn test_reclaim_frees_ring_slots() {
let ring = make_ring(4);
let (mut producer, mut consumer, _) = make_test_producer(&ring);
send_readonly(&mut producer, b"a");
send_readonly(&mut producer, b"b");
send_readonly(&mut producer, b"c");
send_readonly(&mut producer, b"d");
let mut se = producer.chain().readable(1).build().unwrap();
se.write_all(b"e").unwrap();
let res = producer.submit(se);
assert!(
matches!(res, Err(VirtqError::Backpressure)),
"expected Backpressure from full ring"
);
while let Some(result) = consumer.poll(1024).unwrap() {
let (_, reply) = result;
consumer.complete(reply).unwrap();
}
let count = producer.reclaim().unwrap();
assert_eq!(count, 4, "expected 4 reclaimed entries");
send_readonly(&mut producer, b"e");
}
#[test]
fn test_reclaim_buffers_rw_completions() {
let ring = make_ring(4);
let (mut producer, mut consumer, _) = make_test_producer(&ring);
let tok = send_readwrite(&mut producer, b"request", 64);
let (_, reply) = poll_received(&mut consumer);
let ReplyChain::Writable(mut wc) = reply else {
panic!("expected writable");
};
wc.write_all(b"response-data").unwrap();
consumer.complete(wc).unwrap();
let count = producer.reclaim().unwrap();
assert_eq!(count, 1);
let used = producer.poll().unwrap().unwrap();
assert_eq!(used.token(), tok);
assert_eq!(used.to_bytes().unwrap().as_ref(), b"response-data");
}
#[test]
fn test_reclaim_discards_readonly_completions() {
let ring = make_ring(8);
let (mut producer, mut consumer, _) = make_test_producer(&ring);
let _tok_ro1 = send_readonly(&mut producer, b"log1");
let tok_rw = send_readwrite(&mut producer, b"call", 64);
let _tok_ro2 = send_readonly(&mut producer, b"log2");
let (_, reply1) = poll_received(&mut consumer);
consumer.complete(reply1).unwrap();
let (_, reply2) = poll_received(&mut consumer);
let ReplyChain::Writable(mut wc) = reply2 else {
panic!("expected writable");
};
wc.write_all(b"result").unwrap();
consumer.complete(wc).unwrap();
let (_, reply3) = poll_received(&mut consumer);
consumer.complete(reply3).unwrap();
let count = producer.reclaim().unwrap();
assert_eq!(count, 3);
let used = producer.poll().unwrap().unwrap();
assert_eq!(used.token(), tok_rw);
assert_eq!(used.to_bytes().unwrap().as_ref(), b"result");
assert!(producer.poll().unwrap().is_none());
}
#[test]
fn test_reclaim_mixed_with_poll() {
let ring = make_ring(8);
let (mut producer, mut consumer, _) = make_test_producer(&ring);
send_readonly(&mut producer, b"x");
let tok_rw = send_readwrite(&mut producer, b"y", 64);
let (_, reply1) = poll_received(&mut consumer);
consumer.complete(reply1).unwrap();
let (_, reply2) = poll_received(&mut consumer);
let ReplyChain::Writable(mut wc) = reply2 else {
panic!("expected writable");
};
wc.write_all(b"reply").unwrap();
consumer.complete(wc).unwrap();
let used1 = producer.poll().unwrap().unwrap();
assert!(matches!(used1, UsedChain::Ack(_)));
let count = producer.reclaim().unwrap();
assert_eq!(count, 1);
let used2 = producer.poll().unwrap().unwrap();
assert_eq!(used2.token(), tok_rw);
assert_eq!(used2.to_bytes().unwrap().as_ref(), b"reply");
}
#[test]
fn test_reclaim_submit_no_token_collision() {
let ring = make_ring(8);
let (mut producer, mut consumer, _) = make_test_producer(&ring);
let tok_old = send_readonly(&mut producer, b"log");
let (_, reply) = poll_received(&mut consumer);
consumer.complete(reply).unwrap();
let count = producer.reclaim().unwrap();
assert_eq!(count, 1);
let tok_new = send_readwrite(&mut producer, b"call", 64);
assert_ne!(
tok_old, tok_new,
"tokens must be unique across reclaim/submit cycles"
);
let (_, reply) = poll_received(&mut consumer);
let ReplyChain::Writable(mut wc) = reply else {
panic!("expected writable");
};
wc.write_all(b"result").unwrap();
consumer.complete(wc).unwrap();
let used = producer.poll().unwrap().unwrap();
assert_eq!(used.token(), tok_new);
assert_eq!(used.to_bytes().unwrap().as_ref(), b"result");
assert!(producer.poll().unwrap().is_none());
}
#[test]
fn test_reclaim_readonly_does_not_leak_pending() {
let ring = make_ring(4);
let (mut producer, mut consumer, _) = make_test_producer(&ring);
for _ in 0..10 {
for _ in 0..4 {
send_readonly(&mut producer, b"msg");
}
while let Some(result) = consumer.poll(1024).unwrap() {
let (_, reply) = result;
consumer.complete(reply).unwrap();
}
let count = producer.reclaim().unwrap();
assert_eq!(count, 4);
assert!(
producer.poll().unwrap().is_none(),
"pending should be empty after reclaiming RO entries"
);
}
}
}