use super::{
pool::Pool,
task::{Header, Task, UnsafeCell},
};
use commonware_utils::GOLDEN_RATIO;
use crossbeam_utils::CachePadded;
use std::{
future::Future,
num::NonZeroU64,
ptr::NonNull,
sync::{Weak, atomic::AtomicU64},
thread,
};
cfg_if::cfg_if! {
if #[cfg(feature = "loom")] {
use loom::sync::{Mutex, MutexGuard, atomic::{AtomicBool, Ordering}};
} else {
use commonware_utils::sync::{Mutex, MutexGuard};
use std::sync::atomic::{AtomicBool, Ordering};
}
}
const SHARDS_PER_WORKER: usize = 4;
#[derive(Default)]
pub struct Links {
prev: UnsafeCell<Option<NonNull<Header>>>,
next: UnsafeCell<Option<NonNull<Header>>>,
}
struct List {
head: Option<NonNull<Header>>,
}
unsafe impl Send for List {}
impl List {
unsafe fn contains(&self, node: NonNull<Header>) -> bool {
let prev = unsafe { Header::links(node).as_ref().prev.with(|prev| *prev) };
prev.is_some() || self.head == Some(node)
}
unsafe fn push_front(&mut self, node: NonNull<Header>) {
unsafe {
let links = Header::links(node).as_ref();
links.prev.with_mut(|prev| *prev = None);
links.next.with_mut(|next| *next = self.head);
if let Some(head) = self.head {
Header::links(head)
.as_ref()
.prev
.with_mut(|prev| *prev = Some(node));
}
}
self.head = Some(node);
}
unsafe fn remove(&mut self, node: NonNull<Header>) -> bool {
if !unsafe { self.contains(node) } {
return false;
}
unsafe { self.unlink(node) };
true
}
unsafe fn unlink(&mut self, node: NonNull<Header>) {
unsafe {
let links = Header::links(node).as_ref();
let prev = links.prev.with(|prev| *prev);
let next = links.next.with(|next| *next);
match prev {
Some(prev) => Header::links(prev)
.as_ref()
.next
.with_mut(|slot| *slot = next),
None => self.head = next,
}
if let Some(next) = next {
Header::links(next)
.as_ref()
.prev
.with_mut(|slot| *slot = prev);
}
links.prev.with_mut(|prev| *prev = None);
links.next.with_mut(|next| *next = None);
}
}
fn pop_front(&mut self) -> Option<NonNull<Header>> {
let head = self.head?;
unsafe { self.unlink(head) };
Some(head)
}
}
pub struct Tasks {
shards: Box<[CachePadded<Mutex<List>>]>,
shift: u32,
closed: AtomicBool,
id: NonZeroU64,
}
impl Tasks {
pub fn new(workers: usize) -> Self {
if cfg!(feature = "loom") {
return Self::with_shards(1);
}
Self::with_shards(workers.next_power_of_two() * SHARDS_PER_WORKER)
}
fn with_shards(count: usize) -> Self {
assert!(
count.is_power_of_two(),
"shard count must be a power of two"
);
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
let id = NonZeroU64::new(NEXT_ID.fetch_add(1, Ordering::Relaxed))
.expect("task set identities exhausted");
Self {
shards: (0..count)
.map(|_| CachePadded::new(Mutex::new(List { head: None })))
.collect(),
shift: u64::BITS - count.trailing_zeros(),
closed: AtomicBool::new(false),
id,
}
}
pub const fn id(&self) -> NonZeroU64 {
self.id
}
fn index(&self, node: NonNull<Header>) -> usize {
let hash = (node.as_ptr().addr() as u64).wrapping_mul(GOLDEN_RATIO);
hash.checked_shr(self.shift).unwrap_or(0) as usize
}
fn lock(&self, index: usize) -> MutexGuard<'_, List> {
cfg_if::cfg_if! {
if #[cfg(feature = "loom")] {
let list = self.shards[index].lock().unwrap();
} else {
let list = self.shards[index].lock();
}
}
list
}
pub fn is_closed(&self) -> bool {
self.closed.load(Ordering::Acquire)
}
pub fn register<F>(&self, future: F, pool: Weak<Pool>) -> Result<(), Task>
where
F: Future<Output = ()> + Send + 'static,
{
let (task, runnable) = Task::new(future, self, pool);
if let Err(task) = self.insert(task) {
runnable.discard();
return Err(task);
}
#[cfg(test)]
tests::after_insert();
runnable.spawn();
Ok(())
}
pub fn insert(&self, task: Task) -> Result<(), Task> {
assert_eq!(task.owner(), self.id, "task belongs to another runtime");
let node = task.as_ptr();
let mut list = self.lock(self.index(node));
if self.is_closed() {
return Err(task);
}
assert!(!unsafe { list.contains(node) }, "task inserted twice");
unsafe { list.push_front(node) };
let _ = Task::into_raw(task);
Ok(())
}
pub fn remove(&self, task: &Task) -> Option<Task> {
assert_eq!(task.owner(), self.id, "task belongs to another runtime");
let node = task.as_ptr();
let mut list = self.lock(self.index(node));
if !unsafe { list.remove(node) } {
assert!(self.is_closed(), "task missing from an open set");
return None;
}
Some(unsafe { Task::from_raw(node) })
}
pub fn close(&self) {
self.closed.store(true, Ordering::Release);
}
pub fn drain(&self, worker: usize) -> Drain<'_> {
assert!(self.is_closed(), "set drained before closing");
Drain {
tasks: self,
start: worker.wrapping_mul(SHARDS_PER_WORKER) % self.shards.len(),
visited: 0,
}
}
#[cfg(test)]
pub fn teardown(&self) -> Vec<Task> {
self.close();
self.drain(0).collect()
}
#[cfg(test)]
fn try_lock(&self, index: usize) -> Option<MutexGuard<'_, List>> {
cfg_if::cfg_if! {
if #[cfg(feature = "loom")] {
self.shards[index].try_lock().ok()
} else {
self.shards[index].try_lock()
}
}
}
#[cfg(test)]
pub fn lens(&self) -> Vec<usize> {
(0..self.shards.len())
.map(|index| {
let list = self.lock(index);
let mut len = 0;
let mut node = list.head;
while let Some(current) = node {
len += 1;
node = unsafe { Header::links(current).as_ref().next.with(|next| *next) };
}
len
})
.collect()
}
#[cfg(test)]
pub fn live(&self) -> usize {
self.lens().into_iter().sum()
}
}
pub struct Drain<'a> {
tasks: &'a Tasks,
start: usize,
visited: usize,
}
impl Iterator for Drain<'_> {
type Item = Task;
fn next(&mut self) -> Option<Task> {
let shards = self.tasks.shards.len();
while self.visited < shards {
let index = (self.start + self.visited) % shards;
let node = self.tasks.lock(index).pop_front();
if let Some(node) = node {
return Some(unsafe { Task::from_raw(node) });
}
self.visited += 1;
}
None
}
}
impl Drop for Tasks {
fn drop(&mut self) {
if thread::panicking() {
return;
}
for index in 0..self.shards.len() {
assert!(
self.lock(index).head.is_none(),
"set dropped with live tasks"
);
}
}
}
#[cfg(test)]
#[commonware_macros::test_group("miri")]
pub mod tests {
use super::*;
use crate::{iouring::task::tests::refs, utils::extract_panic_message};
use std::{
cell::RefCell,
future::pending,
panic::{AssertUnwindSafe, catch_unwind},
sync::{Arc, Barrier, Weak},
thread,
};
thread_local! {
pub static AFTER_INSERT: RefCell<Option<Box<dyn FnOnce()>>> = const { RefCell::new(None) };
}
pub fn after_insert() {
if let Some(callback) = AFTER_INSERT.try_with(RefCell::take).ok().flatten() {
callback();
}
}
pub fn task(set: &Tasks) -> Task {
let (task, runnable) = Task::new(pending::<()>(), set, Weak::new());
runnable.discard();
task
}
fn finish(task: Task) {
task.clear();
drop(task);
}
#[test]
fn test_insert_then_remove_returns_the_reference_once() {
let set = Tasks::new(4);
let t = task(&set);
assert!(set.insert(t.clone()).is_ok());
assert_eq!(refs(&t), 2);
let removed = set.remove(&t).expect("linked task is removed");
assert_eq!(removed.as_ptr(), t.as_ptr());
drop(removed);
assert_eq!(refs(&t), 1);
assert_eq!(set.live(), 0);
let panic = catch_unwind(AssertUnwindSafe(|| drop(set.remove(&t)))).unwrap_err();
assert_eq!(
extract_panic_message(&*panic),
"task missing from an open set"
);
assert!(set.teardown().is_empty());
assert!(set.remove(&t).is_none());
finish(t);
}
#[test]
fn test_remove_head_middle_and_tail_of_one_shard() {
let set = Tasks::with_shards(1);
let tasks: Vec<Task> = (0..5).map(|_| task(&set)).collect();
for t in &tasks {
assert!(set.insert(t.clone()).is_ok());
}
for index in [2, 4, 0] {
assert!(set.remove(&tasks[index]).is_some());
}
assert_eq!(set.lens(), [2]);
let rest: Vec<_> = set.teardown().iter().map(Task::as_ptr).collect();
assert_eq!(rest, [tasks[3].as_ptr(), tasks[1].as_ptr()]);
for t in tasks {
assert_eq!(refs(&t), 1);
finish(t);
}
}
#[test]
fn test_close_refuses_insertion_and_drain_hands_out_everything() {
let set = Tasks::new(4);
let tasks: Vec<Task> = (0..100).map(|_| task(&set)).collect();
for t in &tasks {
assert!(set.insert(t.clone()).is_ok());
}
set.close();
let late = task(&set);
assert!(
set.insert(late.clone()).is_err(),
"a closed set refuses insertion"
);
assert_eq!(set.live(), 100);
assert_eq!(set.drain(3).count(), 100);
for t in tasks.iter().chain([&late]) {
assert!(set.remove(t).is_none(), "nothing left to remove");
assert_eq!(refs(t), 1);
}
for t in tasks {
finish(t);
}
finish(late);
}
#[test]
fn test_concurrent_drains_hand_out_each_task_once() {
let set = Arc::new(Tasks::new(4));
let tasks: Vec<Task> = (0..64).map(|_| task(&set)).collect();
for t in &tasks {
assert!(set.insert(t.clone()).is_ok());
}
set.close();
for (worker, start) in [(0, 0), (1, 4), (2, 8), (3, 12), (4, 0)] {
assert_eq!(set.drain(worker).start, start);
}
let barrier = Arc::new(Barrier::new(4));
let drains: Vec<_> = (0..4)
.map(|worker| {
let set = set.clone();
let barrier = barrier.clone();
thread::spawn(move || {
barrier.wait();
set.drain(worker)
.map(|t| t.as_ptr().as_ptr().addr())
.collect::<Vec<_>>()
})
})
.collect();
let mut drained: Vec<usize> = drains
.into_iter()
.flat_map(|drain| drain.join().unwrap())
.collect();
drained.sort_unstable();
let mut expected: Vec<usize> = tasks.iter().map(|t| t.as_ptr().as_ptr().addr()).collect();
expected.sort_unstable();
assert_eq!(drained, expected);
for t in tasks {
assert_eq!(refs(&t), 1);
finish(t);
}
}
#[test]
fn test_drain_holds_no_lock_between_tasks() {
let set = Tasks::with_shards(1);
let tasks: Vec<Task> = (0..3).map(|_| task(&set)).collect();
for t in &tasks {
assert!(set.insert(t.clone()).is_ok());
}
set.close();
let late = task(&set);
let mut drained = 0;
for t in set.drain(0) {
assert!(set.try_lock(0).is_some(), "drain held the shard lock");
assert!(set.insert(late.clone()).is_err());
drop(t);
drained += 1;
}
assert_eq!(drained, 3);
for t in tasks.into_iter().chain([late]) {
finish(t);
}
}
#[test]
fn test_task_of_another_set_is_rejected() {
let owner = Tasks::new(1);
let other = Tasks::new(1);
let t = task(&owner);
t.clear();
let panic = catch_unwind(AssertUnwindSafe(|| drop(other.insert(t.clone())))).unwrap_err();
assert!(extract_panic_message(&*panic).contains("task belongs to another runtime"));
assert!(owner.insert(t.clone()).is_ok());
let panic = catch_unwind(AssertUnwindSafe(|| drop(other.remove(&t)))).unwrap_err();
assert!(extract_panic_message(&*panic).contains("task belongs to another runtime"));
assert_eq!(owner.teardown().len(), 1);
assert_eq!(refs(&t), 1);
}
#[test]
#[should_panic(expected = "set drained before closing")]
fn test_drain_of_an_open_set_is_rejected() {
let _ = Tasks::new(1).drain(0);
}
#[test]
fn test_second_insertion_is_rejected() {
let set = Tasks::new(1);
let t = task(&set);
assert!(set.insert(t.clone()).is_ok());
let panic = catch_unwind(AssertUnwindSafe(|| drop(set.insert(t.clone())))).unwrap_err();
assert_eq!(extract_panic_message(&*panic), "task inserted twice");
assert_eq!(refs(&t), 2);
drop(set.teardown());
finish(t);
}
#[test]
fn test_dropping_a_set_with_live_tasks_panics() {
let set = Tasks::new(1);
let t = task(&set);
assert!(set.insert(t.clone()).is_ok());
let panic = catch_unwind(AssertUnwindSafe(|| drop(set))).unwrap_err();
assert_eq!(
extract_panic_message(&*panic),
"set dropped with live tasks"
);
drop(unsafe { Task::from_raw(t.as_ptr()) });
let set = Tasks::new(1);
let u = task(&set);
assert!(set.insert(u.clone()).is_ok());
let panic = catch_unwind(AssertUnwindSafe(|| {
let _set = set;
panic!("original panic");
}))
.unwrap_err();
assert_eq!(extract_panic_message(&*panic), "original panic");
drop(unsafe { Task::from_raw(u.as_ptr()) });
finish(t);
finish(u);
}
#[test]
fn test_hash_spreads_strided_addresses() {
let set = Tasks::new(16);
let shards = set.shards.len();
let per_shard = 256;
for stride in [64, 128, 144, 192, 256, 384, 512, 4096] {
let mut lens = vec![0_usize; shards];
for i in 0..shards * per_shard {
let addr = 0x7f00_0000_0000 + i * stride;
let node = NonNull::new(std::ptr::without_provenance_mut(addr)).unwrap();
lens[set.index(node)] += 1;
}
assert!(
lens.iter()
.all(|&len| len.abs_diff(per_shard) <= per_shard / 8),
"uneven shards at stride {stride}: {lens:?}"
);
}
}
#[cfg(not(miri))]
#[test]
fn test_hashed_shards_spread_cells() {
let set = Tasks::new(16);
let count = 64 * 1024;
let tasks: Vec<Task> = (0..count).map(|_| task(&set)).collect();
for t in &tasks {
assert!(set.insert(t.clone()).is_ok());
}
let lens = set.lens();
let mean = count as f64 / lens.len() as f64;
let max = *lens.iter().max().unwrap() as f64;
let min = *lens.iter().min().unwrap() as f64;
assert!(
max < mean * 1.25 && min > mean * 0.75,
"uneven shards: {lens:?}"
);
drop(set.teardown());
for t in tasks {
finish(t);
}
}
#[test]
fn test_concurrent_insert_and_remove_return_each_task_once() {
cfg_if::cfg_if! {
if #[cfg(miri)] {
let per = 50;
} else {
let per = 2000;
}
}
let threads = 4;
let set = Arc::new(Tasks::new(1));
let tasks: Arc<Vec<Task>> = Arc::new((0..threads * per).map(|_| task(&set)).collect());
let barrier = Arc::new(Barrier::new(threads));
let workers: Vec<_> = (0..threads)
.map(|t| {
let set = set.clone();
let tasks = tasks.clone();
let barrier = barrier.clone();
thread::spawn(move || {
barrier.wait();
for (i, task) in tasks[t * per..(t + 1) * per].iter().enumerate() {
assert!(set.insert(task.clone()).is_ok());
if i % 2 == 0 {
assert!(set.remove(task).is_some());
}
}
})
})
.collect();
for worker in workers {
worker.join().unwrap();
}
assert_eq!(set.live(), threads * per / 2);
assert_eq!(set.teardown().len(), threads * per / 2);
for task in Arc::try_unwrap(tasks).ok().unwrap() {
assert_eq!(refs(&task), 1);
finish(task);
}
}
}
#[cfg(all(test, feature = "loom"))]
mod loom_tests {
use super::{tests::task, *};
use crate::iouring::task::tests::refs;
use loom::{sync::Arc, thread};
#[test]
fn test_insert_remove_and_close_race_in_one_shard() {
loom::model(|| {
let set = Arc::new(Tasks::with_shards(1));
let tasks: Arc<[Task; 3]> = Arc::new([(); 3].map(|_| task(&set)));
assert!(set.insert(tasks[0].clone()).is_ok());
assert!(set.insert(tasks[1].clone()).is_ok());
let remover = thread::spawn({
let set = set.clone();
let tasks = tasks.clone();
move || set.remove(&tasks[0]).is_some()
});
let inserter = thread::spawn({
let set = set.clone();
let tasks = tasks.clone();
move || set.insert(tasks[2].clone()).is_ok()
});
set.close();
let drained: Vec<_> = set.drain(0).map(|t| t.as_ptr()).collect();
let removed = remover.join().unwrap();
let inserted = inserter.join().unwrap();
let returned = |t: &Task| drained.contains(&t.as_ptr());
assert_ne!(removed, returned(&tasks[0]));
assert!(returned(&tasks[1]));
assert_eq!(inserted, returned(&tasks[2]));
assert_eq!(set.live(), 0);
for t in tasks.iter() {
assert_eq!(refs(t), 1);
t.clear();
}
});
}
}