use crate::gc;
use crate::poll::{Interest, RawSocket};
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, VecDeque};
use std::time::{Duration, Instant};
pub type PollFn = unsafe extern "C-unwind" fn(state: *mut u8, task_ctx: *mut u8) -> i64;
pub(crate) fn abort_with(msg: &str) -> ! {
eprintln!("nova: panic: {msg}");
std::process::abort();
}
pub const POLL_PENDING: i64 = 0;
pub const POLL_READY: i64 = 1;
pub const STATE_SLOT_TAG: usize = 0;
pub const STATE_SLOT_OUTPUT: usize = 1;
pub const STATE_SLOT_TEMPS: usize = 2;
pub const FUTURE_SLOT_POLL: usize = 0;
pub const FUTURE_SLOT_STATE: usize = 1;
pub const FUTURE_SIZE: usize = 16;
pub const STATE_MIN_SIZE: usize = (STATE_SLOT_TEMPS) * 8;
struct Task {
poll: PollFn,
state: *mut u8,
done: bool,
output: i64,
taken: bool,
keep_root_until_taken: bool,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum Wait {
Deadline(Instant),
Task { id: i64, deadline: Option<Instant> },
Io {
socket: RawSocket,
interest: Interest,
deadline: Option<Instant>,
},
}
thread_local! {
static QUEUE: RefCell<VecDeque<i64>> = const { RefCell::new(VecDeque::new()) };
static TASKS: RefCell<Vec<Task>> = const { RefCell::new(Vec::new()) };
static BY_STATE: RefCell<HashMap<usize, i64>> = RefCell::new(HashMap::new());
static IN_BLOCK_ON: Cell<bool> = const { Cell::new(false) };
static PARKED: RefCell<Vec<(i64, Wait)>> = const { RefCell::new(Vec::new()) };
static CURRENT: Cell<Option<i64>> = const { Cell::new(None) };
static PENDING_PARK: Cell<Staged> = const {
Cell::new(Staged {
deadline: None,
io: None,
task: None,
})
};
}
pub(crate) fn current_task() -> Option<i64> {
CURRENT.with(|c| c.get())
}
#[cfg(test)]
pub(crate) fn set_current_for_test(id: Option<i64>) {
CURRENT.with(|c| c.set(id));
}
#[cfg(test)]
pub(crate) fn staged_io_for_test() -> Option<(RawSocket, Interest, Option<Instant>)> {
PENDING_PARK.with(|cell| {
let staged = cell.get();
staged
.io
.map(|(socket, interest)| (socket, interest, staged.deadline))
})
}
#[cfg(test)]
pub(crate) fn staged_deadline_for_test() -> Option<Instant> {
PENDING_PARK.with(|cell| cell.get().deadline)
}
#[cfg(test)]
pub(crate) fn staged_task_for_test() -> Option<i64> {
PENDING_PARK.with(|cell| cell.get().task)
}
pub(crate) fn prune_freed_states(is_live: &dyn Fn(usize) -> bool) {
BY_STATE.with(|m| m.borrow_mut().retain(|&addr, _| is_live(addr)));
}
unsafe fn read_future(future: *mut u8) -> (PollFn, *mut u8) {
let words = future as *mut usize;
let poll_code = unsafe { words.add(FUTURE_SLOT_POLL).read() };
let state = unsafe { words.add(FUTURE_SLOT_STATE).read() } as *mut u8;
let poll: PollFn = unsafe { std::mem::transmute::<usize, PollFn>(poll_code) };
(poll, state)
}
unsafe fn spawn_internal(future: *mut u8) -> i64 {
let (poll, state) = unsafe { read_future(future) };
if let Some(prior) = BY_STATE.with(|m| m.borrow().get(&(state as usize)).copied()) {
let still_live = TASKS.with(|tasks| {
!tasks
.borrow()
.get(prior as usize)
.expect("BY_STATE named a task id that TASKS does not have")
.taken
});
if still_live {
abort_with(
"nova_rt_task_spawn: this future is already a live task; spawn it again only after its task has been released",
);
}
}
gc::add_root(state);
let id = TASKS.with(|tasks| {
let mut tasks = tasks.borrow_mut();
let id = tasks.len() as i64;
tasks.push(Task {
poll,
state,
done: false,
output: 0,
taken: false,
keep_root_until_taken: false,
});
id
});
BY_STATE.with(|m| m.borrow_mut().insert(state as usize, id));
QUEUE.with(|queue| queue.borrow_mut().push_back(id));
id
}
unsafe fn spawn_root_internal(future: *mut u8) -> i64 {
let id = unsafe { spawn_internal(future) };
TASKS.with(|tasks| {
tasks
.borrow_mut()
.get_mut(id as usize)
.expect("spawn_internal just registered this id")
.keep_root_until_taken = true;
});
id
}
#[cfg(test)]
pub(crate) unsafe fn spawn_kept_for_test(future: *mut u8) -> i64 {
unsafe { spawn_root_internal(future) }
}
#[derive(Default, Clone, Copy, Debug)]
struct Staged {
deadline: Option<Instant>,
io: Option<(RawSocket, Interest)>,
task: Option<i64>,
}
fn staged_to_wait(staged: Staged) -> Option<Wait> {
if let Some(id) = staged.task {
return Some(Wait::Task {
id,
deadline: staged.deadline,
});
}
if let Some((socket, interest)) = staged.io {
return Some(Wait::Io {
socket,
interest,
deadline: staged.deadline,
});
}
staged.deadline.map(Wait::Deadline)
}
fn try_stage(staged: Staged, wait: Wait) -> Result<Staged, String> {
let mut next = staged;
match wait {
Wait::Deadline(at) => {
next.deadline = Some(match next.deadline {
Some(prev) => prev.min(at),
None => at,
});
}
Wait::Io {
socket,
interest,
deadline,
} => {
if let Some((prev_socket, prev_interest)) = next.io {
return Err(collision_msg(
Wait::Io {
socket: prev_socket,
interest: prev_interest,
deadline: next.deadline,
},
wait,
));
}
if let Some(prev_id) = next.task {
return Err(collision_msg(
Wait::Task {
id: prev_id,
deadline: next.deadline,
},
wait,
));
}
if let Some(at) = deadline {
next.deadline = Some(match next.deadline {
Some(prev) => prev.min(at),
None => at,
});
}
next.io = Some((socket, interest));
}
Wait::Task { id, deadline } => {
if let Some(prev_id) = next.task {
return Err(collision_msg(
Wait::Task {
id: prev_id,
deadline: next.deadline,
},
wait,
));
}
if let Some((prev_socket, prev_interest)) = next.io {
return Err(collision_msg(
Wait::Io {
socket: prev_socket,
interest: prev_interest,
deadline: next.deadline,
},
wait,
));
}
if let Some(at) = deadline {
next.deadline = Some(match next.deadline {
Some(prev) => prev.min(at),
None => at,
});
}
next.task = Some(id);
}
}
Ok(next)
}
fn collision_msg(previous: Wait, wait: Wait) -> String {
format!(
"nova_rt: two parks staged in one poll ({previous:?} then {wait:?}); \
an inner future's POLL_PENDING did not propagate"
)
}
fn stage_park(wait: Wait) {
if CURRENT.with(Cell::get).is_none() {
abort_with("nova_rt: a park was staged outside a poll");
}
PENDING_PARK.with(|cell| match try_stage(cell.get(), wait) {
Ok(next) => cell.set(next),
Err(msg) => abort_with(&msg),
});
}
pub(crate) fn stage_io_park(socket: RawSocket, interest: Interest, deadline: Option<Instant>) {
stage_park(Wait::Io {
socket,
interest,
deadline,
});
}
unsafe fn poll_one(id: i64) {
let (poll, state) = TASKS.with(|tasks| {
let tasks = tasks.borrow();
let task = tasks
.get(id as usize)
.expect("poll_one: task id is not registered");
(task.poll, task.state)
});
CURRENT.with(|c| c.set(Some(id)));
let status = unsafe { poll(state, std::ptr::null_mut()) };
CURRENT.with(|c| c.set(None));
let staged = staged_to_wait(PENDING_PARK.with(|p| p.take()));
if status == POLL_PENDING {
match staged {
Some(wait) => PARKED.with(|parked| parked.borrow_mut().push((id, wait))),
None => QUEUE.with(|queue| queue.borrow_mut().push_back(id)),
}
return;
}
if status != POLL_READY {
panic!(
"poll function for task {id} returned {status}, which is neither \
POLL_PENDING ({POLL_PENDING}) nor POLL_READY ({POLL_READY})"
);
}
let output = unsafe { (state as *mut i64).add(STATE_SLOT_OUTPUT).read() };
let release_now = TASKS.with(|tasks| {
let mut tasks = tasks.borrow_mut();
let task = tasks
.get_mut(id as usize)
.expect("poll_one: task id is not registered");
task.output = output;
task.done = true;
!task.keep_root_until_taken
});
wake_tasks_waiting_on(id);
if release_now {
release_internal(id);
}
}
fn wake_tasks_waiting_on(done_id: i64) {
let woken = PARKED.with(|parked| {
let mut parked = parked.borrow_mut();
let mut woken = Vec::new();
parked.retain(|&(id, wait)| match wait {
Wait::Task { id: target, .. } if target == done_id => {
woken.push(id);
false
}
_ => true,
});
woken
});
QUEUE.with(|queue| {
let mut queue = queue.borrow_mut();
for id in woken {
queue.push_back(id);
}
});
}
fn is_done_internal(id: i64) -> bool {
TASKS.with(|tasks| match tasks.borrow().get(id as usize) {
Some(task) => task.done,
None => abort_with(&format!("nova_rt_task_is_done: unknown task id {id}")),
})
}
fn release_internal(id: i64) {
let state = TASKS.with(|tasks| {
let mut tasks = tasks.borrow_mut();
let Some(task) = tasks.get_mut(id as usize) else {
abort_with(&format!("nova_rt_task_release: unknown task id {id}"));
};
if task.taken {
return None;
}
task.taken = true;
Some(task.state)
});
if let Some(state) = state {
gc::remove_root(state);
}
crate::fs::release_task_slots(id);
}
fn take_output_internal(id: i64) -> i64 {
let (output, state) = TASKS.with(|tasks| {
let mut tasks = tasks.borrow_mut();
let task = tasks
.get_mut(id as usize)
.expect("nova_rt_task_take_output: unknown task id");
assert!(
task.done,
"nova_rt_task_take_output: task {id} has not completed; \
poll nova_rt_task_is_done until it reports true first"
);
assert!(
!task.taken || task.keep_root_until_taken,
"nova_rt_task_take_output: task {id}'s GC root has already been \
released -- a spawned task's is released when it completes -- so \
its output can no longer be taken; read it through its future, as \
JoinHandle::join does"
);
assert!(
!task.taken,
"nova_rt_task_take_output: task {id}'s output was already taken; \
taking released the state object's GC root, so a heap-valued \
output may already have been collected"
);
task.taken = true;
(task.output, task.state)
});
gc::remove_root(state);
crate::fs::release_task_slots(id);
output
}
unsafe fn run_to_completion(future: *mut u8) -> i64 {
let root_id = unsafe { spawn_root_internal(future) };
loop {
while let Some(id) = QUEUE.with(|queue| queue.borrow_mut().pop_front()) {
unsafe { poll_one(id) };
if !PARKED.with(|parked| parked.borrow().is_empty()) {
wake_due(Instant::now());
}
}
if PARKED.with(|parked| parked.borrow().is_empty()) {
break;
}
let io: Vec<(RawSocket, Interest)> = io_parks();
match (earliest_deadline(), io.is_empty()) {
(None, true) => report_deadlock(),
(None, false) => wake_ready(crate::poll::wait(&io, None)),
(Some(at), true) => {
crate::poll::wait(&[], Some(at));
}
(Some(at), false) => wake_ready(crate::poll::wait(&io, Some(at))),
}
wake_due(Instant::now());
}
debug_assert!(
is_done_internal(root_id),
"run_to_completion: the queue drained but the root task is not done"
);
take_output_internal(root_id)
}
fn earliest_deadline() -> Option<Instant> {
PARKED.with(|parked| {
parked
.borrow()
.iter()
.filter_map(|&(_, wait)| match wait {
Wait::Deadline(at) => Some(at),
Wait::Io { deadline, .. } => deadline,
Wait::Task { deadline, .. } => deadline,
})
.min()
})
}
fn io_parks() -> Vec<(RawSocket, Interest)> {
PARKED.with(|parked| {
parked
.borrow()
.iter()
.filter_map(|&(_, wait)| match wait {
Wait::Io {
socket, interest, ..
} => Some((socket, interest)),
Wait::Deadline(_) | Wait::Task { .. } => None,
})
.collect()
})
}
fn wake_ready(ready: Vec<RawSocket>) {
if ready.is_empty() {
return;
}
let woken = PARKED.with(|parked| {
let mut parked = parked.borrow_mut();
let mut woken = Vec::new();
parked.retain(|&(id, wait)| match wait {
Wait::Io { socket, .. } if ready.contains(&socket) => {
woken.push(id);
false
}
_ => true,
});
woken
});
QUEUE.with(|queue| {
let mut queue = queue.borrow_mut();
for id in woken {
queue.push_back(id);
}
});
}
fn wake_due(now: Instant) {
let woken = PARKED.with(|parked| {
let mut parked = parked.borrow_mut();
let mut woken = Vec::new();
parked.retain(|&(id, wait)| match wait {
Wait::Deadline(deadline) if deadline <= now => {
woken.push(id);
false
}
Wait::Io {
deadline: Some(deadline),
..
} if deadline <= now => {
woken.push(id);
false
}
Wait::Task {
deadline: Some(deadline),
..
} if deadline <= now => {
woken.push(id);
false
}
_ => true,
});
woken
});
QUEUE.with(|queue| {
let mut queue = queue.borrow_mut();
for id in woken {
queue.push_back(id);
}
});
}
fn deadlock_report() -> String {
let entries = PARKED.with(|parked| parked.borrow().clone());
let plural = if entries.len() == 1 {
"task is"
} else {
"tasks are"
};
let mut report = format!(
"nova: deadlock: {} {plural} parked and none can wake\n",
entries.len()
);
for (id, wait) in entries {
match wait {
Wait::Task {
id: target,
deadline: None,
} => {
report.push_str(&format!(
" task {id} is waiting for task {target} to finish\n"
));
}
Wait::Task {
id: target,
deadline: Some(_),
} => {
report.push_str(&format!(
" task {id} is waiting for task {target} to finish, with a deadline\n"
));
}
Wait::Deadline(_) => {
report.push_str(&format!(" task {id} is waiting on a deadline\n"));
}
Wait::Io { deadline: None, .. } => {
report.push_str(&format!(" task {id} is waiting on i/o\n"));
}
Wait::Io {
deadline: Some(_), ..
} => {
report.push_str(&format!(" task {id} is waiting on i/o with a deadline\n"));
}
}
}
report
}
fn report_deadlock() -> ! {
eprint!("{}", deadlock_report());
std::process::abort();
}
#[no_mangle]
pub unsafe extern "C-unwind" fn nova_rt_task_spawn(future: *mut u8) -> i64 {
unsafe { spawn_internal(future) }
}
#[no_mangle]
pub unsafe extern "C-unwind" fn nova_rt_task_block_on(future: *mut u8) -> i64 {
if IN_BLOCK_ON.with(|in_block_on| in_block_on.get()) {
abort_with(
"nova_rt_task_block_on called re-entrantly: an async fn must not call block_on, \
which would run a second executor loop from inside the first one's frame",
);
}
IN_BLOCK_ON.with(|in_block_on| in_block_on.set(true));
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
unsafe { run_to_completion(future) }
}));
IN_BLOCK_ON.with(|in_block_on| in_block_on.set(false));
match result {
Ok(output) => output,
Err(payload) => std::panic::resume_unwind(payload),
}
}
unsafe fn task_id_of(future: *mut u8, who: &str) -> i64 {
let (_, state) = unsafe { read_future(future) };
match BY_STATE.with(|m| m.borrow().get(&(state as usize)).copied()) {
Some(id) => id,
None => abort_with(&format!(
"{who}: this future was never spawned, so there is no task to ask about"
)),
}
}
#[no_mangle]
pub unsafe extern "C-unwind" fn nova_rt_task_is_done(future: *mut u8) -> i8 {
let id = unsafe { task_id_of(future, "nova_rt_task_is_done") };
is_done_internal(id) as i8
}
#[no_mangle]
pub unsafe extern "C-unwind" fn nova_rt_task_take_output(id: i64) -> i64 {
take_output_internal(id)
}
pub fn output_bits(id: i64) -> i64 {
TASKS.with(|tasks| {
let tasks = tasks.borrow();
let task = tasks
.get(id as usize)
.expect("output_bits: unknown task id");
assert!(
task.done,
"output_bits: task {id} has not completed, so its output field \
still holds its 0 initializer"
);
task.output
})
}
#[no_mangle]
pub unsafe extern "C-unwind" fn nova_rt_task_release(future: *mut u8) {
let id = unsafe { task_id_of(future, "nova_rt_task_release") };
release_internal(id);
}
const YIELD_STATE_SIZE: usize = STATE_MIN_SIZE;
const _: () = assert!(YIELD_STATE_SIZE >= (STATE_SLOT_OUTPUT + 1) * 8);
unsafe extern "C-unwind" fn poll_yield_once(state: *mut u8, _task_ctx: *mut u8) -> i64 {
let slots = state as *mut i64;
let tag = unsafe { slots.add(STATE_SLOT_TAG).read() };
if tag == 0 {
unsafe { slots.add(STATE_SLOT_TAG).write(1) };
return POLL_PENDING;
}
unsafe { slots.add(STATE_SLOT_OUTPUT).write(0) };
POLL_READY
}
pub(crate) fn build_future(
poll: PollFn,
state_size: usize,
init: impl FnOnce(*mut i64),
) -> *mut u8 {
debug_assert!(
state_size >= STATE_MIN_SIZE,
"build_future: state_size {state_size} is smaller than STATE_MIN_SIZE \
({STATE_MIN_SIZE}); STATE_SLOT_OUTPUT is read unconditionally on \
completion and would be read out of bounds"
);
let state = gc::alloc(state_size, true);
gc::add_root(state);
init(state as *mut i64);
let fat = gc::alloc(FUTURE_SIZE, true);
unsafe {
(fat as *mut i64)
.add(FUTURE_SLOT_POLL)
.write(poll as usize as i64);
(fat as *mut i64).add(FUTURE_SLOT_STATE).write(state as i64);
}
gc::remove_root(state);
fat
}
#[no_mangle]
pub extern "C-unwind" fn nova_rt_task_yield_future() -> *mut u8 {
let poll: PollFn = poll_yield_once;
build_future(poll, YIELD_STATE_SIZE, |_slots| {})
}
unsafe extern "C-unwind" fn poll_sleep(state: *mut u8, _task_ctx: *mut u8) -> i64 {
let slots = state as *mut i64;
let deadline = unsafe { slots.add(SLEEP_SLOT_DEADLINE_NANOS).read() };
if crate::time::now_nanos() >= deadline {
unsafe { slots.add(STATE_SLOT_OUTPUT).write(0) };
return POLL_READY;
}
stage_park(Wait::Deadline(instant_from_deadline_nanos(deadline)));
POLL_PENDING
}
fn deadline_nanos_from_now(nanos: i64) -> i64 {
crate::time::now_nanos().saturating_add(nanos.max(0))
}
fn instant_from_deadline_nanos(deadline: i64) -> Instant {
let ns = u64::try_from(deadline).unwrap_or(0);
furthest_representable_instant(crate::time::epoch(), Duration::from_nanos(ns))
}
fn furthest_representable_instant(base: Instant, mut duration: Duration) -> Instant {
loop {
if let Some(instant) = base.checked_add(duration) {
return instant;
}
duration /= 2;
}
}
const SLEEP_SLOT_DEADLINE_NANOS: usize = STATE_SLOT_TEMPS;
const SLEEP_STATE_SIZE: usize = STATE_MIN_SIZE + 8;
const _: () = assert!(SLEEP_STATE_SIZE >= (SLEEP_SLOT_DEADLINE_NANOS + 1) * 8);
#[no_mangle]
pub extern "C-unwind" fn nova_rt_task_sleep_future_nanos(nanos: i64) -> *mut u8 {
let poll: PollFn = poll_sleep;
build_future(poll, SLEEP_STATE_SIZE, |slots| {
unsafe {
slots
.add(SLEEP_SLOT_DEADLINE_NANOS)
.write(deadline_nanos_from_now(nanos))
};
})
}
unsafe extern "C-unwind" fn poll_join(state: *mut u8, _task_ctx: *mut u8) -> i64 {
let slots = state as *mut i64;
let target = unsafe { slots.add(JOIN_SLOT_TARGET).read() };
if is_done_internal(target) {
unsafe { slots.add(STATE_SLOT_OUTPUT).write(0) };
return POLL_READY;
}
stage_park(Wait::Task {
id: target,
deadline: None,
});
POLL_PENDING
}
const JOIN_SLOT_TARGET: usize = STATE_SLOT_TEMPS;
const JOIN_STATE_SIZE: usize = STATE_MIN_SIZE + 8;
const _: () = assert!(JOIN_STATE_SIZE >= (JOIN_SLOT_TARGET + 1) * 8);
#[no_mangle]
pub unsafe extern "C-unwind" fn nova_rt_task_join_future(future: *mut u8) -> *mut u8 {
let target = unsafe { task_id_of(future, "nova_rt_task_join_future") };
let poll: PollFn = poll_join;
build_future(poll, JOIN_STATE_SIZE, |slots| {
unsafe { slots.add(JOIN_SLOT_TARGET).write(target) };
})
}
const TIMEOUT_SLOT_INNER: usize = STATE_SLOT_TEMPS;
const TIMEOUT_SLOT_DEADLINE_NANOS: usize = STATE_SLOT_TEMPS + 1;
const TIMEOUT_STATE_SIZE: usize = STATE_MIN_SIZE + 16;
const _: () = assert!(TIMEOUT_STATE_SIZE >= (TIMEOUT_SLOT_DEADLINE_NANOS + 1) * 8);
pub const TIMEOUT_STATUS_COMPLETED: i64 = 0;
pub const TIMEOUT_STATUS_ELAPSED: i64 = 1;
unsafe extern "C-unwind" fn poll_timeout(state: *mut u8, _task_ctx: *mut u8) -> i64 {
let slots = state as *mut i64;
let inner = unsafe { slots.add(TIMEOUT_SLOT_INNER).read() } as *mut u8;
let deadline = unsafe { slots.add(TIMEOUT_SLOT_DEADLINE_NANOS).read() };
let inner_poll_addr = unsafe { (inner as *mut usize).add(FUTURE_SLOT_POLL).read() };
let inner_poll: PollFn = unsafe { std::mem::transmute(inner_poll_addr) };
let inner_state = unsafe { (inner as *mut usize).add(FUTURE_SLOT_STATE).read() } as *mut u8;
let park_before_inner = PENDING_PARK.with(Cell::get);
let inner_status = unsafe { inner_poll(inner_state, std::ptr::null_mut()) };
if inner_status == POLL_READY {
PENDING_PARK.with(|cell| cell.set(park_before_inner));
unsafe { slots.add(STATE_SLOT_OUTPUT).write(TIMEOUT_STATUS_COMPLETED) };
return POLL_READY;
}
if inner_status != POLL_PENDING {
abort_with(&format!(
"nova_rt: a future polled by a timeout returned {inner_status}, which is \
neither POLL_PENDING ({POLL_PENDING}) nor POLL_READY ({POLL_READY})"
));
}
if crate::time::now_nanos() >= deadline {
PENDING_PARK.with(|cell| cell.set(park_before_inner));
unsafe { slots.add(STATE_SLOT_OUTPUT).write(TIMEOUT_STATUS_ELAPSED) };
return POLL_READY;
}
stage_park(Wait::Deadline(instant_from_deadline_nanos(deadline)));
POLL_PENDING
}
#[no_mangle]
pub extern "C-unwind" fn nova_rt_task_timeout_future(nanos: i64, fut: *mut u8) -> *mut u8 {
let poll: PollFn = poll_timeout;
let deadline = deadline_nanos_from_now(nanos);
build_future(poll, TIMEOUT_STATE_SIZE, |slots| {
unsafe { slots.add(TIMEOUT_SLOT_INNER).write(fut as i64) };
unsafe { slots.add(TIMEOUT_SLOT_DEADLINE_NANOS).write(deadline) };
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
unsafe extern "C-unwind" fn poll_suspend_once(state: *mut u8, _ctx: *mut u8) -> i64 {
let slots = state as *mut i64;
let tag = unsafe { *slots.add(STATE_SLOT_TAG) };
if tag == 0 {
unsafe { *slots.add(STATE_SLOT_TAG) = 1 };
POLL_PENDING
} else {
unsafe { *slots.add(STATE_SLOT_OUTPUT) = 42 };
POLL_READY
}
}
unsafe extern "C-unwind" fn poll_ready_now(state: *mut u8, _ctx: *mut u8) -> i64 {
unsafe { *(state as *mut i64).add(STATE_SLOT_OUTPUT) = 7 };
POLL_READY
}
#[inline(never)]
fn make_future(f: PollFn, temps: usize) -> *mut u8 {
let state = gc::alloc((STATE_SLOT_TEMPS + temps) * 8, true);
let fat = gc::alloc(FUTURE_SIZE, true);
unsafe {
(fat as *mut usize).add(FUTURE_SLOT_POLL).write(f as usize);
(fat as *mut usize)
.add(FUTURE_SLOT_STATE)
.write(state as usize);
}
fat
}
fn state_of(future: *mut u8) -> usize {
unsafe { (future as *mut usize).add(FUTURE_SLOT_STATE).read() }
}
#[cfg(test)]
fn poll_once_for_test(fut: *mut u8) -> i64 {
let poll_addr = unsafe { (fut as *mut usize).add(FUTURE_SLOT_POLL).read() };
let poll: PollFn = unsafe { std::mem::transmute(poll_addr) };
let state = state_of(fut) as *mut u8;
let previous = current_task();
set_current_for_test(Some(0));
let status = unsafe { poll(state, std::ptr::null_mut()) };
PENDING_PARK.with(|cell| {
cell.take();
});
set_current_for_test(previous);
status
}
#[cfg(test)]
fn output_of_for_test(fut: *mut u8) -> i64 {
let slots = state_of(fut) as *mut i64;
unsafe { slots.add(STATE_SLOT_OUTPUT).read() }
}
fn test_future(poll: PollFn) -> *mut u8 {
build_future(poll, STATE_MIN_SIZE, |_| {})
}
fn with_parked<T>(entries: &[(i64, Wait)], f: impl FnOnce() -> T) -> T {
let previous = PARKED.with(|p| p.replace(entries.to_vec()));
let result = f();
PARKED.with(|p| *p.borrow_mut() = previous);
result
}
#[test]
fn block_on_runs_a_ready_future_and_returns_its_output() {
let fut = make_future(poll_ready_now, 0);
assert_eq!(unsafe { nova_rt_task_block_on(fut) }, 7);
}
#[test]
fn block_on_re_polls_a_pending_future_until_it_completes() {
let fut = make_future(poll_suspend_once, 0);
assert_eq!(unsafe { nova_rt_task_block_on(fut) }, 42);
}
#[test]
fn a_spawned_task_runs_to_completion_and_reports_done() {
let fut = make_future(poll_suspend_once, 0);
let id = unsafe { nova_rt_task_spawn(fut) };
assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 0);
let root = make_future(poll_ready_now, 0);
unsafe { nova_rt_task_block_on(root) };
assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 1);
assert_eq!(output_bits(id), 42);
}
#[test]
fn output_bits_of_an_unfinished_task_panics() {
let fut = make_future(poll_suspend_once, 0);
let id = unsafe { nova_rt_task_spawn(fut) };
let r = std::panic::catch_unwind(|| output_bits(id));
assert!(r.is_err(), "output_bits must refuse an unfinished task");
}
#[test]
fn a_completed_tasks_state_stays_rooted_until_its_output_is_taken() {
let fut = make_future(poll_ready_now, 0);
let state = state_of(fut);
let id = unsafe { spawn_root_internal(fut) };
assert_eq!(
gc::root_count(state),
1,
"spawn must register the state object exactly once"
);
unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 1);
assert_eq!(
gc::root_count(state),
1,
"a completed task's state must stay rooted until its output is \
taken -- the output value lives in the state object's own output \
slot, and Task::output's copy is in a Rust-heap Vec the \
collector never scans"
);
assert_eq!(unsafe { nova_rt_task_take_output(id) }, 7);
assert_eq!(
gc::root_count(state),
0,
"take_output must release the state object's root exactly once, \
so add_root/remove_root stay balanced"
);
}
#[test]
fn a_spawned_tasks_root_is_released_at_completion() {
let fut = make_future(poll_ready_now, 0);
let state = state_of(fut);
let id = unsafe { nova_rt_task_spawn(fut) };
assert_eq!(
gc::root_count(state),
1,
"spawn must register the state object exactly once"
);
unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 1);
assert_eq!(
gc::root_count(state),
0,
"a spawned task's root must be released at completion, so a \
handle nobody joins does not keep its state alive"
);
assert_eq!(output_bits(id), 7, "the TASKS entry keeps its output copy");
}
#[test]
fn a_spawned_tasks_stashed_fs_payload_is_released_at_completion() {
let fut = make_future(poll_ready_now, 0);
let id = unsafe { nova_rt_task_spawn(fut) };
let payload = crate::gc_str("release-at-completion-payload");
let addr = payload as usize;
crate::fs::stash_for_test(id, crate::fs::Slot::Buffer, payload);
assert_eq!(
gc::root_count(addr),
1,
"stash_for_test must root its pointer"
);
unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 1);
assert_eq!(
gc::root_count(addr),
0,
"completion must release the task's stashed fs payload, not only \
its state root"
);
}
#[test]
fn taking_a_spawned_tasks_output_after_completion_panics_naming_the_release() {
let fut = make_future(poll_ready_now, 0);
let id = unsafe { nova_rt_task_spawn(fut) };
unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
let r = std::panic::catch_unwind(|| unsafe { nova_rt_task_take_output(id) });
let msg = match r {
Ok(v) => panic!("take must panic for a released spawned task, got {v}"),
Err(e) => e.downcast_ref::<String>().cloned().unwrap_or_default(),
};
assert!(
msg.contains("released when it completes"),
"the panic must name the cause: {msg}"
);
}
#[test]
fn spawning_a_completed_unjoined_future_again_succeeds() {
let fut = make_future(poll_ready_now, 0);
let first = unsafe { nova_rt_task_spawn(fut) };
unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 1);
let second = unsafe { nova_rt_task_spawn(fut) };
assert_ne!(first, second, "a re-spawn registers a new task");
unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 1);
}
std::thread_local! {
static OBSERVED_ROOT_STATE: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
static OBSERVED_ROOT_COUNT: std::cell::Cell<i64> = const { std::cell::Cell::new(-1) };
}
unsafe extern "C-unwind" fn poll_observe_root(_state: *mut u8, _ctx: *mut u8) -> i64 {
let root_state = OBSERVED_ROOT_STATE.with(|c| c.get());
OBSERVED_ROOT_COUNT.with(|c| c.set(gc::root_count(root_state) as i64));
POLL_READY
}
unsafe extern "C-unwind" fn poll_spawn_observer_then_ready(
state: *mut u8,
_ctx: *mut u8,
) -> i64 {
OBSERVED_ROOT_STATE.with(|c| c.set(state as usize));
let observer = make_future(poll_observe_root, 0);
unsafe { nova_rt_task_spawn(observer) };
unsafe { *(state as *mut i64).add(STATE_SLOT_OUTPUT) = 7 };
POLL_READY
}
#[test]
fn a_block_on_roots_state_stays_rooted_until_taken() {
let root = make_future(poll_spawn_observer_then_ready, 0);
let root_state = state_of(root);
assert_eq!(unsafe { nova_rt_task_block_on(root) }, 7);
assert_eq!(
OBSERVED_ROOT_COUNT.with(|c| c.get()),
1,
"the block_on root's state must stay rooted between its \
completion and run_to_completion's take"
);
assert_eq!(
gc::root_count(root_state),
0,
"run_to_completion's take must release it exactly once"
);
}
#[test]
fn releasing_a_task_also_releases_its_stashed_fs_payload() {
let fut = make_future(poll_ready_now, 0);
let id = unsafe { nova_rt_task_spawn(fut) };
let payload = crate::gc_str("release-internal-payload");
let addr = payload as usize;
crate::fs::stash_for_test(id, crate::fs::Slot::Buffer, payload);
assert_eq!(
gc::root_count(addr),
1,
"stash_for_test must root its pointer"
);
release_internal(id);
assert_eq!(
gc::root_count(addr),
0,
"release_internal must release the task's stashed fs payload, \
not only its own state root"
);
}
#[test]
fn taking_a_tasks_output_also_releases_its_stashed_fs_payload() {
let fut = make_future(poll_ready_now, 0);
let id = unsafe { spawn_root_internal(fut) };
unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 1);
let payload = crate::gc_str("take-output-internal-payload");
let addr = payload as usize;
crate::fs::stash_for_test(id, crate::fs::Slot::Buffer, payload);
assert_eq!(
gc::root_count(addr),
1,
"stash_for_test must root its pointer"
);
assert_eq!(
take_output_internal(id),
7,
"poll_ready_now's own output, unaffected by the stashed fs payload"
);
assert_eq!(
gc::root_count(addr),
0,
"take_output_internal must release the task's stashed fs \
payload, not only its own state root"
);
}
#[test]
fn releasing_a_task_releases_all_three_stashed_fs_slot_kinds() {
let fut = make_future(poll_ready_now, 0);
let id = unsafe { nova_rt_task_spawn(fut) };
let buffer = crate::gc_str("all-three-buffer-payload");
let array = crate::gc_str("all-three-array-payload");
let message = crate::gc_str("all-three-message-payload");
let addrs = [buffer as usize, array as usize, message as usize];
crate::fs::stash_for_test(id, crate::fs::Slot::Buffer, buffer);
crate::fs::stash_for_test(id, crate::fs::Slot::Array, array);
crate::fs::stash_for_test(id, crate::fs::Slot::Message, message);
for addr in addrs {
assert_eq!(
gc::root_count(addr),
1,
"stash_for_test must root each of the three payloads"
);
}
release_internal(id);
for addr in addrs {
assert_eq!(
gc::root_count(addr),
0,
"release_internal must release all three of the task's \
stashed fs payload roots -- buffer, array and message alike \
-- not only whichever kind a caller happened to seed"
);
}
}
#[test]
fn taking_an_output_twice_panics_rather_than_returning_stale_bits() {
let fut = make_future(poll_ready_now, 0);
let id = unsafe { spawn_root_internal(fut) };
unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
assert_eq!(unsafe { nova_rt_task_take_output(id) }, 7);
let r = std::panic::catch_unwind(|| unsafe { nova_rt_task_take_output(id) });
assert!(r.is_err(), "a second take must panic, not succeed");
}
#[test]
fn taking_the_output_of_an_unfinished_task_panics() {
let fut = make_future(poll_suspend_once, 0);
let id = unsafe { nova_rt_task_spawn(fut) };
assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 0);
let r = std::panic::catch_unwind(|| unsafe { nova_rt_task_take_output(id) });
assert!(
r.is_err(),
"taking an unfinished task's output must panic, not return 0"
);
}
#[test]
fn an_out_of_range_poll_status_panics_rather_than_completing_the_task() {
unsafe extern "C-unwind" fn poll_bogus_status(_s: *mut u8, _c: *mut u8) -> i64 {
2
}
let fut = make_future(poll_bogus_status, 0);
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe {
nova_rt_task_block_on(fut)
}));
let payload = r.expect_err("an unknown poll status must panic");
let msg = payload
.downcast_ref::<String>()
.map(String::as_str)
.unwrap_or("");
assert!(
msg.contains("returned 2"),
"the panic must name the offending status, got {msg:?}"
);
}
#[test]
fn two_pending_tasks_interleave_round_robin() {
thread_local! {
static ORDER: RefCell<Vec<i64>> = const { RefCell::new(Vec::new()) };
}
ORDER.with(|o| o.borrow_mut().clear());
unsafe extern "C-unwind" fn poll_a(state: *mut u8, _c: *mut u8) -> i64 {
unsafe { record(state, 1) }
}
unsafe extern "C-unwind" fn poll_b(state: *mut u8, _c: *mut u8) -> i64 {
unsafe { record(state, 2) }
}
unsafe fn record(state: *mut u8, who: i64) -> i64 {
let slots = state as *mut i64;
let tag = unsafe { *slots.add(STATE_SLOT_TAG) };
ORDER.with(|o| o.borrow_mut().push(who));
if tag < 2 {
unsafe { *slots.add(STATE_SLOT_TAG) = tag + 1 };
POLL_PENDING
} else {
unsafe { *slots.add(STATE_SLOT_OUTPUT) = who };
POLL_READY
}
}
let a = make_future(poll_a, 0);
let b = make_future(poll_b, 0);
unsafe {
nova_rt_task_spawn(a);
nova_rt_task_spawn(b);
nova_rt_task_block_on(make_future(poll_suspend_once, 0));
}
let order = ORDER.with(|o| o.borrow().clone());
let a_positions: Vec<usize> = order
.iter()
.enumerate()
.filter(|(_, &w)| w == 1)
.map(|(i, _)| i)
.collect();
let b_positions: Vec<usize> = order
.iter()
.enumerate()
.filter(|(_, &w)| w == 2)
.map(|(i, _)| i)
.collect();
assert!(
a_positions.len() >= 3 && b_positions.len() >= 3,
"order = {order:?}"
);
assert!(
a_positions[0] < b_positions[0] && b_positions[0] < a_positions[1],
"expected round-robin interleaving, got {order:?}"
);
}
#[test]
fn the_re_entrancy_guard_is_armed_only_inside_block_on() {
thread_local! {
static SEEN: Cell<bool> = const { Cell::new(false) };
}
unsafe extern "C-unwind" fn poll_observes_guard(state: *mut u8, _c: *mut u8) -> i64 {
SEEN.with(|s| s.set(IN_BLOCK_ON.with(|g| g.get())));
unsafe { *(state as *mut i64).add(STATE_SLOT_OUTPUT) = 3 };
POLL_READY
}
assert!(
!IN_BLOCK_ON.with(|g| g.get()),
"the guard must not be armed before any block_on"
);
assert_eq!(
unsafe { nova_rt_task_block_on(make_future(poll_observes_guard, 0)) },
3
);
assert!(
SEEN.with(|s| s.get()),
"the guard must be armed while a poll function runs"
);
assert!(
!IN_BLOCK_ON.with(|g| g.get()),
"the guard must be disarmed once block_on returns"
);
}
#[test]
fn releasing_a_task_unroots_its_state_exactly_once_however_often_it_is_called() {
let fut = make_future(poll_ready_now, 0);
let state = state_of(fut);
unsafe { nova_rt_task_spawn(fut) };
gc::add_root(state as *mut u8);
assert_eq!(gc::root_count(state), 2);
unsafe { nova_rt_task_release(fut) };
assert_eq!(
gc::root_count(state),
1,
"release must cancel the executor's own registration"
);
unsafe {
nova_rt_task_release(fut);
nova_rt_task_release(fut);
}
assert_eq!(
gc::root_count(state),
1,
"a repeated release must be a no-op, not another remove_root"
);
gc::remove_root(state as *mut u8);
}
#[test]
fn a_released_tasks_output_slot_still_holds_its_value() {
let fut = make_future(poll_ready_now, 0);
unsafe { nova_rt_task_spawn(fut) };
unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 1);
unsafe { nova_rt_task_release(fut) };
let out = unsafe { (state_of(fut) as *mut i64).add(STATE_SLOT_OUTPUT).read() };
assert_eq!(
out, 7,
"the value stays in the slot the poll fn wrote it to"
);
}
#[test]
fn a_handle_on_a_never_spawned_future_is_not_reported_done() {
let fut = make_future(poll_ready_now, 0);
unsafe { nova_rt_task_spawn(fut) };
assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 0, "not polled yet");
}
#[test]
fn is_done_follows_the_future_it_is_given_not_a_positional_id() {
let a = make_future(poll_ready_now, 0);
let b = make_future(poll_suspend_once, 0);
let (id_a, id_b) = unsafe { (nova_rt_task_spawn(a), nova_rt_task_spawn(b)) };
unsafe {
poll_one(id_a);
poll_one(id_b);
}
assert_eq!(unsafe { nova_rt_task_is_done(a) }, 1);
assert_eq!(
unsafe { nova_rt_task_is_done(b) },
0,
"b needs a second poll; reporting it done means the lookup did not \
distinguish the two futures"
);
}
#[test]
fn releasing_by_future_unroots_that_futures_state_and_no_other() {
let a = make_future(poll_ready_now, 0);
let b = make_future(poll_ready_now, 0);
let (sa, sb) = (state_of(a), state_of(b));
unsafe {
nova_rt_task_spawn(a);
nova_rt_task_spawn(b);
}
assert_eq!(gc::root_count(sa), 1);
assert_eq!(gc::root_count(sb), 1);
unsafe { nova_rt_task_release(a) };
assert_eq!(gc::root_count(sa), 0, "release must unroot its own target");
assert_eq!(gc::root_count(sb), 1, "and must not touch another task's");
}
#[test]
fn releasing_the_same_future_twice_unroots_once() {
let fut = make_future(poll_ready_now, 0);
let state = state_of(fut);
unsafe {
nova_rt_task_spawn(fut);
nova_rt_task_release(fut);
nova_rt_task_release(fut);
}
assert_eq!(gc::root_count(state), 0);
}
#[test]
fn spawning_the_same_future_again_after_release_succeeds() {
let fut = make_future(poll_ready_now, 0);
let id1 = unsafe { nova_rt_task_spawn(fut) };
unsafe { nova_rt_task_release(fut) };
let id2 = unsafe { nova_rt_task_spawn(fut) };
assert_ne!(
id1, id2,
"the second spawn must register a new task, not reuse the first's"
);
unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
assert_eq!(
unsafe { nova_rt_task_is_done(fut) },
1,
"the re-spawned task must actually run, not merely avoid aborting"
);
}
#[test]
fn a_swept_states_key_is_dropped_so_a_recycled_address_cannot_misresolve() {
let fut = make_future(poll_ready_now, 0);
let state = state_of(fut);
unsafe { nova_rt_task_spawn(fut) };
unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
unsafe { nova_rt_task_release(fut) };
assert!(
BY_STATE.with(|m| m.borrow().contains_key(&state)),
"spawn must have registered this state, or the removal below \
proves nothing"
);
gc::sweep_with_roots_for_test(&[]);
assert!(
!BY_STATE.with(|m| m.borrow().contains_key(&state)),
"a freed state's key must not survive it: the address can be \
handed to an unrelated object, and this map is what the two \
Nova-facing reads resolve, so a surviving key answers about the \
old task instead of rejecting a future that was never spawned"
);
}
#[test]
fn a_reachable_futures_key_survives_a_collection_so_a_second_read_resolves() {
let fut = make_future(poll_ready_now, 0);
let state = state_of(fut);
unsafe { nova_rt_task_spawn(fut) };
unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
unsafe { nova_rt_task_release(fut) };
gc::sweep_with_roots_for_test(&[fut as usize]);
assert_eq!(
gc::object_info(state).map(|(_, scan)| scan),
Some(true),
"the state must have been reached through the future's own words, \
or this test is asserting nothing about tracing"
);
assert!(
BY_STATE.with(|m| m.borrow().contains_key(&state)),
"a state a handle can still reach must keep its key, or a second \
join aborts on a handle that did nothing wrong"
);
assert_eq!(
unsafe { nova_rt_task_is_done(fut) },
1,
"and the surviving key must still resolve to that task"
);
}
#[test]
fn a_sweep_freeing_only_non_state_objects_removes_no_key() {
let fut = make_future(poll_ready_now, 0);
let state = state_of(fut);
unsafe { nova_rt_task_spawn(fut) };
unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
unsafe { nova_rt_task_release(fut) };
let keys: Vec<usize> = BY_STATE.with(|m| m.borrow().keys().copied().collect());
assert!(
keys.contains(&state),
"spawn must have registered this state"
);
for _ in 0..100 {
gc::alloc(24, true);
}
let before = BY_STATE.with(|m| m.borrow().clone());
gc::sweep_with_roots_for_test(&keys);
assert_eq!(
BY_STATE.with(|m| m.borrow().clone()),
before,
"a sweep that freed no state removed a key"
);
}
#[test]
fn a_large_states_key_is_kept_while_reachable_and_dropped_once_freed() {
let mut kept = Vec::new();
let mut dropped = Vec::new();
for i in 0..6 {
let _filler = gc::alloc(3000, true);
let fut = make_future(poll_ready_now, 300);
unsafe { nova_rt_task_spawn(fut) };
if i % 2 == 0 {
kept.push(fut);
} else {
dropped.push(fut);
}
}
unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
for &f in kept.iter().chain(&dropped) {
unsafe { nova_rt_task_release(f) };
}
let kept_states: Vec<usize> = kept.iter().map(|&f| state_of(f)).collect();
let dropped_states: Vec<usize> = dropped.iter().map(|&f| state_of(f)).collect();
for s in kept_states.iter().chain(&dropped_states) {
assert!(
BY_STATE.with(|m| m.borrow().contains_key(s)),
"spawn must have registered every state, or the checks below prove nothing"
);
assert!(
gc::object_info(*s).is_some_and(|(size, _)| size > 2048),
"the state must be on the large path"
);
}
let roots: Vec<usize> = kept.iter().map(|&f| f as usize).collect();
gc::sweep_with_roots_for_test(&roots);
for s in &kept_states {
assert!(
BY_STATE.with(|m| m.borrow().contains_key(s)),
"a large state a handle can still reach lost its key"
);
}
for s in &dropped_states {
assert!(
!BY_STATE.with(|m| m.borrow().contains_key(s)),
"a freed large state's key survived it"
);
}
}
#[test]
fn the_yield_futures_layout_is_the_one_the_abi_declares() {
let fut = nova_rt_task_yield_future();
assert_eq!(
gc::object_info(fut as usize),
Some((FUTURE_SIZE, true)),
"the fat pointer must be exactly the two-word future, scanned"
);
let state = state_of(fut);
assert_eq!(
gc::object_info(state),
Some((STATE_MIN_SIZE, true)),
"the state object must be at least the tag and output slots, scanned"
);
let poll = unsafe { (fut as *mut usize).add(FUTURE_SLOT_POLL).read() };
let expected: PollFn = poll_yield_once;
assert_eq!(
poll, expected as usize,
"word 0 must be the poll function's address, not the state's"
);
}
#[test]
fn the_sleep_futures_layout_is_the_one_the_abi_declares() {
let fut = nova_rt_task_sleep_future_nanos(0);
assert_eq!(
gc::object_info(fut as usize),
Some((FUTURE_SIZE, true)),
"the fat pointer must be exactly the two-word future, scanned"
);
let state = state_of(fut);
assert_eq!(
gc::object_info(state),
Some((SLEEP_STATE_SIZE, true)),
"the state object must be the ABI minimum plus the one temp slot \
holding `nanos`, scanned"
);
let poll = unsafe { (fut as *mut usize).add(FUTURE_SLOT_POLL).read() };
let expected: PollFn = poll_sleep;
assert_eq!(
poll, expected as usize,
"word 0 must be the poll function's address, not the state's"
);
}
#[test]
fn a_sleep_stages_a_deadline_of_the_right_magnitude() {
let before = Instant::now();
let fut = nova_rt_task_sleep_future_nanos(50 * 1_000_000);
let poll_addr = unsafe { (fut as *mut usize).add(FUTURE_SLOT_POLL).read() };
let poll: PollFn = unsafe { std::mem::transmute(poll_addr) };
let state = state_of(fut) as *mut u8;
let previous = current_task();
set_current_for_test(Some(0));
let first = unsafe { poll(state, std::ptr::null_mut()) };
let staged = staged_deadline_for_test();
PENDING_PARK.with(|cell| {
cell.take();
});
set_current_for_test(previous);
assert_eq!(first, POLL_PENDING, "a sleep must park on its first poll");
let staged = staged.expect("a sleep must stage a deadline");
let delta = staged.duration_since(before);
assert!(
delta >= Duration::from_millis(40) && delta <= Duration::from_millis(500),
"a 50ms sleep staged a deadline {delta:?} out -- a unit error, not jitter"
);
}
#[test]
fn a_sleep_polled_early_re_stages_the_same_deadline() {
let fut = nova_rt_task_sleep_future_nanos(60 * 1_000_000_000);
let poll_addr = unsafe { (fut as *mut usize).add(FUTURE_SLOT_POLL).read() };
let poll: PollFn = unsafe { std::mem::transmute(poll_addr) };
let state = state_of(fut) as *mut u8;
let previous = current_task();
set_current_for_test(Some(0));
let first = unsafe { poll(state, std::ptr::null_mut()) };
let staged_first = staged_deadline_for_test();
PENDING_PARK.with(|cell| {
cell.take();
});
let second = unsafe { poll(state, std::ptr::null_mut()) };
let staged_second = staged_deadline_for_test();
PENDING_PARK.with(|cell| {
cell.take();
});
set_current_for_test(previous);
assert_eq!(
first, POLL_PENDING,
"a 60s sleep must park on its first poll"
);
assert_eq!(
second, POLL_PENDING,
"and must stay pending when polled again before its deadline"
);
assert_eq!(
staged_second, staged_first,
"the re-staged deadline must be the original, not a fresh one"
);
}
#[test]
fn a_sleep_polled_after_its_deadline_completes() {
let fut = nova_rt_task_sleep_future_nanos(0);
let poll_addr = unsafe { (fut as *mut usize).add(FUTURE_SLOT_POLL).read() };
let poll: PollFn = unsafe { std::mem::transmute(poll_addr) };
let state = state_of(fut) as *mut u8;
let previous = current_task();
set_current_for_test(Some(0));
let status = unsafe { poll(state, std::ptr::null_mut()) };
PENDING_PARK.with(|cell| {
cell.take();
});
set_current_for_test(previous);
assert_eq!(status, POLL_READY, "a zero-nanosecond sleep is already due");
}
#[test]
fn instant_from_deadline_nanos_of_i64_max_is_not_already_due() {
let before = Instant::now();
let staged = instant_from_deadline_nanos(i64::MAX);
assert!(
staged >= before,
"a deadline this far in the future must not stage an instant \
that is already due"
);
}
#[test]
fn furthest_representable_instant_forces_the_halving_fallback_and_is_not_already_due() {
let base = Instant::now();
assert!(
base.checked_add(Duration::MAX).is_none(),
"this test only exercises the halving loop if Duration::MAX \
overflows Instant::checked_add on this platform; if that ever \
changes, this assertion (not the one below) is what will fail"
);
let before = Instant::now();
let staged = furthest_representable_instant(base, Duration::MAX);
assert!(
staged >= before,
"the furthest representable instant must not be treated as \
already due"
);
}
#[test]
fn a_zero_duration_timeout_over_a_ready_future_reports_completed() {
let inner = nova_rt_task_sleep_future_nanos(0);
let fut = nova_rt_task_timeout_future(0, inner);
let status = poll_once_for_test(fut);
assert_eq!(status, POLL_READY);
assert_eq!(
output_of_for_test(fut),
TIMEOUT_STATUS_COMPLETED,
"the inner future was ready, so this must not report elapsed"
);
}
#[test]
fn an_expired_timeout_over_a_pending_future_reports_elapsed() {
let inner = nova_rt_task_sleep_future_nanos(60 * 1_000_000_000);
let fut = nova_rt_task_timeout_future(0, inner);
let status = poll_once_for_test(fut);
assert_eq!(status, POLL_READY);
assert_eq!(output_of_for_test(fut), TIMEOUT_STATUS_ELAPSED);
}
#[test]
fn a_live_timeout_over_a_pending_future_stages_both_deadlines_merged() {
let inner = nova_rt_task_sleep_future_nanos(60 * 1_000_000_000);
let fut = nova_rt_task_timeout_future(1_000_000, inner);
let before = Instant::now();
let poll_addr = unsafe { (fut as *mut usize).add(FUTURE_SLOT_POLL).read() };
let poll: PollFn = unsafe { std::mem::transmute(poll_addr) };
let state = state_of(fut) as *mut u8;
let previous = current_task();
set_current_for_test(Some(0));
let status = unsafe { poll(state, std::ptr::null_mut()) };
let staged = staged_deadline_for_test();
PENDING_PARK.with(|cell| {
cell.take();
});
set_current_for_test(previous);
assert_eq!(
status, POLL_PENDING,
"neither the inner nor the deadline is done"
);
let staged = staged.expect("a park must be staged");
let delta = staged.duration_since(before);
assert!(
delta <= Duration::from_secs(1),
"the merged deadline must be the timeout's 1ms, not the inner's \
60s -- got {delta:?}"
);
}
#[derive(Debug)]
struct StagedForTest {
deadline: Option<Instant>,
io: Option<(RawSocket, Interest, Option<Instant>)>,
task: Option<i64>,
}
fn poll_once_and_read_staged_for_test(
fut: *mut u8,
pre_staged: Option<Wait>,
) -> (i64, StagedForTest) {
let poll_addr = unsafe { (fut as *mut usize).add(FUTURE_SLOT_POLL).read() };
let poll: PollFn = unsafe { std::mem::transmute(poll_addr) };
let state = state_of(fut) as *mut u8;
let previous = current_task();
set_current_for_test(Some(0));
if let Some(wait) = pre_staged {
stage_park(wait);
}
let status = unsafe { poll(state, std::ptr::null_mut()) };
let staged = StagedForTest {
deadline: staged_deadline_for_test(),
io: staged_io_for_test(),
task: staged_task_for_test(),
};
PENDING_PARK.with(|cell| {
cell.take();
});
set_current_for_test(previous);
(status, staged)
}
unsafe extern "C-unwind" fn poll_parks_on_a_task_forever(
_state: *mut u8,
_task_ctx: *mut u8,
) -> i64 {
stage_park(Wait::Task {
id: 0,
deadline: None,
});
POLL_PENDING
}
unsafe extern "C-unwind" fn poll_parks_on_an_io_forever(
_state: *mut u8,
_task_ctx: *mut u8,
) -> i64 {
stage_park(Wait::Io {
socket: RawSocket(9),
interest: Interest::Read,
deadline: None,
});
POLL_PENDING
}
unsafe extern "C-unwind" fn poll_parks_then_completes(
state: *mut u8,
_task_ctx: *mut u8,
) -> i64 {
stage_park(Wait::Task {
id: 0,
deadline: None,
});
unsafe { (state as *mut i64).add(STATE_SLOT_OUTPUT).write(0) };
POLL_READY
}
#[test]
fn a_live_timeout_over_an_io_parking_inner_stages_the_io_wait_and_the_deadline() {
let inner = make_future(poll_parks_on_an_io_forever, 0);
let before = Instant::now();
let fut = nova_rt_task_timeout_future(5 * 1_000_000_000, inner);
let (status, staged) = poll_once_and_read_staged_for_test(fut, None);
assert_eq!(
status, POLL_PENDING,
"neither the inner nor the 5s deadline is done"
);
let (socket, interest, io_deadline) = staged
.io
.expect("the inner's I/O wait must survive into the staged park");
assert_eq!((socket, interest), (RawSocket(9), Interest::Read));
let io_deadline = io_deadline.expect(
"the timeout's own deadline must ride *inside* the inner's I/O \
wait -- one task, one PARKED entry",
);
assert_eq!(
staged.deadline,
Some(io_deadline),
"the deadline `staged_io_for_test` reports and the one \
`staged_deadline_for_test` reports are the same single deadline"
);
let delta = io_deadline.duration_since(before);
assert!(
delta >= Duration::from_secs(4) && delta <= Duration::from_secs(30),
"a 5s timeout staged a deadline {delta:?} out -- a unit error, \
not jitter"
);
assert_eq!(
staged.task, None,
"nothing staged a task wait, so this must be empty"
);
}
#[test]
fn an_elapsed_timeout_discards_the_park_its_abandoned_inner_staged() {
let inner = make_future(poll_parks_on_a_task_forever, 0);
let fut = nova_rt_task_timeout_future(0, inner);
let (status, staged) = poll_once_and_read_staged_for_test(fut, None);
assert_eq!(status, POLL_READY, "a zero-duration timeout is already due");
assert_eq!(
output_of_for_test(fut),
TIMEOUT_STATUS_ELAPSED,
"the inner never completes, so this is the elapsed exit"
);
assert_eq!(
staged.task, None,
"the abandoned inner's task wait must not survive this poll: the \
next suspension in this same task poll would collide with it and \
abort the process"
);
assert_eq!(staged.deadline, None, "nothing staged a deadline");
assert!(staged.io.is_none(), "nothing staged an I/O wait");
}
#[test]
fn an_elapsed_timeout_discards_an_io_park_its_abandoned_inner_staged() {
let inner = make_future(poll_parks_on_an_io_forever, 0);
let fut = nova_rt_task_timeout_future(0, inner);
let (status, staged) = poll_once_and_read_staged_for_test(fut, None);
assert_eq!(status, POLL_READY, "a zero-duration timeout is already due");
assert_eq!(output_of_for_test(fut), TIMEOUT_STATUS_ELAPSED);
assert!(
staged.io.is_none(),
"the abandoned inner's I/O wait must not survive this poll either \
-- got {:?}",
staged.io
);
assert_eq!(staged.task, None, "and nothing staged a task wait");
assert_eq!(staged.deadline, None, "and nothing staged a deadline");
}
#[test]
fn a_completed_timeout_discards_a_park_its_inner_staged_before_completing() {
let inner = make_future(poll_parks_then_completes, 0);
let fut = nova_rt_task_timeout_future(30 * 1_000_000_000, inner);
let (status, staged) = poll_once_and_read_staged_for_test(fut, None);
assert_eq!(status, POLL_READY, "the inner completed");
assert_eq!(
output_of_for_test(fut),
TIMEOUT_STATUS_COMPLETED,
"the inner beat the 30s deadline"
);
assert_eq!(
staged.task, None,
"a park staged by a poll that then completed must not survive \
this poll either"
);
}
#[test]
fn an_elapsed_timeout_restores_the_park_staged_before_it_rather_than_clearing() {
let already_staged = Instant::now() + Duration::from_secs(30);
let inner = make_future(poll_parks_on_a_task_forever, 0);
let fut = nova_rt_task_timeout_future(0, inner);
let (status, staged) =
poll_once_and_read_staged_for_test(fut, Some(Wait::Deadline(already_staged)));
assert_eq!(status, POLL_READY, "a zero-duration timeout is already due");
assert_eq!(output_of_for_test(fut), TIMEOUT_STATUS_ELAPSED);
assert_eq!(
staged.task, None,
"the abandoned inner's task wait must be gone"
);
assert_eq!(
staged.deadline,
Some(already_staged),
"a park staged before this timeout ran must survive it -- \
clearing to Staged::default() would discard a park poll_timeout \
never staged, and this is the assertion that says so"
);
}
#[test]
fn a_timeouts_inner_future_survives_a_root_set_sweep_of_only_its_own_state() {
let inner = nova_rt_task_sleep_future_nanos(60 * 1_000_000_000);
let inner_state = state_of(inner);
let fut = nova_rt_task_timeout_future(60 * 1_000_000_000, inner);
let fut_state = state_of(fut);
assert_eq!(
gc::root_count(inner_state) + gc::root_count(fut_state),
0,
"build_future's add_root/remove_root pairing left a root pinned \
on one of these states; this test would pass regardless of \
TIMEOUT_SLOT_INNER if PINNED were consulted here (it is not,\
but this state should never arise regardless)"
);
gc::sweep_with_roots_for_test(&[fut_state]);
assert!(
gc::object_info(inner_state).is_some(),
"the inner state must stay live: with only the timeout's own \
state in the root set, the inner state is reachable through \
nothing but the fat pointer TIMEOUT_SLOT_INNER stores"
);
}
#[test]
fn the_join_futures_layout_is_the_one_the_abi_declares() {
let target = make_future(poll_ready_now, 0);
unsafe { nova_rt_task_spawn(target) };
let fut = unsafe { nova_rt_task_join_future(target) };
assert_eq!(
gc::object_info(fut as usize),
Some((FUTURE_SIZE, true)),
"the fat pointer must be exactly the two-word future, scanned"
);
let state = state_of(fut);
assert_eq!(
gc::object_info(state),
Some((JOIN_STATE_SIZE, true)),
"the state object must be the ABI minimum plus the one temp slot \
holding the target task id, scanned"
);
let poll = unsafe { (fut as *mut usize).add(FUTURE_SLOT_POLL).read() };
let expected: PollFn = poll_join;
assert_eq!(
poll, expected as usize,
"word 0 must be the poll function's address, not the state's"
);
}
#[test]
fn a_build_future_result_has_the_layout_the_abi_declares() {
let fut = test_future(poll_ready_now);
assert_eq!(
gc::object_info(fut as usize),
Some((FUTURE_SIZE, true)),
"the fat pointer must be exactly the two-word future, scanned"
);
let state = state_of(fut);
assert_eq!(
gc::object_info(state),
Some((STATE_MIN_SIZE, true)),
"the state object must be at least the tag and output slots, scanned"
);
}
#[test]
fn the_yield_future_pends_once_then_completes() {
let fut = nova_rt_task_yield_future();
let (poll, state) = unsafe { read_future(fut) };
assert_eq!(
unsafe { poll(state, std::ptr::null_mut()) },
POLL_PENDING,
"the first poll must suspend, or `yield_now` never yields"
);
assert_eq!(
unsafe { poll(state, std::ptr::null_mut()) },
POLL_READY,
"the second poll must complete, or `yield_now` never returns"
);
}
#[test]
fn two_yield_futures_do_not_share_a_resume_tag() {
let a = nova_rt_task_yield_future();
let b = nova_rt_task_yield_future();
assert_ne!(
state_of(a),
state_of(b),
"each call allocates its own state"
);
let (poll_a, state_a) = unsafe { read_future(a) };
let (poll_b, state_b) = unsafe { read_future(b) };
assert_eq!(
unsafe { poll_a(state_a, std::ptr::null_mut()) },
POLL_PENDING
);
assert_eq!(
unsafe { poll_b(state_b, std::ptr::null_mut()) },
POLL_PENDING,
"polling one yield future must not advance another's tag"
);
}
#[test]
fn a_task_awaiting_a_yield_future_resumes_on_its_next_turn() {
unsafe extern "C-unwind" fn poll_awaits_yield(state: *mut u8, ctx: *mut u8) -> i64 {
let slots = state as *mut i64;
let mut inner = unsafe { slots.add(STATE_SLOT_TEMPS).read() };
if inner == 0 {
inner = nova_rt_task_yield_future() as i64;
unsafe { slots.add(STATE_SLOT_TEMPS).write(inner) };
}
let (poll, inner_state) = unsafe { read_future(inner as *mut u8) };
let status = unsafe { poll(inner_state, ctx) };
if status == POLL_PENDING {
return POLL_PENDING;
}
unsafe { slots.add(STATE_SLOT_OUTPUT).write(11) };
POLL_READY
}
let fut = make_future(poll_awaits_yield, 1);
assert_eq!(
unsafe { nova_rt_task_block_on(fut) },
11,
"the executor must re-poll a task that awaited a yield future"
);
}
unsafe extern "C-unwind" fn poll_park_once(state: *mut u8, _task_ctx: *mut u8) -> i64 {
let slots = state as *mut i64;
let tag = unsafe { slots.add(STATE_SLOT_TAG).read() };
if tag == 0 {
unsafe { slots.add(STATE_SLOT_TAG).write(1) };
stage_park(Wait::Deadline(Instant::now()));
return POLL_PENDING;
}
unsafe { slots.add(STATE_SLOT_OUTPUT).write(0) };
POLL_READY
}
#[test]
fn a_staged_park_moves_the_task_out_of_the_ready_queue() {
let fut = test_future(poll_park_once);
let _id = unsafe { spawn_internal(fut) };
assert_eq!(QUEUE.with(|q| q.borrow().len()), 1, "spawn should queue it");
let id = QUEUE.with(|q| q.borrow_mut().pop_front()).expect("queued");
unsafe { poll_one(id) };
assert_eq!(
QUEUE.with(|q| q.borrow().len()),
0,
"a parked task must NOT be re-queued -- that is the whole change"
);
assert_eq!(
PARKED.with(|p| p.borrow().len()),
1,
"it must be in the park set instead"
);
let _ = id;
}
unsafe extern "C-unwind" fn poll_deadline_then_io(state: *mut u8, _task_ctx: *mut u8) -> i64 {
let slots = state as *mut i64;
let tag = unsafe { slots.add(STATE_SLOT_TAG).read() };
if tag == 0 {
unsafe { slots.add(STATE_SLOT_TAG).write(1) };
stage_park(Wait::Deadline(Instant::now() + Duration::from_secs(30)));
stage_park(Wait::Io {
socket: RawSocket(9),
interest: Interest::Read,
deadline: None,
});
return POLL_PENDING;
}
unsafe { slots.add(STATE_SLOT_OUTPUT).write(0) };
POLL_READY
}
#[test]
fn a_deadline_and_an_io_wait_stage_together() {
let fut = test_future(poll_deadline_then_io);
let id = unsafe { spawn_internal(fut) };
let popped = QUEUE.with(|q| q.borrow_mut().pop_front());
assert_eq!(popped, Some(id), "spawn should have queued it");
unsafe { poll_one(id) };
let entries = PARKED.with(|p| p.borrow().clone());
assert_eq!(
entries.len(),
1,
"a deadline staged alongside an I/O wait must fold into ONE \
PARKED entry, not two -- got: {entries:?}"
);
match entries[0].1 {
Wait::Io {
socket,
interest,
deadline,
} => {
assert_eq!(socket, RawSocket(9));
assert_eq!(interest, Interest::Read);
assert!(
deadline.is_some(),
"the separately-staged deadline must ride inside this \
Io wait"
);
}
other => panic!("expected a single Wait::Io entry, got {other:?}"),
}
PARKED.with(|p| p.borrow_mut().clear());
}
#[test]
fn two_io_waits_in_one_poll_still_abort() {
let first = Wait::Io {
socket: RawSocket(1),
interest: Interest::Read,
deadline: None,
};
let second = Wait::Io {
socket: RawSocket(2),
interest: Interest::Write,
deadline: None,
};
let staged =
try_stage(Staged::default(), first).expect("the first I/O wait stages cleanly");
let err =
try_stage(staged, second).expect_err("a second I/O wait in the same poll must collide");
assert!(err.contains("two parks staged in one poll"), "got: {err}");
assert!(
err.contains("deadline: None"),
"with no deadline staged, the reconstructed I/O wait must say so: {err}"
);
let staged = try_stage(
Staged::default(),
Wait::Deadline(Instant::now() + Duration::from_secs(5)),
)
.expect("a bare deadline stages");
let staged = try_stage(staged, first).expect("an I/O wait joins a staged deadline");
let err = try_stage(staged, second)
.expect_err("a second I/O wait must still collide, deadline or not");
assert!(
err.contains("deadline: Some("),
"the merged deadline must appear in the wait the diagnostic \
reconstructs: {err}"
);
}
#[test]
fn two_task_waits_in_one_poll_still_abort() {
let first = Wait::Task {
id: 1,
deadline: None,
};
let second = Wait::Task {
id: 2,
deadline: None,
};
let staged =
try_stage(Staged::default(), first).expect("the first task wait stages cleanly");
let err = try_stage(staged, second)
.expect_err("a second task wait in the same poll must collide");
assert!(err.contains("two parks staged in one poll"), "got: {err}");
assert!(
err.contains("Task { id: 1, deadline: None } then Task { id: 2"),
"the diagnostic must name the already-staged wait first and the \
incoming one second: {err}"
);
let at = Instant::now() + Duration::from_secs(5);
let staged =
try_stage(Staged::default(), Wait::Deadline(at)).expect("a bare deadline stages");
let staged = try_stage(staged, first).expect("a task wait joins a staged deadline");
let err = try_stage(staged, second)
.expect_err("a second task wait must still collide, deadline or not");
assert!(
err.contains("deadline: Some("),
"the merged deadline must appear in the wait the diagnostic \
reconstructs: {err}"
);
}
#[test]
fn a_task_wait_and_an_io_wait_in_one_poll_still_abort_in_either_order() {
let task = Wait::Task {
id: 3,
deadline: None,
};
let io = Wait::Io {
socket: RawSocket(4),
interest: Interest::Write,
deadline: None,
};
let at = Instant::now() + Duration::from_secs(5);
let staged = try_stage(Staged::default(), task).expect("the task wait stages cleanly");
let err = try_stage(staged, io).expect_err("an I/O wait must not join a staged task wait");
assert!(err.contains("two parks staged in one poll"), "got: {err}");
assert!(
err.contains("Task { id: 3, deadline: None } then Io {"),
"the diagnostic must reconstruct the staged task wait, then name \
the incoming I/O wait: {err}"
);
let staged =
try_stage(Staged::default(), Wait::Deadline(at)).expect("a bare deadline stages");
let staged = try_stage(staged, task).expect("a task wait joins a staged deadline");
let err = try_stage(staged, io)
.expect_err("an I/O wait must still collide with a timed task wait");
assert!(
err.contains("deadline: Some("),
"the merged deadline must ride in the reconstructed task wait: {err}"
);
let staged = try_stage(Staged::default(), io).expect("the I/O wait stages cleanly");
let err = try_stage(staged, task).expect_err("a task wait must not join a staged I/O wait");
assert!(err.contains("two parks staged in one poll"), "got: {err}");
assert!(
err.contains("Io { socket: RawSocket(4)"),
"the diagnostic must reconstruct the staged I/O wait with its own \
socket, not the incoming task wait: {err}"
);
assert!(
err.contains("then Task { id: 3"),
"and name the incoming task wait second: {err}"
);
let staged =
try_stage(Staged::default(), Wait::Deadline(at)).expect("a bare deadline stages");
let staged = try_stage(staged, io).expect("an I/O wait joins a staged deadline");
let err = try_stage(staged, task)
.expect_err("a task wait must still collide with a timed I/O wait");
assert!(
err.contains("deadline: Some("),
"the merged deadline must ride in the reconstructed I/O wait: {err}"
);
}
#[test]
fn two_deadlines_in_one_poll_merge_to_the_earlier() {
let base = Instant::now();
let soon = base + Duration::from_secs(1);
let later = base + Duration::from_secs(30);
let staged =
try_stage(Staged::default(), Wait::Deadline(later)).expect("first deadline stages");
let staged = try_stage(staged, Wait::Deadline(soon))
.expect("a second deadline must merge, not collide");
assert_eq!(
staged.deadline,
Some(soon),
"later-then-sooner must keep the earlier"
);
let staged =
try_stage(Staged::default(), Wait::Deadline(soon)).expect("first deadline stages");
let staged = try_stage(staged, Wait::Deadline(later))
.expect("a second deadline must merge, not collide");
assert_eq!(
staged.deadline,
Some(soon),
"sooner-then-later must keep the earlier"
);
}
#[test]
fn a_task_wait_and_a_deadline_co_stage_in_either_order() {
let at = Instant::now() + Duration::from_secs(5);
let staged = try_stage(
Staged::default(),
Wait::Task {
id: 7,
deadline: None,
},
)
.expect("task stages");
let staged = try_stage(staged, Wait::Deadline(at))
.expect("a deadline must join a task wait, not collide");
assert_eq!((staged.task, staged.deadline), (Some(7), Some(at)));
let staged = try_stage(Staged::default(), Wait::Deadline(at)).expect("deadline stages");
let staged = try_stage(
staged,
Wait::Task {
id: 7,
deadline: None,
},
)
.expect("a task wait must join a deadline, not collide");
assert_eq!((staged.task, staged.deadline), (Some(7), Some(at)));
}
#[test]
fn a_timed_io_wait_and_a_bare_deadline_merge_to_the_earlier_in_either_order() {
let base = Instant::now();
let soon = base + Duration::from_secs(1);
let later = base + Duration::from_secs(30);
let staged =
try_stage(Staged::default(), Wait::Deadline(soon)).expect("bare deadline stages");
let staged = try_stage(
staged,
Wait::Io {
socket: RawSocket(1),
interest: Interest::Read,
deadline: Some(later),
},
)
.expect("a timed Io wait must merge with a staged deadline, not collide");
assert_eq!(
staged.deadline,
Some(soon),
"deadline(soon)-then-timed-io(later) must keep the earlier"
);
let staged =
try_stage(Staged::default(), Wait::Deadline(later)).expect("bare deadline stages");
let staged = try_stage(
staged,
Wait::Io {
socket: RawSocket(1),
interest: Interest::Read,
deadline: Some(soon),
},
)
.expect("a timed Io wait must merge with a staged deadline, not collide");
assert_eq!(
staged.deadline,
Some(soon),
"deadline(later)-then-timed-io(soon) must keep the earlier"
);
let staged = try_stage(
Staged::default(),
Wait::Io {
socket: RawSocket(1),
interest: Interest::Read,
deadline: Some(soon),
},
)
.expect("timed Io stages");
let staged = try_stage(staged, Wait::Deadline(later))
.expect("a bare deadline must merge with a staged timed Io, not collide");
assert_eq!(
staged.deadline,
Some(soon),
"timed-io-then-deadline must keep the earlier"
);
}
#[test]
fn a_timed_task_wait_and_a_bare_deadline_merge_to_the_earlier_in_either_order() {
let base = Instant::now();
let soon = base + Duration::from_secs(1);
let later = base + Duration::from_secs(30);
let staged =
try_stage(Staged::default(), Wait::Deadline(soon)).expect("bare deadline stages");
let staged = try_stage(
staged,
Wait::Task {
id: 7,
deadline: Some(later),
},
)
.expect("a timed task wait must merge with a staged deadline, not collide");
assert_eq!(
staged.deadline,
Some(soon),
"deadline(soon)-then-timed-task(later) must keep the earlier"
);
let staged =
try_stage(Staged::default(), Wait::Deadline(later)).expect("bare deadline stages");
let staged = try_stage(
staged,
Wait::Task {
id: 7,
deadline: Some(soon),
},
)
.expect("a timed task wait must merge with a staged deadline, not collide");
assert_eq!(
staged.deadline,
Some(soon),
"deadline(later)-then-timed-task(soon) must keep the earlier"
);
let staged = try_stage(
Staged::default(),
Wait::Task {
id: 7,
deadline: Some(soon),
},
)
.expect("timed task stages");
let staged = try_stage(staged, Wait::Deadline(later))
.expect("a bare deadline must merge with a staged timed task, not collide");
assert_eq!(
staged.deadline,
Some(soon),
"timed-task-then-deadline must keep the earlier"
);
}
#[test]
fn a_deadline_park_is_woken_and_the_task_completes() {
let fut = test_future(poll_park_once);
let out = unsafe { run_to_completion(fut) };
assert_eq!(
out, 0,
"the parked task must be woken and run to completion"
);
assert!(
PARKED.with(|p| p.borrow().is_empty()),
"the park set must be empty once the loop exits"
);
}
unsafe extern "C-unwind" fn poll_park_after_delay(state: *mut u8, _task_ctx: *mut u8) -> i64 {
let slots = state as *mut i64;
let tag = unsafe { slots.add(STATE_SLOT_TAG).read() };
if tag == 0 {
unsafe { slots.add(STATE_SLOT_TAG).write(1) };
let delay_ms = unsafe { slots.add(STATE_SLOT_TEMPS).read() };
stage_park(Wait::Deadline(
Instant::now() + Duration::from_millis(delay_ms as u64),
));
return POLL_PENDING;
}
let sibling_id = unsafe { slots.add(STATE_SLOT_TEMPS + 1).read() };
let sibling_was_already_done = is_done_internal(sibling_id) as i64;
unsafe { slots.add(STATE_SLOT_OUTPUT).write(sibling_was_already_done) };
POLL_READY
}
#[test]
fn two_not_yet_due_deadlines_drain_the_queue_then_wake_in_deadline_order() {
const SHORT_DELAY_MS: i64 = 15;
const LONG_DELAY_MS: i64 = 300;
let long = make_future(poll_park_after_delay, 2);
let short = make_future(poll_park_after_delay, 2);
unsafe {
(state_of(long) as *mut i64)
.add(STATE_SLOT_TEMPS)
.write(LONG_DELAY_MS);
(state_of(short) as *mut i64)
.add(STATE_SLOT_TEMPS)
.write(SHORT_DELAY_MS);
}
let long_id = unsafe { spawn_internal(long) };
let short_id = unsafe { spawn_internal(short) };
unsafe {
(state_of(long) as *mut i64)
.add(STATE_SLOT_TEMPS + 1)
.write(short_id);
(state_of(short) as *mut i64)
.add(STATE_SLOT_TEMPS + 1)
.write(long_id);
}
let root = test_future(poll_ready_now);
assert_eq!(
unsafe { run_to_completion(root) },
7,
"the root task must still complete normally"
);
assert!(
PARKED.with(|p| p.borrow().is_empty()),
"both deadline-parked tasks must be woken, not left parked -- an \
empty park set here is exactly what distinguishes this from the \
deadlock this same code path would otherwise report"
);
assert!(
is_done_internal(long_id) && is_done_internal(short_id),
"both tasks must run to completion"
);
assert_eq!(
TASKS.with(|t| t.borrow()[short_id as usize].output),
0,
"the short deadline must wake and complete before the long one, \
so it must not see the long task already done"
);
assert_eq!(
TASKS.with(|t| t.borrow()[long_id as usize].output),
1,
"the long deadline must wake after the short one -- deadline \
order, not spawn order, since long was spawned first -- so it \
must see the short task already done"
);
}
unsafe extern "C-unwind" fn poll_park_on_timed_task_wait(
state: *mut u8,
_task_ctx: *mut u8,
) -> i64 {
let slots = state as *mut i64;
let tag = unsafe { slots.add(STATE_SLOT_TAG).read() };
if tag == 0 {
unsafe { slots.add(STATE_SLOT_TAG).write(1) };
stage_park(Wait::Task {
id: 999,
deadline: Some(Instant::now() + Duration::from_millis(15)),
});
return POLL_PENDING;
}
unsafe { slots.add(STATE_SLOT_OUTPUT).write(0) };
POLL_READY
}
#[test]
fn a_lone_timed_task_wait_sleeps_instead_of_deadlocking() {
let fut = test_future(poll_park_on_timed_task_wait);
let out = unsafe { run_to_completion(fut) };
assert_eq!(
out, 0,
"a lone timed task wait must be woken by its own deadline and \
complete, not deadlock"
);
assert!(
PARKED.with(|p| p.borrow().is_empty()),
"the park set must be empty once the loop exits"
);
}
#[test]
fn the_earliest_deadline_is_the_one_slept_on() {
let far = Instant::now() + Duration::from_secs(30);
let near = Instant::now();
PARKED.with(|p| {
let mut p = p.borrow_mut();
p.push((7, Wait::Deadline(far)));
p.push((8, Wait::Deadline(near)));
});
assert_eq!(
earliest_deadline(),
Some(near),
"sleeping on the later deadline would strand the earlier task"
);
PARKED.with(|p| p.borrow_mut().clear());
}
#[test]
fn wake_due_wakes_a_task_wait_whose_deadline_elapsed() {
PARKED.with(|p| p.borrow_mut().clear());
QUEUE.with(|q| q.borrow_mut().clear());
let past = Instant::now();
PARKED.with(|p| {
p.borrow_mut().push((
41,
Wait::Task {
id: 999,
deadline: Some(past),
},
));
});
wake_due(past + Duration::from_millis(1));
let queued = QUEUE.with(|q| q.borrow().contains(&41));
assert!(
queued,
"a timed task wait whose deadline passed must be re-queued"
);
assert!(
PARKED.with(|p| p.borrow().is_empty()),
"and it must be removed from PARKED, not woken twice"
);
PARKED.with(|p| p.borrow_mut().clear());
QUEUE.with(|q| q.borrow_mut().clear());
}
#[test]
fn earliest_deadline_counts_a_timed_task_wait_and_ignores_a_bare_one() {
PARKED.with(|p| p.borrow_mut().clear());
let base = Instant::now();
let soon = base + Duration::from_secs(1);
let later = base + Duration::from_secs(30);
PARKED.with(|p| {
let mut p = p.borrow_mut();
p.push((
1,
Wait::Task {
id: 999,
deadline: None,
},
));
p.push((
2,
Wait::Task {
id: 998,
deadline: Some(later),
},
));
p.push((3, Wait::Deadline(soon)));
});
assert_eq!(earliest_deadline(), Some(soon));
PARKED.with(|p| p.borrow_mut().clear());
PARKED.with(|p| {
let mut p = p.borrow_mut();
p.push((
1,
Wait::Task {
id: 999,
deadline: None,
},
));
p.push((
2,
Wait::Task {
id: 998,
deadline: Some(later),
},
));
});
assert_eq!(
earliest_deadline(),
Some(later),
"a timed task wait is the only deadline here and must be found"
);
PARKED.with(|p| p.borrow_mut().clear());
}
#[test]
fn wake_due_wakes_a_due_io_wait_and_leaves_the_rest_parked() {
let due = Instant::now();
let not_due = due + Duration::from_secs(30);
PARKED.with(|p| {
let mut p = p.borrow_mut();
p.push((
1,
Wait::Io {
socket: RawSocket(1),
interest: Interest::Read,
deadline: Some(due),
},
));
p.push((
2,
Wait::Io {
socket: RawSocket(2),
interest: Interest::Write,
deadline: Some(not_due),
},
));
p.push((
3,
Wait::Io {
socket: RawSocket(3),
interest: Interest::Read,
deadline: None,
},
));
});
wake_due(Instant::now());
assert_eq!(
QUEUE.with(|q| q.borrow().clone()),
std::collections::VecDeque::from([1]),
"only the due I/O wait's task must be woken"
);
assert_eq!(
PARKED.with(|p| p.borrow().clone()),
vec![
(
2,
Wait::Io {
socket: RawSocket(2),
interest: Interest::Write,
deadline: Some(not_due),
}
),
(
3,
Wait::Io {
socket: RawSocket(3),
interest: Interest::Read,
deadline: None,
}
),
],
"the not-yet-due and the untimed I/O waits must stay parked"
);
PARKED.with(|p| p.borrow_mut().clear());
QUEUE.with(|q| q.borrow_mut().clear());
}
#[test]
fn wake_ready_wakes_only_the_task_whose_socket_is_ready() {
let ready_sock = RawSocket(11);
let idle_sock = RawSocket(22);
PARKED.with(|p| {
let mut p = p.borrow_mut();
p.push((
1,
Wait::Io {
socket: ready_sock,
interest: Interest::Read,
deadline: None,
},
));
p.push((
2,
Wait::Io {
socket: idle_sock,
interest: Interest::Read,
deadline: None,
},
));
});
wake_ready(vec![ready_sock]);
assert_eq!(
QUEUE.with(|q| q.borrow().clone()),
std::collections::VecDeque::from([1]),
"only the task parked on the socket poll::wait reported ready \
must be woken"
);
assert_eq!(
PARKED.with(|p| p.borrow().clone()),
vec![(
2,
Wait::Io {
socket: idle_sock,
interest: Interest::Read,
deadline: None,
}
)],
"a task parked on a socket that was not reported ready must stay \
parked"
);
PARKED.with(|p| p.borrow_mut().clear());
QUEUE.with(|q| q.borrow_mut().clear());
}
const SPIN_TURNS: i64 = 5;
unsafe extern "C-unwind" fn poll_spin_n_times(state: *mut u8, _task_ctx: *mut u8) -> i64 {
let slots = state as *mut i64;
let turns = unsafe { slots.add(STATE_SLOT_TAG).read() };
if turns < SPIN_TURNS {
unsafe { slots.add(STATE_SLOT_TAG).write(turns + 1) };
return POLL_PENDING;
}
unsafe { slots.add(STATE_SLOT_OUTPUT).write(0) };
POLL_READY
}
unsafe extern "C-unwind" fn poll_park_then_read_sibling_progress(
state: *mut u8,
_task_ctx: *mut u8,
) -> i64 {
let slots = state as *mut i64;
let tag = unsafe { slots.add(STATE_SLOT_TAG).read() };
if tag == 0 {
unsafe { slots.add(STATE_SLOT_TAG).write(1) };
stage_park(Wait::Deadline(Instant::now()));
return POLL_PENDING;
}
let sibling = unsafe { slots.add(STATE_SLOT_TEMPS).read() } as *mut i64;
let sibling_progress = unsafe { sibling.add(STATE_SLOT_TAG).read() };
unsafe { slots.add(STATE_SLOT_OUTPUT).write(sibling_progress) };
POLL_READY
}
#[test]
fn a_self_requeuing_task_does_not_starve_a_sibling_parked_on_a_deadline() {
let spinner = test_future(poll_spin_n_times);
let (_, spinner_state) = unsafe { read_future(spinner) };
let sleeper = make_future(poll_park_then_read_sibling_progress, 1);
let (_, sleeper_state) = unsafe { read_future(sleeper) };
unsafe {
(sleeper_state as *mut i64)
.add(STATE_SLOT_TEMPS)
.write(spinner_state as i64);
}
let spinner_id = unsafe { spawn_internal(spinner) };
let sleeper_id = unsafe { spawn_internal(sleeper) };
let root = test_future(poll_ready_now);
assert_eq!(
unsafe { run_to_completion(root) },
7,
"the root task must still complete normally"
);
assert!(
is_done_internal(sleeper_id),
"the deadline-parked task must have been woken and completed"
);
let sibling_progress_at_wake =
TASKS.with(|tasks| tasks.borrow()[sleeper_id as usize].output);
assert!(
sibling_progress_at_wake < SPIN_TURNS,
"the parked task must be woken while the spinner still has \
pending turns left ({sibling_progress_at_wake} of {SPIN_TURNS} \
turns spent) -- {sibling_progress_at_wake} >= {SPIN_TURNS} means \
it was only woken once the spinner had already finished on its \
own, which is the starvation bug this test exists to catch"
);
assert!(
is_done_internal(spinner_id),
"run_to_completion must not return until the spinner also \
finishes, regardless of when the sleeper was woken"
);
}
#[test]
fn joining_an_already_done_task_completes_without_parking() {
let target = make_future(poll_ready_now, 0);
let target_id = unsafe { spawn_internal(target) };
let popped_target = QUEUE.with(|q| q.borrow_mut().pop_front());
assert_eq!(
popped_target,
Some(target_id),
"spawn should have queued the target"
);
unsafe { poll_one(target_id) };
assert!(
is_done_internal(target_id),
"target must be done before the join below means anything"
);
let join_fut = unsafe { nova_rt_task_join_future(target) };
let joiner_id = unsafe { spawn_internal(join_fut) };
let popped = QUEUE.with(|q| q.borrow_mut().pop_front());
assert_eq!(
popped,
Some(joiner_id),
"spawn should have queued the joiner"
);
unsafe { poll_one(joiner_id) };
assert!(
is_done_internal(joiner_id),
"joining an already-finished task must complete on the first \
poll, not park"
);
assert!(
PARKED.with(|p| p.borrow().is_empty()),
"no park may be staged on the immediate-ready path"
);
}
#[test]
fn joining_a_pending_task_parks_and_is_woken_when_the_target_finishes() {
let target = make_future(poll_ready_now, 0);
let target_id = unsafe { spawn_internal(target) };
let join_fut = unsafe { nova_rt_task_join_future(target) };
let joiner_id = unsafe { spawn_internal(join_fut) };
assert_eq!(
QUEUE.with(|q| q.borrow_mut().drain(..).collect::<Vec<_>>()),
vec![target_id, joiner_id],
"spawn should have queued both, in order"
);
unsafe { poll_one(joiner_id) };
assert!(
!is_done_internal(joiner_id),
"the joiner must not complete while its target is still pending"
);
assert_eq!(
PARKED.with(|p| p.borrow().clone()),
vec![(
joiner_id,
Wait::Task {
id: target_id,
deadline: None
}
)],
"the joiner must be parked on the target's id, not re-queued"
);
assert!(
QUEUE.with(|q| q.borrow().is_empty()),
"a parked task must not also sit in the ready queue"
);
unsafe { poll_one(target_id) };
assert!(
is_done_internal(target_id),
"poll_ready_now completes on its first poll"
);
assert!(
PARKED.with(|p| p.borrow().is_empty()),
"the target's completion must wake the joiner out of the park set"
);
let woken = QUEUE.with(|q| q.borrow_mut().pop_front());
assert_eq!(
woken,
Some(joiner_id),
"the woken joiner must be the one re-queued"
);
unsafe { poll_one(joiner_id) };
assert!(
is_done_internal(joiner_id),
"once woken, the joiner must complete -- its target is now done"
);
}
#[test]
fn a_deadlock_names_every_parked_task_and_its_reason() {
PARKED.with(|p| {
let mut p = p.borrow_mut();
p.push((
1,
Wait::Task {
id: 2,
deadline: None,
},
));
p.push((
2,
Wait::Task {
id: 1,
deadline: None,
},
));
});
let report = deadlock_report();
assert!(report.contains("2 tasks are parked"), "got: {report}");
assert!(
report.contains("task 1 is waiting for task 2 to finish"),
"every parked task must be named, not just the first -- got: {report}"
);
assert!(
report.contains("task 2 is waiting for task 1 to finish"),
"the second parked task is missing -- got: {report}"
);
PARKED.with(|p| p.borrow_mut().clear());
}
#[test]
fn a_task_parked_on_itself_is_reported_as_a_deadlock() {
PARKED.with(|p| {
p.borrow_mut().push((
3,
Wait::Task {
id: 3,
deadline: None,
},
))
});
let report = deadlock_report();
assert!(
report.contains("task 3 is waiting for task 3 to finish"),
"got: {report}"
);
assert!(
report.contains("1 task is"),
"singular headline -- got: {report}"
);
PARKED.with(|p| p.borrow_mut().clear());
}
#[test]
fn a_park_on_io_with_no_deadline_is_not_a_deadlock() {
let report = with_parked(
&[(
7,
Wait::Io {
socket: RawSocket(-1),
interest: Interest::Read,
deadline: None,
},
)],
deadlock_report,
);
assert!(
report.contains("waiting on i/o"),
"an I/O park must describe itself, got: {report}"
);
}
unsafe extern "C-unwind" fn poll_stage_then_ready(state: *mut u8, _task_ctx: *mut u8) -> i64 {
let slots = state as *mut i64;
stage_park(Wait::Task {
id: 0,
deadline: None,
});
unsafe { slots.add(STATE_SLOT_OUTPUT).write(0) };
POLL_READY
}
#[test]
fn a_park_staged_by_a_poll_that_completes_is_discarded() {
let fut = test_future(poll_stage_then_ready);
let out = unsafe { run_to_completion(fut) };
assert_eq!(out, 0);
assert!(
PARKED.with(|p| p.borrow().is_empty()),
"a completed task must not be left parked -- it would fake a deadlock forever"
);
}
#[test]
fn a_discarded_park_does_not_leak_onto_the_next_task_polled() {
let a = test_future(poll_stage_then_ready);
let b = test_future(poll_suspend_once);
let (id_a, id_b) = unsafe { (spawn_internal(a), spawn_internal(b)) };
assert_eq!(
QUEUE.with(|q| q.borrow_mut().drain(..).collect::<Vec<_>>()),
vec![id_a, id_b],
"spawn should have queued both, in order"
);
unsafe {
poll_one(id_a);
poll_one(id_b);
}
assert!(
PARKED.with(|p| p.borrow().is_empty()),
"b never called stage_park and returned POLL_PENDING on its own; \
it must be re-queued, not parked on a's leftover wait -- got: {:?}",
PARKED.with(|p| p.borrow().clone())
);
assert_eq!(
QUEUE.with(|q| q.borrow().clone()),
std::collections::VecDeque::from([id_b]),
"b must be the one task left in the ready queue"
);
}
mod root_registration {
use super::*;
#[inline(never)]
fn hide(addr: usize) -> usize {
!addr
}
#[inline(never)]
fn reveal(hidden: usize) -> usize {
!hidden
}
#[inline(never)]
fn setup_task_state(spawn: bool) -> usize {
let mut fut = make_future(poll_suspend_once, 0);
let mut state = state_of(fut);
let hidden = hide(state);
if spawn {
unsafe {
nova_rt_task_spawn(fut);
}
}
fut = std::ptr::null_mut::<u8>();
state = 0;
std::hint::black_box(fut);
std::hint::black_box(state);
hidden
}
#[test]
#[ignore = "not flaky on its own -- never observed to fail by itself. \
Gated only to stay paired with the sweep-asserting control \
test that does flake: this one asserts an object SURVIVED, \
which also passes against a collector that frees nothing at \
all, so running it while its control is ignored would leave \
a green assertion proving nothing. See \
docs/adr/0010-conservative-scan-root-test-gating.md. \
Reachable with `cargo test -- --ignored`."]
fn a_spawned_tasks_state_is_registered_as_a_gc_root() {
let hidden = setup_task_state(true);
gc::collect_for_test();
let state = reveal(hidden);
assert!(
gc::object_info(state).is_some(),
"a parked task's state object was swept; add_root is not wired"
);
}
#[test]
#[ignore = "flaky: asserts an unreachable object was SWEPT, and the \
conservative stack scan intermittently retains it instead \
-- a stale stack word in an already-returned frame is read \
as a root, in debug as well as release, at default test \
parallelism. Mechanism identified, not fixed; two remedies \
were each measured to make it worse. See \
docs/adr/0010-conservative-scan-root-test-gating.md. \
Reachable with `cargo test -- --ignored`."]
fn an_unspawned_tasks_state_is_swept() {
let hidden = setup_task_state(false);
gc::collect_for_test();
let state = reveal(hidden);
assert!(
gc::object_info(state).is_none(),
"an unregistered, unreachable task state survived a collection; \
this test cannot discriminate a working registry from a \
collector that frees nothing"
);
}
}
}