#![cfg(loom)]
#![allow(
clippy::unwrap_used,
reason = "loom model lock and thread failures are test assertions"
)]
use std::collections::VecDeque;
use loom::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
use loom::sync::{Arc, Mutex};
use loom::thread;
const JOBS: usize = 3;
const BATCH_LIMIT: usize = 2;
struct Injector {
jobs: Mutex<VecDeque<usize>>,
queued: AtomicUsize,
marks: [AtomicU8; JOBS],
}
impl Injector {
fn new() -> Self {
Self {
jobs: Mutex::new((0..JOBS).collect()),
queued: AtomicUsize::new(JOBS),
marks: std::array::from_fn(|_| AtomicU8::new(0)),
}
}
fn try_dequeue(&self) -> Option<usize> {
self.jobs.lock().unwrap().pop_front()
}
fn record_claim(&self, id: usize) {
let previous = self.marks[id].fetch_add(1, Ordering::Relaxed);
assert_eq!(previous, 0, "job claimed more than once: {id}");
}
fn queued(&self) -> usize {
self.queued.load(Ordering::Relaxed)
}
}
fn steal_one(injector: &Injector) {
if injector.queued.load(Ordering::Relaxed) == 0 {
return;
}
if let Some(id) = injector.try_dequeue() {
injector.record_claim(id);
injector.queued.fetch_sub(1, Ordering::Relaxed);
}
}
fn steal_batch(injector: &Injector) {
if injector.queued.load(Ordering::Relaxed) == 0 {
return;
}
let Some(first) = injector.try_dequeue() else {
return;
};
injector.record_claim(first);
let mut retained = 0;
while retained < BATCH_LIMIT - 1 {
let Some(id) = injector.try_dequeue() else {
break;
};
injector.record_claim(id);
retained += 1;
}
thread::yield_now();
injector.queued.fetch_sub(retained + 1, Ordering::Relaxed);
}
#[test]
fn mixed_single_and_batch_handoff_conserves_jobs() {
loom::model(|| {
let injector = Arc::new(Injector::new());
let batch_injector = Arc::clone(&injector);
let batch = thread::spawn(move || steal_batch(&batch_injector));
let single_injector = Arc::clone(&injector);
let single = thread::spawn(move || steal_one(&single_injector));
batch.join().unwrap();
single.join().unwrap();
for (id, mark) in injector.marks.iter().enumerate() {
assert_eq!(
mark.load(Ordering::Relaxed),
1,
"published job was not claimed exactly once: {id}"
);
}
assert_eq!(injector.queued(), 0, "deferred accounting must settle");
assert!(injector.jobs.lock().unwrap().is_empty());
});
}