#![allow(clippy::type_complexity)]
#![allow(clippy::return_self_not_must_use)]
use std::{
any::Any,
cell,
collections::{HashMap, VecDeque},
rc,
sync::{
self,
atomic::{self, AtomicBool, AtomicUsize, Ordering},
Arc,
},
time,
};
use crate::{
synca::{mpp, DurationWaker, Entry, EntryList, IdleMan, OnSignal, Sleepers, Waiter},
valtron::{
AnyResult, EventReadinessPtr, ExecutionEngine, ExecutionIterator, NoAction, State,
TaskIterator, TaskStatus,
},
};
use crate::{
synca::{Timeable, Timing},
valtron::{DualSequeunceChildAndParentLinkedTask, FinishChildBeforeParentTask},
};
use foundation_compact::rng::ChaCha8Rng;
use foundation_compact::rng::SeedableRng;
use concurrent_queue::{ConcurrentQueue, PopError, PushError};
#[allow(unused)]
use crate::compati::Mutex;
use crate::valtron::{
BoxedExecutionEngine, BoxedExecutionIterator, ExecutionAction, ExecutorError, GlobalTask,
ProcessController, SharedTaskQueue, SpawnInfo, SpawnType, TaskReadyResolver, TaskSpawnConfig,
};
use crate::valtron::executors::constants::{
DEFAULT_KILL_SIGNAL_CHECK_INTERVAL, DEFAULT_NOTIFY_QUEUE_MAX_SPINS, DEFAULT_READINESS_WAIT,
};
pub struct WrapTask<I, T>
where
I: Iterator<Item = T>,
{
iter: I,
}
impl<I, T> WrapTask<I, T>
where
I: Iterator<Item = T>,
{
pub fn new(iter: I) -> Self {
Self { iter }
}
}
impl<I, T> TaskIterator for WrapTask<I, T>
where
I: Iterator<Item = T>,
{
type Pending = ();
type Ready = T;
type Spawner = NoAction;
fn next_status(&mut self) -> Option<TaskStatus<Self::Ready, Self::Pending, Self::Spawner>> {
self.iter.next().map(TaskStatus::Ready)
}
}
const MAX_YIELD_DURATION: time::Duration = time::Duration::from_millis(100);
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct ThreadId(Entry, String);
impl ThreadId {
#[must_use]
pub fn new(entry: Entry, name: String) -> Self {
Self(entry, name)
}
pub fn get_mut(&mut self) -> &mut Entry {
&mut self.0
}
#[must_use]
pub fn get_ref(&self) -> &Entry {
&self.0
}
#[must_use]
pub fn get_cloned(&self) -> Entry {
self.0
}
#[must_use]
pub fn get_name(&self) -> &String {
&self.1
}
}
pub enum ThreadActivity {
Started(ThreadId),
Stopped(ThreadId),
Blocked(ThreadId),
Unblocked(ThreadId),
Parked(ThreadId),
Unparked(ThreadId),
Panicked(ThreadId, Box<dyn Any + Send>),
BroadcastedTask,
}
impl core::fmt::Display for ThreadActivity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ThreadActivity::Panicked(id, _) => write!(f, "ThreadActivity::Panicked({id:?})"),
ThreadActivity::Started(id) => write!(f, "ThreadActivity::Started({id:?})"),
ThreadActivity::Stopped(id) => write!(f, "ThreadActivity::Stopped({id:?})"),
ThreadActivity::Blocked(id) => write!(f, "ThreadActivity::Blocked({id:?})"),
ThreadActivity::Unblocked(id) => write!(f, "ThreadActivity::Unblocked({id:?})"),
ThreadActivity::Parked(id) => write!(f, "ThreadActivity::Parked({id:?})"),
ThreadActivity::Unparked(id) => write!(f, "ThreadActivity::Unparked({id:?})"),
ThreadActivity::BroadcastedTask => write!(f, "ThreadActivity::BroadcastedTask"),
}
}
}
impl core::fmt::Debug for ThreadActivity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ThreadActivity::Panicked(id, _) => write!(f, "ThreadActivity::Panicked({id:?})"),
ThreadActivity::Started(id) => write!(f, "ThreadActivity::Started({id:?})"),
ThreadActivity::Stopped(id) => write!(f, "ThreadActivity::Stopped({id:?})"),
ThreadActivity::Blocked(id) => write!(f, "ThreadActivity::Blocked({id:?})"),
ThreadActivity::Unblocked(id) => write!(f, "ThreadActivity::Unblocked({id:?})"),
ThreadActivity::Parked(id) => write!(f, "ThreadActivity::Parked({id:?})"),
ThreadActivity::Unparked(id) => write!(f, "ThreadActivity::Unparked({id:?})"),
ThreadActivity::BroadcastedTask => write!(f, "ThreadActivity::BroadcastedTask"),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PriorityOrder {
Top,
Bottom,
}
pub enum Sleepable {
Timable(DurationWaker<Entry>),
Atomic(sync::Arc<AtomicBool>, Entry),
Readiness(EventReadinessPtr, Entry),
}
impl core::fmt::Debug for Sleepable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Timable(inner) => f.debug_tuple("Timable").field(inner).finish(),
Self::Atomic(_, entry) => f.debug_tuple("Atomic").field(entry).finish(),
Self::Readiness(_, entry) => f.debug_tuple("Readiness").field(entry).finish(),
}
}
}
impl Sleepable {
pub fn deadline(&self) -> Option<time::Instant> {
match self {
Sleepable::Timable(inner) => Some(inner.deadline()),
Sleepable::Atomic(_, _) | Sleepable::Readiness(_, _) => None,
}
}
}
impl Timeable for Sleepable {
fn remaining_duration(&self) -> Option<time::Duration> {
match self {
Sleepable::Timable(inner) => inner.remaining(),
Sleepable::Atomic(_, _) | Sleepable::Readiness(_, _) => None,
}
}
}
impl Waiter for Sleepable {
fn is_ready(&self, dur: Option<time::Duration>) -> bool {
match self {
Sleepable::Timable(inner) => inner.is_ready(dur),
Sleepable::Atomic(inner, _) => inner.load(atomic::Ordering::SeqCst),
Sleepable::Readiness(signal, _) => signal.is_ready(dur),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum NotificationItem<T> {
Ready(T),
None,
}
#[derive(Debug)]
pub struct NotifyQueue<T> {
queue: Arc<ConcurrentQueue<T>>,
condvar: Arc<foundation_nostd::comp::condvar_comp::CondVar>,
mutex: Arc<foundation_nostd::comp::condvar_comp::Mutex<bool>>,
max_spins: AtomicUsize,
}
impl<T> Clone for NotifyQueue<T> {
fn clone(&self) -> Self {
Self {
queue: self.queue.clone(),
condvar: self.condvar.clone(),
mutex: self.mutex.clone(),
max_spins: AtomicUsize::new(self.max_spins.load(Ordering::Relaxed)),
}
}
}
impl<T> NotifyQueue<T> {
pub fn unbounded() -> Self {
Self {
queue: Arc::new(ConcurrentQueue::unbounded()),
condvar: Arc::new(foundation_nostd::comp::condvar_comp::CondVar::new()),
mutex: Arc::new(foundation_nostd::comp::condvar_comp::Mutex::new(false)),
max_spins: AtomicUsize::new(DEFAULT_NOTIFY_QUEUE_MAX_SPINS),
}
}
pub fn bounded(capacity: usize) -> Self {
Self {
queue: Arc::new(ConcurrentQueue::bounded(capacity)),
condvar: Arc::new(foundation_nostd::comp::condvar_comp::CondVar::new()),
mutex: Arc::new(foundation_nostd::comp::condvar_comp::Mutex::new(false)),
max_spins: AtomicUsize::new(DEFAULT_NOTIFY_QUEUE_MAX_SPINS),
}
}
pub fn set_max_spins(&self, max_spins: usize) {
self.max_spins.store(max_spins, Ordering::Relaxed);
}
pub fn push(&self, item: T) -> Result<(), PushError<T>> {
self.queue.push(item)?;
let mut guard = self.mutex.lock().unwrap();
*guard = true;
self.condvar.notify_one();
Ok(())
}
pub fn pop(&self) -> Result<T, PopError> {
self.queue.pop()
}
pub fn wait_for_item(&self, timeout: time::Duration) -> Option<NotificationItem<T>> {
match self.queue.pop() {
Ok(item) => return Some(NotificationItem::Ready(item)),
Err(PopError::Closed) => return None,
Err(PopError::Empty) => {}
}
let max_spins = self.max_spins.load(Ordering::Relaxed);
let mut spins = 0;
let mut guard = self.mutex.lock().unwrap();
loop {
if spins >= max_spins {
*guard = false;
return Some(NotificationItem::None);
}
match self.queue.pop() {
Ok(item) => {
*guard = false;
return Some(NotificationItem::Ready(item));
}
Err(PopError::Closed) => {
*guard = false;
return None;
}
Err(PopError::Empty) => {}
}
let result = self.condvar.wait_timeout(guard, timeout).unwrap();
guard = result.0;
spins += 1;
match self.queue.pop() {
Ok(item) => {
*guard = false;
return Some(NotificationItem::Ready(item));
}
Err(PopError::Closed) => {
*guard = false;
return None;
}
Err(PopError::Empty) => {}
}
*guard = false;
}
}
pub fn is_empty(&self) -> bool {
self.queue.is_empty()
}
pub fn is_closed(&self) -> bool {
self.queue.is_closed()
}
pub fn len(&self) -> usize {
self.queue.len()
}
pub fn close(&self) -> bool {
self.queue.close()
}
pub fn queue(&self) -> &ConcurrentQueue<T> {
&self.queue
}
pub fn queue_arc(&self) -> Arc<ConcurrentQueue<T>> {
self.queue.clone()
}
}
#[derive(Debug)]
pub struct NotifyRecvIter<T> {
queue: Arc<NotifyQueue<T>>,
}
impl<T> Clone for NotifyRecvIter<T> {
fn clone(&self) -> Self {
Self {
queue: self.queue.clone(),
}
}
}
impl<T> NotifyRecvIter<T> {
pub fn new(queue: Arc<NotifyQueue<T>>) -> Self {
Self { queue }
}
#[must_use]
pub fn is_closed(&self) -> bool {
self.queue.is_closed()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.queue.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.queue.len()
}
pub fn block_recv(&self, timeout: time::Duration) -> Option<NotificationItem<T>> {
self.queue.wait_for_item(timeout)
}
}
#[derive(Debug)]
pub struct NotifyRecvIterator<T>(NotifyRecvIter<T>, time::Duration);
impl<T> NotifyRecvIterator<T> {
pub fn from_notify_queue(item: Arc<NotifyQueue<T>>, dur: time::Duration) -> Self {
Self::new(NotifyRecvIter::new(item), dur)
}
#[must_use]
pub fn new(item: NotifyRecvIter<T>, dur: time::Duration) -> Self {
Self(item, dur)
}
#[must_use]
pub fn is_closed(&self) -> bool {
self.0.is_closed()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
}
impl<T> Iterator for NotifyRecvIterator<T> {
type Item = NotificationItem<T>;
fn next(&mut self) -> Option<Self::Item> {
match self.0.block_recv(self.1) {
Some(NotificationItem::Ready(value)) => Some(NotificationItem::Ready(value)),
Some(NotificationItem::None) => Some(NotificationItem::None),
None => None, }
}
}
use crate::valtron::iterators::Stream;
#[derive(Debug)]
pub struct NotifyQueueStreamIterator<D, P> {
chan: Arc<NotifyQueue<Stream<D, P>>>,
max_turns: usize,
park_duration: time::Duration,
}
impl<D, P> NotifyQueueStreamIterator<D, P> {
pub fn new(
chan: Arc<NotifyQueue<Stream<D, P>>>,
max_turns: usize,
park_duration: time::Duration,
) -> Self {
assert!(max_turns > 0, "max_turns must be greater than 0");
Self {
chan,
max_turns,
park_duration,
}
}
#[must_use]
pub fn chan(&self) -> &Arc<NotifyQueue<Stream<D, P>>> {
&self.chan
}
#[must_use]
pub fn max_turns(&self) -> usize {
self.max_turns
}
#[must_use]
pub fn park_duration(&self) -> time::Duration {
self.park_duration
}
#[must_use]
pub fn is_closed(&self) -> bool {
self.chan.is_closed()
}
#[must_use]
pub fn len(&self) -> usize {
self.chan.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.chan.is_empty()
}
}
impl<D, P> Iterator for NotifyQueueStreamIterator<D, P> {
type Item = Stream<D, P>;
fn next(&mut self) -> Option<Self::Item> {
match self.chan.pop() {
Ok(value) => {
return Some(value);
}
Err(PopError::Closed) => {
return None;
}
Err(PopError::Empty) => {}
}
match self.chan.wait_for_item(self.park_duration) {
Some(NotificationItem::Ready(value)) => Some(value),
Some(NotificationItem::None) => {
Some(Stream::Wait)
}
None => {
None
}
}
}
}
pub struct ExecutorState {
pub state_owner: String,
pub wakeup_priority: PriorityOrder,
pub global_tasks: SharedTaskQueue,
pub current_task: rc::Rc<cell::RefCell<Option<Entry>>>,
pub local_tasks: rc::Rc<cell::RefCell<EntryList<BoxedExecutionIterator>>>,
pub task_graph: rc::Rc<cell::RefCell<HashMap<Entry, Entry>>>,
pub packed_tasks: rc::Rc<cell::RefCell<HashMap<Entry, bool>>>,
pub processing: rc::Rc<cell::RefCell<VecDeque<Entry>>>,
pub rng: rc::Rc<cell::RefCell<ChaCha8Rng>>,
pub sleepers: Sleepers<Sleepable>,
pub idler: rc::Rc<cell::RefCell<IdleMan>>,
pub depends_true_violations: rc::Rc<cell::RefCell<HashMap<Entry, u32>>>,
}
static DEQUEUE_CAPACITY: usize = 10;
const READINESS_POLL_QUANTUM: time::Duration = time::Duration::from_millis(1);
const DEPENDS_TRUE_PANIC_THRESHOLD: u32 = 4096;
impl ExecutorState {
pub fn new(
state_owner: String,
global_tasks: SharedTaskQueue,
wakeup_priority: PriorityOrder,
rng: ChaCha8Rng,
idler: IdleMan,
) -> Self {
Self {
state_owner,
wakeup_priority,
global_tasks,
rng: rc::Rc::new(cell::RefCell::new(rng)),
idler: rc::Rc::new(cell::RefCell::new(idler)),
current_task: rc::Rc::new(cell::RefCell::new(None)),
task_graph: rc::Rc::new(cell::RefCell::new(HashMap::new())),
packed_tasks: rc::Rc::new(cell::RefCell::new(HashMap::new())),
local_tasks: rc::Rc::new(cell::RefCell::new(EntryList::new())),
sleepers: Sleepers::with_max_readiness_wait(DEFAULT_READINESS_WAIT),
processing: rc::Rc::new(cell::RefCell::new(VecDeque::with_capacity(
DEQUEUE_CAPACITY,
))),
depends_true_violations: rc::Rc::new(cell::RefCell::new(HashMap::new())),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ScheduleOutcome {
GlobalTaskAcquired,
NoTaskRunningOrAcquired,
LocalTaskRunning,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ProgressIndicator {
NoWork,
CanProgress(Option<State>),
SpinWait(time::Duration),
Wait,
}
impl Clone for ExecutorState {
fn clone(&self) -> Self {
Self {
state_owner: self.state_owner.clone(),
rng: self.rng.clone(),
idler: self.idler.clone(),
sleepers: self.sleepers.clone(),
wakeup_priority: self.wakeup_priority.clone(),
current_task: self.current_task.clone(),
global_tasks: self.global_tasks.clone(),
local_tasks: self.local_tasks.clone(),
task_graph: self.task_graph.clone(),
packed_tasks: self.packed_tasks.clone(),
processing: self.processing.clone(),
depends_true_violations: rc::Rc::new(cell::RefCell::new(HashMap::new())),
}
}
}
impl core::fmt::Debug for ExecutorState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExecutorState")
.field("priority", &self.wakeup_priority)
.field("global_tasks", &self.global_tasks)
.field("current_task", &self.current_task)
.field("local_tasks", &self.local_tasks.borrow().allocated_slots())
.field("task_graph", &self.task_graph)
.field("packed_tasks", &self.packed_tasks)
.field("processing", &self.processing)
.field("rng", &self.rng)
.field("sleepers", &self.sleepers)
.field("idler", &self.idler.borrow().count())
.finish()
}
}
impl ExecutorState {
#[must_use]
pub fn get_rng(&self) -> rc::Rc<cell::RefCell<ChaCha8Rng>> {
self.rng.clone()
}
fn number_of_local_tasks(&self) -> usize {
self.local_tasks.borrow().active_slots()
}
fn number_of_inprocess(&self) -> usize {
self.processing.borrow().len()
}
#[inline]
#[must_use]
pub fn get_task_dependents(&self, target: Entry) -> Vec<Entry> {
let mut deps = Vec::new();
let mut previous = Some(target);
while previous.is_some() {
let key = previous.unwrap();
match self.task_graph.borrow().get(&key) {
Some(dep) => {
deps.push(*dep);
previous = Some(*dep);
}
None => break,
}
}
deps
}
#[inline]
#[must_use]
pub fn is_packed(&self, target: &Entry) -> bool {
*self.packed_tasks.borrow().get(target).unwrap_or(&false)
}
#[inline]
pub fn wake_up(&self, target: Entry) {
if !self.local_tasks.borrow_mut().has(&target) {
tracing::debug!(
"[SLEEPER] task {:?} no longer in local_tasks, skipping wake_up",
target
);
return;
}
self.packed_tasks.borrow_mut().remove(&target);
match self.wakeup_priority {
PriorityOrder::Top => {
self.processing.borrow_mut().push_front(target);
}
PriorityOrder::Bottom => {
self.processing.borrow_mut().push_back(target);
}
}
}
#[inline]
pub fn wakeup_ready_sleepers(&self) {
for matured in self.sleepers.get_matured() {
match matured {
Sleepable::Timable(wakeable) => self.wake_up(wakeable.handle),
Sleepable::Atomic(_, entry) => self.wake_up(entry),
Sleepable::Readiness(_, entry) => self.wake_up(entry),
}
}
}
#[inline]
#[must_use]
pub fn number_of_sleepers(&self) -> usize {
self.sleepers.count()
}
#[inline]
#[must_use]
pub fn has_sleeping_tasks(&self) -> bool {
self.sleepers.has_pending_tasks()
}
#[inline]
#[must_use]
pub fn time_until_next_wakeup(&self) -> Option<time::Duration> {
self.sleepers.time_until_next()
}
#[inline]
#[must_use]
pub fn has_local_tasks(&self) -> bool {
self.local_tasks.borrow().active_slots() > 0
}
#[must_use]
pub fn has_active_tasks(&self) -> bool {
self.total_active_tasks() > 0
}
#[must_use]
pub fn has_inflight_task(&self) -> bool {
!self.processing.borrow().is_empty()
}
#[must_use]
pub fn has_incoming_global_tasks(&self) -> bool {
!self.global_tasks.is_empty()
}
#[inline]
#[must_use]
pub fn has_runnable_work(&self) -> bool {
self.has_active_tasks() || self.has_inflight_task() || self.has_incoming_global_tasks()
}
#[must_use]
pub fn total_inprocess_tasks(&self) -> usize {
self.number_of_inprocess()
}
#[must_use]
pub fn total_sleeping_tasks(&self) -> usize {
self.sleepers.count()
}
#[must_use]
pub fn total_tasks(&self) -> usize {
self.local_tasks.borrow().active_slots()
}
pub fn total_active_tasks(&self) -> usize {
let local_task_count = self.local_tasks.borrow().active_slots();
let sleeping_task_count = self.sleepers.count();
local_task_count.saturating_sub(sleeping_task_count)
}
#[inline]
pub fn remove_sleepers_for_entry(&self, entry: &Entry) {
let _ = self.sleepers.remove(entry);
}
#[inline]
pub fn schedule_next(&self) -> ScheduleOutcome {
if self.local_tasks.borrow().active_slots() > 0 && !self.processing.borrow().is_empty() {
return ScheduleOutcome::LocalTaskRunning;
}
match self.global_tasks.pop() {
Ok(task) => {
let task_entry = self.local_tasks.borrow_mut().insert(task);
self.processing.borrow_mut().push_front(task_entry);
ScheduleOutcome::GlobalTaskAcquired
}
Err(_) => ScheduleOutcome::NoTaskRunningOrAcquired,
}
}
#[inline]
pub fn request_global_task(&self) -> ProgressIndicator {
if self.has_active_tasks() {
return ProgressIndicator::CanProgress(None);
}
match self.schedule_next() {
ScheduleOutcome::GlobalTaskAcquired => ProgressIndicator::CanProgress(None),
ScheduleOutcome::NoTaskRunningOrAcquired => {
if self.has_sleeping_tasks() {
return ProgressIndicator::CanProgress(None);
}
ProgressIndicator::NoWork
}
ScheduleOutcome::LocalTaskRunning => {
unreachable!("No local task should be running at this point")
}
}
}
#[inline]
pub fn schedule_and_do_work(&self, engine: BoxedExecutionEngine) -> ProgressIndicator {
match self.request_global_task() {
ProgressIndicator::CanProgress(_) => {}
ProgressIndicator::NoWork => {
return ProgressIndicator::NoWork;
}
ProgressIndicator::SpinWait(_) => {
unreachable!("Requesting global task should never spin wait")
}
ProgressIndicator::Wait => {
return ProgressIndicator::Wait;
}
}
self.wakeup_ready_sleepers();
match self.do_work(engine) {
ProgressIndicator::CanProgress(state) => ProgressIndicator::CanProgress(state),
ProgressIndicator::NoWork => {
if self.has_sleeping_tasks() {
if let Some(min_sleep_dur) = self.sleepers.min_duration() {
return ProgressIndicator::SpinWait(min_sleep_dur);
}
return ProgressIndicator::CanProgress(None);
}
self.current_task.borrow_mut().take();
match self.idler.borrow_mut().increment() {
Some(next_dur) => ProgressIndicator::SpinWait(next_dur),
None => ProgressIndicator::NoWork,
}
}
ProgressIndicator::SpinWait(duration) => {
if self.has_inflight_task() {
return ProgressIndicator::CanProgress(None);
}
if self.has_incoming_global_tasks() {
return ProgressIndicator::CanProgress(None);
}
self.current_task.borrow_mut().take();
match self.schedule_next() {
ScheduleOutcome::GlobalTaskAcquired => ProgressIndicator::CanProgress(None),
ScheduleOutcome::NoTaskRunningOrAcquired => {
ProgressIndicator::SpinWait(duration)
}
ScheduleOutcome::LocalTaskRunning => {
unreachable!("global task never spinWaits")
}
}
}
ProgressIndicator::Wait => ProgressIndicator::Wait,
}
}
pub fn check_processing_queue(&self) -> Option<ProgressIndicator> {
let has_sleeping_tasks = self.has_sleeping_tasks();
let handle = self.processing.borrow_mut();
if handle.is_empty() {
if has_sleeping_tasks {
return Some(ProgressIndicator::CanProgress(None));
}
return Some(ProgressIndicator::NoWork);
}
None
}
#[inline]
pub fn do_work(&self, engine: BoxedExecutionEngine) -> ProgressIndicator {
if let Some(inner) = self.check_processing_queue() {
match inner {
ProgressIndicator::NoWork => {
return ProgressIndicator::NoWork;
}
ProgressIndicator::CanProgress(inner) => {
return ProgressIndicator::CanProgress(inner);
}
ProgressIndicator::SpinWait(_) => {
unreachable!("check_processing_queue should never reach here")
}
ProgressIndicator::Wait => {
unreachable!("check_processing_queue should never return Wait")
}
}
}
let top_entry = self.processing.borrow_mut().pop_front().unwrap();
let remaining_tasks = self.processing.borrow().len();
if self.is_packed(&top_entry) {
return ProgressIndicator::CanProgress(None);
}
self.current_task.borrow_mut().replace(top_entry);
let iter_container = self.local_tasks.borrow_mut().park(&top_entry);
assert!(
iter_container.is_some(),
"An entry must always have a task attached"
);
let mut iter = iter_container.unwrap();
let next_result = iter.next(top_entry, engine);
if next_result.is_none() {
self.local_tasks.borrow_mut().unpark(&top_entry, iter);
self.remove_sleepers_for_entry(&top_entry);
self.local_tasks.borrow_mut().take(&top_entry);
if remaining_tasks == 0 {
return ProgressIndicator::NoWork;
}
return ProgressIndicator::CanProgress(None);
}
if !matches!(next_result.as_ref().unwrap(), State::Depends(_)) {
self.depends_true_violations.borrow_mut().remove(&top_entry);
}
match next_result.unwrap() {
State::SpawnFailed(parent_key) => {
self.local_tasks.borrow_mut().unpark(&top_entry, iter);
self.remove_sleepers_for_entry(&top_entry);
self.local_tasks.borrow_mut().take(&top_entry);
if remaining_tasks == 0 {
ProgressIndicator::NoWork
} else {
ProgressIndicator::CanProgress(Some(State::SpawnFailed(parent_key)))
}
}
State::Panicked => {
self.local_tasks.borrow_mut().unpark(&top_entry, iter);
self.remove_sleepers_for_entry(&top_entry);
self.local_tasks.borrow_mut().take(&top_entry);
if remaining_tasks == 0 {
ProgressIndicator::NoWork
} else {
ProgressIndicator::CanProgress(Some(State::Panicked))
}
}
State::SpawnFinished(info) => {
self.local_tasks.borrow_mut().unpark(&top_entry, iter);
if !info.has_parent_and_child() {
return match info.spawn_type() {
SpawnType::Lifted => {
let child_entry = info.parent().unwrap();
let parent_task = self
.local_tasks
.borrow_mut()
.take(&top_entry)
.expect("get parent task");
let child_task = self
.local_tasks
.borrow_mut()
.take(&child_entry)
.expect("get child task");
let task =
FinishChildBeforeParentTask::new(info, parent_task, child_task);
let new_task_id = self.local_tasks.borrow_mut().insert(Box::new(task));
self.processing.borrow_mut().push_front(new_task_id);
ProgressIndicator::CanProgress(Some(State::SpawnFinished(
SpawnInfo::new(SpawnType::Lifted, Some(new_task_id), None),
)))
}
SpawnType::Broadcasted | SpawnType::None | SpawnType::Scheduled => {
tracing::trace!("spawned process without parent, requeueing: {:?} - id: {:?}", info, top_entry);
self.processing.borrow_mut().push_front(top_entry);
ProgressIndicator::CanProgress(Some(State::SpawnFinished(info)))
}
SpawnType::LiftedWithParent | SpawnType::Sequenced => {
unreachable!("This state must always come with a parent");
}
};
}
match info.spawn_type() {
SpawnType::LiftedWithParent => {
assert!(
top_entry == info.parent().unwrap(),
"Parent entry id must be the same as top entry"
);
let parent_entry = info.parent().unwrap();
let child_entry = info.child().unwrap();
let parent_task = self
.local_tasks
.borrow_mut()
.take(&parent_entry)
.expect("get parent task");
let child_task = self
.local_tasks
.borrow_mut()
.take(&child_entry)
.expect("get child task");
let task = FinishChildBeforeParentTask::new(info, parent_task, child_task);
let new_task_id = self.local_tasks.borrow_mut().insert(Box::new(task));
self.processing.borrow_mut().push_front(new_task_id);
ProgressIndicator::CanProgress(Some(State::SpawnFinished(SpawnInfo::new(
SpawnType::Lifted,
Some(new_task_id),
None,
))))
}
SpawnType::Sequenced => {
assert!(
top_entry == info.parent().unwrap(),
"Parent entry id must be the same as top entry"
);
let parent_entry = info.parent().unwrap();
let child_entry = info.child().unwrap();
let parent_task = self
.local_tasks
.borrow_mut()
.take(&parent_entry)
.expect("get parent task");
let child_task = self
.local_tasks
.borrow_mut()
.take(&child_entry)
.expect("get child task");
let task = DualSequeunceChildAndParentLinkedTask::new(
info,
parent_task,
child_task,
);
let new_task_id = self.local_tasks.borrow_mut().insert(Box::new(task));
self.processing.borrow_mut().push_front(new_task_id);
ProgressIndicator::CanProgress(Some(State::SpawnFinished(SpawnInfo::new(
SpawnType::Lifted,
Some(new_task_id),
None,
))))
}
_ => {
tracing::trace!(
"spawned process without parent, requeueing: {:?} - id: {:?}",
info,
top_entry
);
self.processing.borrow_mut().push_front(top_entry);
ProgressIndicator::CanProgress(Some(State::SpawnFinished(info)))
}
}
}
State::Done => {
self.local_tasks.borrow_mut().unpark(&top_entry, iter);
self.remove_sleepers_for_entry(&top_entry);
self.local_tasks.borrow_mut().take(&top_entry);
if remaining_tasks == 0 {
ProgressIndicator::NoWork
} else {
ProgressIndicator::CanProgress(Some(State::Done))
}
}
State::ReadyValue(entry_id) => {
self.local_tasks.borrow_mut().unpark(&top_entry, iter);
self.processing.borrow_mut().push_front(top_entry);
ProgressIndicator::CanProgress(Some(State::ReadyValue(entry_id)))
}
State::Progressed => {
self.local_tasks.borrow_mut().unpark(&top_entry, iter);
self.processing.borrow_mut().push_front(top_entry);
ProgressIndicator::CanProgress(Some(State::Progressed))
}
State::Pending(duration) => {
self.local_tasks.borrow_mut().unpark(&top_entry, iter);
let final_state = if let Some(inner) = duration {
self.sleepers.insert(
top_entry,
Sleepable::Timable(DurationWaker::from_now(top_entry, inner)),
);
if !self.processing.borrow().is_empty() {
return ProgressIndicator::CanProgress(Some(State::Pending(duration)));
}
ProgressIndicator::SpinWait(inner)
} else {
self.processing.borrow_mut().push_front(top_entry);
ProgressIndicator::CanProgress(None)
};
final_state
}
State::Reschedule => {
self.local_tasks.borrow_mut().unpark(&top_entry, iter);
self.processing.borrow_mut().push_back(top_entry);
ProgressIndicator::CanProgress(Some(State::Reschedule))
}
State::Wait => {
self.local_tasks.borrow_mut().unpark(&top_entry, iter);
self.processing.borrow_mut().push_front(top_entry);
ProgressIndicator::Wait
}
State::Depends(signal) => {
if self.sleepers.contains(&top_entry) {
panic!(
"Task {:?} returned State::Depends while already registered as a sleeper. \
A sleeping task must be woken before it can send Depends again.",
top_entry
);
}
if signal.is_ready(None) {
self.depends_true_violations
.borrow_mut()
.entry(top_entry)
.and_modify(|c| *c += 1)
.or_insert(1);
let violations = *self
.depends_true_violations
.borrow()
.get(&top_entry)
.unwrap();
if violations >= DEPENDS_TRUE_PANIC_THRESHOLD {
panic!(
"Task {:?} returned State::Depends with an already-ready signal {} \
consecutive times. This is a task bug, not an executor issue.",
top_entry, violations
);
}
if violations <= 3 || violations.is_multiple_of(256) {
tracing::warn!(
"[DEPENDS] Task {:?} returned already-ready signal (violation {}/{})",
top_entry,
violations,
DEPENDS_TRUE_PANIC_THRESHOLD
);
}
self.local_tasks.borrow_mut().unpark(&top_entry, iter);
self.processing.borrow_mut().push_back(top_entry);
return ProgressIndicator::CanProgress(Some(State::Pending(None)));
}
self.depends_true_violations.borrow_mut().remove(&top_entry);
self.local_tasks.borrow_mut().unpark(&top_entry, iter);
self.sleepers
.insert(top_entry, Sleepable::Readiness(signal, top_entry));
if self.has_incoming_global_tasks() || self.has_inflight_task() {
return ProgressIndicator::CanProgress(None);
}
match self.idler.borrow_mut().increment() {
Some(next_dur) => ProgressIndicator::SpinWait(next_dur),
None => ProgressIndicator::NoWork,
}
}
}
}
}
impl ExecutorState {
#[inline]
pub fn sequenced(
&self,
task: BoxedExecutionIterator,
parent: Entry,
) -> AnyResult<SpawnInfo, ExecutorError> {
match *self.current_task.borrow() {
Some(current_task) => {
if !current_task.eq(&parent) {
return Err(ExecutorError::ParentMustBeExecutingToLift);
}
}
None => unreachable!("Parent task should never truly be empty at this point"),
}
let task_entry = self.local_tasks.borrow_mut().insert(task);
Ok(SpawnInfo::new(
SpawnType::Sequenced,
Some(task_entry),
Some(parent),
))
}
#[inline]
pub fn lift(
&self,
task: BoxedExecutionIterator,
parent: Option<Entry>,
) -> AnyResult<SpawnInfo, ExecutorError> {
if let Some(parent_handle) = &parent {
match *self.current_task.borrow() {
Some(current_task) => {
if !current_task.eq(parent_handle) {
return Err(ExecutorError::ParentMustBeExecutingToLift);
}
}
None => unreachable!("Current task should never truly be empty at this point"),
}
}
let task_entry = self.local_tasks.borrow_mut().insert(task);
Ok(if parent.is_some() {
SpawnInfo::new(SpawnType::LiftedWithParent, Some(task_entry), parent)
} else {
self.processing.borrow_mut().push_front(task_entry);
SpawnInfo::new(SpawnType::Lifted, Some(task_entry), parent)
})
}
#[inline]
pub fn schedule(&self, task: BoxedExecutionIterator) -> AnyResult<SpawnInfo, ExecutorError> {
let task_entry = self.local_tasks.borrow_mut().insert(task);
self.processing.borrow_mut().push_back(task_entry);
Ok(SpawnInfo::new(SpawnType::Scheduled, Some(task_entry), None))
}
}
impl ExecutorState {
pub fn broadcast(&self, task: GlobalTask) -> AnyResult<SpawnInfo, ExecutorError> {
match self.global_tasks.push(task) {
Ok(()) => Ok(SpawnInfo::new(SpawnType::Broadcasted, None, None)),
Err(err) => match err {
PushError::Full(_) => Err(ExecutorError::QueueFull),
PushError::Closed(_) => Err(ExecutorError::QueueClosed),
},
}
}
}
pub struct ReferencedExecutorState {
inner: rc::Rc<ExecutorState>,
activities: Option<mpp::Sender<ThreadActivity>>,
}
impl From<rc::Rc<ExecutorState>> for ReferencedExecutorState {
fn from(value: rc::Rc<ExecutorState>) -> Self {
ReferencedExecutorState {
inner: value,
activities: None,
}
}
}
impl Clone for ReferencedExecutorState {
fn clone(&self) -> Self {
ReferencedExecutorState {
inner: rc::Rc::clone(&self.inner),
activities: self.activities.clone(),
}
}
}
#[allow(unused)]
impl ReferencedExecutorState {
#[must_use]
pub fn new(
inner: rc::Rc<ExecutorState>,
activities: Option<mpp::Sender<ThreadActivity>>,
) -> Self {
Self { inner, activities }
}
#[must_use]
pub fn clone_queue(&self) -> SharedTaskQueue {
self.inner.global_tasks.clone()
}
#[must_use]
pub fn clone_state(&self) -> rc::Rc<ExecutorState> {
self.inner.clone()
}
fn use_activities(&mut self, activities: mpp::Sender<ThreadActivity>) {
self.activities = Some(activities);
}
fn get_ref(&self) -> &rc::Rc<ExecutorState> {
&self.inner
}
fn get_ref_mut(&mut self) -> &mut rc::Rc<ExecutorState> {
&mut self.inner
}
fn number_of_local_tasks(&self) -> usize {
self.inner.number_of_local_tasks()
}
fn number_of_inprocess(&self) -> usize {
self.inner.number_of_inprocess()
}
}
impl ReferencedExecutorState {
#[inline]
#[must_use]
pub fn get_rng(&self) -> rc::Rc<cell::RefCell<ChaCha8Rng>> {
self.inner.get_rng()
}
#[inline]
#[must_use]
pub fn number_of_sleepers(&self) -> usize {
self.inner.number_of_sleepers()
}
#[inline]
#[must_use]
pub fn time_until_next_wakeup(&self) -> Option<time::Duration> {
self.inner.time_until_next_wakeup()
}
#[inline]
#[must_use]
pub fn total_tasks(&self) -> usize {
self.inner.total_tasks()
}
#[must_use]
pub fn has_inflight_task(&self) -> bool {
self.inner.has_inflight_task()
}
#[inline]
#[must_use]
pub fn has_runnable_work(&self) -> bool {
self.inner.has_runnable_work()
}
#[must_use]
pub fn schedule_and_do_work(&self, engine: BoxedExecutionEngine) -> ProgressIndicator {
self.inner.schedule_and_do_work(engine)
}
}
pub struct LocalExecutionEngine {
inner: rc::Rc<ExecutorState>,
activities: Option<mpp::Sender<ThreadActivity>>,
}
impl Clone for LocalExecutionEngine {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
activities: self.activities.clone(),
}
}
}
impl LocalExecutionEngine {
#[must_use]
pub fn new(
inner: rc::Rc<ExecutorState>,
activities: Option<mpp::Sender<ThreadActivity>>,
) -> Self {
Self { inner, activities }
}
#[allow(unused)]
fn use_activities(&mut self, activities: mpp::Sender<ThreadActivity>) {
self.activities = Some(activities);
}
}
#[allow(clippy::needless_lifetimes)]
impl<'a> ExecutionEngine for Box<&'a LocalExecutionEngine> {
fn lift(
&self,
task: BoxedExecutionIterator,
parent: Option<Entry>,
) -> AnyResult<SpawnInfo, ExecutorError> {
(**self).lift(task, parent)
}
fn sequenced(
&self,
task: BoxedExecutionIterator,
parent: Entry,
) -> AnyResult<SpawnInfo, ExecutorError> {
(**self).sequenced(task, parent)
}
fn schedule(&self, task: BoxedExecutionIterator) -> AnyResult<SpawnInfo, ExecutorError> {
(**self).schedule(task)
}
fn broadcast(&self, task: GlobalTask) -> AnyResult<SpawnInfo, ExecutorError> {
(**self).broadcast(task)
}
fn shared_queue(&self) -> SharedTaskQueue {
(**self).shared_queue()
}
fn boxed_engine(&self) -> BoxedExecutionEngine {
(**self).boxed_engine()
}
fn rng(&self) -> rc::Rc<cell::RefCell<ChaCha8Rng>> {
(**self).rng()
}
}
impl ExecutionEngine for Box<LocalExecutionEngine> {
fn sequenced(
&self,
task: BoxedExecutionIterator,
parent: Entry,
) -> AnyResult<SpawnInfo, ExecutorError> {
(**self).sequenced(task, parent)
}
fn lift(
&self,
task: BoxedExecutionIterator,
parent: Option<Entry>,
) -> AnyResult<SpawnInfo, ExecutorError> {
(**self).lift(task, parent)
}
fn schedule(&self, task: BoxedExecutionIterator) -> AnyResult<SpawnInfo, ExecutorError> {
(**self).schedule(task)
}
fn broadcast(&self, task: GlobalTask) -> AnyResult<SpawnInfo, ExecutorError> {
(**self).broadcast(task)
}
fn shared_queue(&self) -> SharedTaskQueue {
(**self).shared_queue()
}
fn boxed_engine(&self) -> BoxedExecutionEngine {
(**self).boxed_engine()
}
fn rng(&self) -> rc::Rc<cell::RefCell<ChaCha8Rng>> {
(**self).rng()
}
}
impl ExecutionEngine for LocalExecutionEngine {
fn sequenced(
&self,
task: BoxedExecutionIterator,
parent: Entry,
) -> AnyResult<SpawnInfo, ExecutorError> {
let entry = self.inner.sequenced(task, parent)?;
Ok(entry)
}
fn lift(
&self,
task: BoxedExecutionIterator,
parent: Option<Entry>,
) -> AnyResult<SpawnInfo, ExecutorError> {
let entry = self.inner.lift(task, parent)?;
Ok(entry)
}
fn schedule(&self, task: BoxedExecutionIterator) -> AnyResult<SpawnInfo, ExecutorError> {
let entry = self.inner.schedule(task)?;
Ok(entry)
}
fn broadcast(&self, task: GlobalTask) -> AnyResult<SpawnInfo, ExecutorError> {
let info = self.inner.broadcast(task)?;
if let Some(sender) = &self.activities {
if let Err(err) = sender.send(ThreadActivity::BroadcastedTask) {
tracing::error!("Failure in sending thread activity occurred: {:?}", err);
return Err(ExecutorError::FailedToSendThreadActivity);
}
}
Ok(info)
}
fn boxed_engine(&self) -> BoxedExecutionEngine {
Box::new(self.clone())
}
fn shared_queue(&self) -> SharedTaskQueue {
self.inner.global_tasks.clone()
}
fn rng(&self) -> rc::Rc<cell::RefCell<ChaCha8Rng>> {
self.inner.get_rng()
}
}
impl ReferencedExecutorState {
#[inline]
#[must_use]
pub fn local_engine(&self) -> LocalExecutionEngine {
LocalExecutionEngine::new(self.inner.clone(), self.activities.clone())
}
#[inline]
#[must_use]
pub fn boxed_engine(&self) -> BoxedExecutionEngine {
Box::new(self.local_engine())
}
}
pub struct LocalThreadExecutor<T: ProcessController + Clone> {
state_owner: String,
kill_signal: Option<Arc<OnSignal>>,
state: ReferencedExecutorState,
no_work_yield: time::Duration,
max_yield_duration: time::Duration,
yielder: T,
kill_signal_check_interval: usize,
}
impl<T: ProcessController + Clone> Clone for LocalThreadExecutor<T> {
fn clone(&self) -> Self {
LocalThreadExecutor {
state_owner: self.state_owner.clone(),
state: self.state.clone(),
yielder: self.yielder.clone(),
no_work_yield: self.no_work_yield,
max_yield_duration: self.max_yield_duration,
kill_signal: self.kill_signal.clone(),
kill_signal_check_interval: self.kill_signal_check_interval,
}
}
}
#[allow(unused)]
#[allow(clippy::too_many_arguments)]
impl<T: ProcessController + Clone> LocalThreadExecutor<T> {
pub fn new(
state_owner: String,
tasks: SharedTaskQueue,
rng: ChaCha8Rng,
idler: IdleMan,
priority: PriorityOrder,
yielder: T,
no_work_yield: time::Duration,
kill_signal: Option<Arc<OnSignal>>,
activities: Option<mpp::Sender<ThreadActivity>>,
kill_signal_check_interval: usize,
) -> Self {
Self {
state_owner: state_owner.clone(),
yielder,
kill_signal,
no_work_yield,
max_yield_duration: time::Duration::from_millis(100),
kill_signal_check_interval,
state: ReferencedExecutorState::new(
rc::Rc::new(ExecutorState::new(state_owner, tasks, priority, rng, idler)),
activities,
),
}
}
pub fn from_seed(
seed: u64,
state_owner: String,
tasks: SharedTaskQueue,
idler: IdleMan,
priority: PriorityOrder,
yielder: T,
no_work_yield: time::Duration,
kill_signal: Option<Arc<OnSignal>>,
activities: Option<mpp::Sender<ThreadActivity>>,
) -> Self {
Self::new(
state_owner,
tasks,
ChaCha8Rng::seed_from_u64(seed),
idler,
priority,
yielder,
no_work_yield,
kill_signal,
activities,
DEFAULT_KILL_SIGNAL_CHECK_INTERVAL,
)
}
pub fn from_rng<R: foundation_compact::rng::Rng>(
state_owner: String,
tasks: SharedTaskQueue,
rng: &mut R,
idler: IdleMan,
priority: PriorityOrder,
yielder: T,
no_work_yield: time::Duration,
kill_signal: Option<Arc<OnSignal>>,
activities: Option<mpp::Sender<ThreadActivity>>,
) -> Self {
Self::from_seed(
rng.next_u64(),
state_owner,
tasks,
idler,
priority,
yielder,
no_work_yield,
kill_signal,
activities,
)
}
pub fn with_kill_signal_check_interval(mut self, interval: usize) -> Self {
self.kill_signal_check_interval = interval;
self
}
}
#[allow(unused)]
impl<T: ProcessController + Clone> LocalThreadExecutor<T> {
#[inline]
pub fn local_engine(&self) -> LocalExecutionEngine {
self.state.local_engine()
}
#[inline]
pub fn boxed_engine(&self) -> BoxedExecutionEngine {
self.state.boxed_engine()
}
#[inline]
pub fn number_of_sleepers(&self) -> usize {
self.state.number_of_sleepers()
}
#[inline]
pub fn number_of_local_tasks(&self) -> usize {
self.state.number_of_local_tasks()
}
#[inline]
pub fn number_of_inprocess(&self) -> usize {
self.state.number_of_inprocess()
}
}
#[allow(unused)]
impl<T: ProcessController + Clone> LocalThreadExecutor<T> {
#[inline]
pub fn get_rng(&self) -> rc::Rc<cell::RefCell<ChaCha8Rng>> {
self.state.get_rng()
}
#[inline]
pub fn run_once(&self) -> ProgressIndicator {
let local_executor = self.state.local_engine();
self.state.schedule_and_do_work(Box::new(local_executor))
}
#[inline]
pub fn run_until<S>(&self, checker: S)
where
S: Fn(ProgressIndicator) -> bool,
{
loop {
if let Some(ref kill_signal) = self.kill_signal {
if kill_signal.probe() {
return;
}
}
let local_executor = self.state.local_engine();
let response = self.state.schedule_and_do_work(Box::new(local_executor));
if checker(response.clone()) {
break;
}
#[cfg(any(feature = "js-wasmbindgen", feature = "js-foundation-wasm"))]
if matches!(&response, ProgressIndicator::NoWork) {
break;
}
#[cfg(any(feature = "js-wasmbindgen", feature = "js-foundation-wasm"))]
if matches!(
&response,
ProgressIndicator::CanProgress(Some(State::Reschedule))
) {
break;
}
if let ProgressIndicator::SpinWait(spin_duration) = response {
#[cfg(any(feature = "js-wasmbindgen", feature = "js-foundation-wasm"))]
{
let signal = self.yielder.yield_for(spin_duration);
if self.yielder.should_stop(&signal) {
break;
}
}
#[cfg(not(any(feature = "js-wasmbindgen", feature = "js-foundation-wasm")))]
self.yielder.yield_for(spin_duration);
continue;
}
#[cfg(any(feature = "js-wasmbindgen", feature = "js-foundation-wasm"))]
if matches!(&response, ProgressIndicator::Wait) {
let signal = self
.yielder
.yield_for(crate::valtron::wasm::JS_WAIT_CHECK_INTERVAL);
if self.yielder.should_stop(&signal) {
break;
}
}
if let ProgressIndicator::NoWork = response {
if self.state.total_tasks() == 0 {
break;
}
}
let yield_duration = self
.state
.time_until_next_wakeup()
.unwrap_or(self.no_work_yield)
.min(MAX_YIELD_DURATION);
self.yielder.yield_for(yield_duration);
}
}
#[inline]
pub fn block_until_finished(&self) {
'main_loop: loop {
for _ in 0..200 {
match self.run_once() {
ProgressIndicator::NoWork => {
#[cfg(any(feature = "js-wasmbindgen", feature = "js-foundation-wasm"))]
{
break 'main_loop; }
#[cfg(not(any(
feature = "js-wasmbindgen",
feature = "js-foundation-wasm"
)))]
break 'main_loop;
}
ProgressIndicator::SpinWait(duration) => {
#[cfg(any(feature = "js-wasmbindgen", feature = "js-foundation-wasm"))]
{
let signal = self.yielder.yield_for(duration);
if self.yielder.should_stop(&signal) {
break 'main_loop; }
}
#[cfg(not(any(
feature = "js-wasmbindgen",
feature = "js-foundation-wasm"
)))]
self.yielder.yield_for(duration);
}
ProgressIndicator::CanProgress(_) => {}
ProgressIndicator::Wait => {
#[cfg(any(feature = "js-wasmbindgen", feature = "js-foundation-wasm"))]
{
let signal = self
.yielder
.yield_for(crate::valtron::wasm::JS_WAIT_CHECK_INTERVAL);
if self.yielder.should_stop(&signal) {
break 'main_loop; }
}
#[cfg(not(any(
feature = "js-wasmbindgen",
feature = "js-foundation-wasm"
)))]
self.yielder.yield_for(self.no_work_yield);
}
}
}
let yield_duration = self
.state
.time_until_next_wakeup()
.unwrap_or(self.no_work_yield);
self.yielder.yield_for(yield_duration);
}
}
#[inline]
pub fn block_on(&self) {
let kill_signal = self.kill_signal
.clone()
.expect("Calling LocalThreadExecutor::block_on requires kill_signal to be provided for termination");
loop {
if kill_signal.probe() {
return;
}
for i in 0..200 {
if i % self.kill_signal_check_interval == 0 && kill_signal.probe() {
return;
}
match self.run_once() {
ProgressIndicator::CanProgress(_) => {}
ProgressIndicator::NoWork => {
if kill_signal.probe() {
return;
}
let yield_duration =
self.state.time_until_next_wakeup().unwrap_or_else(|| {
if self.state.number_of_sleepers() > 0 {
READINESS_POLL_QUANTUM
} else {
self.no_work_yield
}
});
self.yielder.yield_for(yield_duration);
}
ProgressIndicator::SpinWait(duration) => {
if kill_signal.probe() {
return;
}
self.yielder.yield_for(duration);
}
ProgressIndicator::Wait => {
if kill_signal.probe() {
return;
}
self.yielder.yield_for(self.no_work_yield);
}
}
}
}
}
}
#[must_use]
pub fn typed_task<Task, Action, Resolver>(
engine: BoxedExecutionEngine,
) -> TaskSpawnConfig<Task::Ready, Task::Pending, Task::Spawner, Resolver, Task>
where
Task::Ready: Send,
Task::Pending: Send,
Action: ExecutionAction + Send + 'static,
Task: TaskIterator<Spawner = Action> + Send + 'static,
Resolver: TaskReadyResolver<Task::Spawner, Task::Ready, Task::Pending> + 'static,
{
TaskSpawnConfig::new(engine)
}
#[must_use]
pub fn any_task<Task, Action>(
engine: BoxedExecutionEngine,
) -> TaskSpawnConfig<
Task::Ready,
Task::Pending,
Task::Spawner,
Box<dyn TaskReadyResolver<Task::Spawner, Task::Ready, Task::Pending> + 'static>,
Task,
>
where
Task::Ready: Send,
Task::Pending: Send,
Task: TaskIterator<Spawner = Action> + Send + 'static,
Action: ExecutionAction + Send + 'static,
{
TaskSpawnConfig::new(engine)
}
#[must_use]
pub fn send_any_task<Task, Action>(
engine: BoxedExecutionEngine,
) -> TaskSpawnConfig<
Task::Ready,
Task::Pending,
Task::Spawner,
Box<dyn TaskReadyResolver<Task::Spawner, Task::Ready, Task::Pending> + Send + 'static>,
Task,
>
where
Task::Ready: Send,
Task::Pending: Send,
Action: ExecutionAction + Send + 'static,
Task: TaskIterator<Spawner = Action> + Send + 'static,
{
TaskSpawnConfig::new(engine)
}
#[must_use]
pub fn send_typed_task<Task, Action, Resolver>(
engine: BoxedExecutionEngine,
) -> TaskSpawnConfig<Task::Ready, Task::Pending, Task::Spawner, Resolver, Task>
where
Task::Ready: Send + 'static,
Task::Pending: Send + 'static,
Task: TaskIterator<Spawner = Action> + Send + 'static,
Action: ExecutionAction + Send + 'static,
Resolver: TaskReadyResolver<Task::Spawner, Task::Ready, Task::Pending> + Send + 'static,
{
TaskSpawnConfig::new(engine)
}
#[cfg(all(test, not(target_family = "wasm")))]
mod test_local_thread_executor {
use std::{
sync::atomic::{AtomicUsize, Ordering},
thread,
time::Duration,
};
use crate::{
panic_if_failed,
retries::ExponentialBackoffDecider,
synca::SleepyMan,
valtron::{
BoolSignal, BoxedSendExecutionAction, EventReadiness, ExecutionAction, InlineAction,
InlineActionBehaviour, IntoBoxedSendExecutionAction, NoSpawner, OnNext,
ProcessController, TaskIterator, TaskStatus, WrapTask,
},
};
use super::*;
use foundation_compact::rng::Rng;
use sync::Arc;
use tracing_test::traced_test;
#[derive(Default, Clone)]
struct NoYielder;
impl ProcessController for NoYielder {
type YieldSignal = ();
fn yield_for(&self, _: time::Duration) {}
}
struct Counter(&'static str, usize, usize, usize);
impl TaskIterator for Counter {
type Ready = usize;
type Spawner = NoSpawner;
type Pending = time::Duration;
fn next_status(&mut self) -> Option<TaskStatus<Self::Ready, Self::Pending, Self::Spawner>> {
let old_count = self.1;
let new_count = old_count + 1;
self.1 = new_count;
tracing::trace!(task = self.0, count = new_count, "Counter advanced");
if new_count == self.2 {
return None;
}
if new_count == self.3 {
return Some(TaskStatus::Delayed(time::Duration::from_millis(5)));
}
Some(TaskStatus::Ready(new_count))
}
}
enum ListItemInner {
List(Option<Vec<usize>>),
Response(NotifyRecvIterator<TaskStatus<usize, (), NoSpawner>>),
Done,
}
struct ListItems(InlineActionBehaviour, Option<ListItemInner>);
impl TaskIterator for ListItems {
type Ready = usize;
type Pending = ();
type Spawner = BoxedSendExecutionAction;
fn next_status(&mut self) -> Option<TaskStatus<Self::Ready, Self::Pending, Self::Spawner>> {
match self.1.take() {
Some(val) => match val {
ListItemInner::List(mut items) => {
if let Some(inner) = items.take() {
let task = WrapTask::new(inner.into_iter());
let (inline_action, receiver) = InlineAction::new(
self.0,
task,
std::time::Duration::from_millis(100),
);
self.1 = Some(ListItemInner::Response(receiver));
return Some(TaskStatus::Spawn(
inline_action.into_box_send_execution_action(),
));
}
self.1 = Some(ListItemInner::Done);
None
}
ListItemInner::Response(mut iter) => {
let val_next = iter.next();
self.1 = Some(ListItemInner::Response(iter));
match val_next {
Some(NotificationItem::Ready(status)) => match status {
TaskStatus::Init => Some(TaskStatus::Init),
TaskStatus::Spread(items) => Some(TaskStatus::Spread(items)),
TaskStatus::Delayed(dur) => Some(TaskStatus::Delayed(dur)),
TaskStatus::Pending(dur) => Some(TaskStatus::Pending(dur)),
TaskStatus::Ready(item) => Some(TaskStatus::Ready(item)),
TaskStatus::Spawn(action) => {
Some(TaskStatus::Spawn(action.into_box_send_execution_action()))
}
TaskStatus::Ignore => Some(TaskStatus::Ignore),
TaskStatus::Wait => Some(TaskStatus::Wait),
TaskStatus::Depends(signal) => Some(TaskStatus::Depends(signal)),
},
Some(NotificationItem::None) => Some(TaskStatus::Wait),
None => None,
}
}
ListItemInner::Done => None,
},
None => None,
}
}
}
#[test]
#[traced_test]
fn test_task_line_action_directly() {
let seed = foundation_compact::rng::rng().next_u64();
let kill_signal = Arc::new(OnSignal::new());
let global: SharedTaskQueue = Arc::new(ConcurrentQueue::bounded(10));
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
Some(kill_signal.clone()),
None,
);
let item_list = vec![1, 2, 3];
let task = WrapTask::new(item_list.into_iter());
let (mut inline_action, receiver) = InlineAction::new(
InlineActionBehaviour::Lift,
task,
std::time::Duration::from_millis(10),
);
inline_action
.apply(None, executor.boxed_engine())
.expect("should deliver event");
executor.run_until(|state| ProgressIndicator::NoWork == state);
let all_response: Vec<_> = receiver
.filter_map(|item| match item {
NotificationItem::Ready(v) => Some(v),
NotificationItem::None => None,
})
.collect();
assert_eq!(
all_response,
vec![
TaskStatus::Ready(1),
TaskStatus::Ready(2),
TaskStatus::Ready(3),
]
);
}
#[test]
#[traced_test]
fn test_task_line_send_action() {
let seed = foundation_compact::rng::rng().next_u64();
let kill_signal = Arc::new(OnSignal::new());
let global: SharedTaskQueue = Arc::new(ConcurrentQueue::bounded(10));
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
Some(kill_signal.clone()),
None,
);
let receiver = panic_if_failed!(send_any_task(executor.boxed_engine())
.with_task(ListItems(
InlineActionBehaviour::Lift,
Some(ListItemInner::List(Some(vec![1, 2, 3])))
))
.as_scheduled()
.recv(std::time::Duration::from_millis(100)));
executor.run_until(|state| ProgressIndicator::NoWork == state);
let all_response: Vec<TaskStatus<usize, (), BoxedSendExecutionAction>> = receiver
.filter_map(|item| match item {
NotificationItem::Ready(v) => Some(v),
NotificationItem::None => None,
})
.collect();
assert_eq!(
all_response,
vec![
TaskStatus::Ready(1),
TaskStatus::Ready(2),
TaskStatus::Ready(3),
]
);
}
#[test]
#[traced_test]
fn test_task_line_send_action_for_parent_lifted() {
let seed = foundation_compact::rng::rng().next_u64();
let kill_signal = Arc::new(OnSignal::new());
let global: SharedTaskQueue = Arc::new(ConcurrentQueue::bounded(10));
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
Some(kill_signal.clone()),
None,
);
let receiver = panic_if_failed!(send_any_task(executor.boxed_engine())
.with_task(ListItems(
InlineActionBehaviour::LiftWithParent,
Some(ListItemInner::List(Some(vec![1, 2, 3])))
))
.as_scheduled()
.recv(std::time::Duration::from_millis(100)));
executor.run_until(|state| ProgressIndicator::NoWork == state);
let all_response: Vec<TaskStatus<usize, (), BoxedSendExecutionAction>> = receiver
.filter_map(|item| match item {
NotificationItem::Ready(v) => Some(v),
NotificationItem::None => None,
})
.collect();
assert_eq!(
all_response,
vec![
TaskStatus::Ready(1),
TaskStatus::Ready(2),
TaskStatus::Ready(3),
]
);
}
#[test]
#[traced_test]
fn test_task_line_send_action_for_sequenced() {
let seed = foundation_compact::rng::rng().next_u64();
let kill_signal = Arc::new(OnSignal::new());
let global: SharedTaskQueue = Arc::new(ConcurrentQueue::bounded(10));
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
Some(kill_signal.clone()),
None,
);
let receiver = panic_if_failed!(send_any_task(executor.boxed_engine())
.with_task(ListItems(
InlineActionBehaviour::Sequenced,
Some(ListItemInner::List(Some(vec![1, 2, 3])))
))
.as_scheduled()
.recv(std::time::Duration::from_millis(100)));
executor.run_until(|state| ProgressIndicator::NoWork == state);
let all_response: Vec<TaskStatus<usize, (), BoxedSendExecutionAction>> = receiver
.filter_map(|item| match item {
NotificationItem::Ready(v) => Some(v),
NotificationItem::None => None,
})
.collect();
assert_eq!(
all_response,
vec![
TaskStatus::Ready(1),
TaskStatus::Ready(2),
TaskStatus::Ready(3),
]
);
}
#[test]
#[traced_test]
fn scenario_0_can_kill_local_executor_via_kill_signal() {
let global: SharedTaskQueue = Arc::new(ConcurrentQueue::bounded(10));
let counts: Arc<Mutex<Vec<TaskStatus<usize, time::Duration, NoSpawner>>>> =
Arc::new(Mutex::new(Vec::new()));
let seed = foundation_compact::rng::rng().next_u64();
let kill_signal = Arc::new(OnSignal::new());
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
Some(kill_signal.clone()),
None,
);
let count_clone = Arc::clone(&counts);
panic_if_failed!(send_typed_task(executor.boxed_engine())
.with_task(Counter("Counter1", 0, 3, 3))
.on_next(move |next, _| count_clone.lock().unwrap().push(next))
.as_broadcast()
.spawn());
assert_eq!(
executor.run_once(),
ProgressIndicator::CanProgress(Some(State::Progressed))
);
let thread_handle = thread::spawn(move || {
tracing::debug!("Starting sleep timer for 100ms to kill executor");
thread::sleep(Duration::from_millis(100));
tracing::debug!("Trigger kill signal");
kill_signal.turn_on();
tracing::debug!("Executor kill signal sent");
});
tracing::debug!("Blocking main thread with block_on()");
executor.block_on();
tracing::debug!("Thread released from block_on()");
thread_handle.join().expect("should end correctly");
}
#[test]
#[traced_test]
fn scenario_one_task_a_runs_to_completion() {
let global: SharedTaskQueue = Arc::new(ConcurrentQueue::bounded(10));
let counts: Arc<Mutex<Vec<TaskStatus<usize, time::Duration, NoSpawner>>>> =
Arc::new(Mutex::new(Vec::new()));
let seed = foundation_compact::rng::rng().next_u64();
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
None,
None,
);
let count_clone = Arc::clone(&counts);
let on_next = OnNext::on_next(Counter("Counter1", 0, 3, 3), move |next, _engine| {
count_clone.lock().unwrap().push(next)
});
panic_if_failed!(global.push(on_next.into()));
assert_eq!(
executor.run_once(),
ProgressIndicator::CanProgress(Some(State::Progressed))
);
assert_eq!(
executor.run_once(),
ProgressIndicator::CanProgress(Some(State::Progressed))
);
assert_eq!(executor.run_once(), ProgressIndicator::NoWork);
assert_eq!(executor.number_of_sleepers(), 0);
let count_list = counts.lock().unwrap().clone();
assert_eq!(
count_list,
vec![TaskStatus::Ready(1), TaskStatus::Ready(2),]
);
}
#[test]
#[traced_test]
fn scenario_one_can_use_local_executor_builder_to_queue_task() {
let global: SharedTaskQueue = Arc::new(ConcurrentQueue::bounded(10));
let counts: Arc<Mutex<Vec<TaskStatus<usize, time::Duration, NoSpawner>>>> =
Arc::new(Mutex::new(Vec::new()));
let seed = foundation_compact::rng::rng().next_u64();
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
None,
None,
);
let count_clone = Arc::clone(&counts);
panic_if_failed!(send_typed_task(executor.boxed_engine())
.with_task(Counter("Counter1", 0, 3, 3))
.on_next(move |next, _| count_clone.lock().unwrap().push(next))
.as_broadcast()
.spawn());
assert_eq!(
executor.run_once(),
ProgressIndicator::CanProgress(Some(State::Progressed))
);
assert_eq!(
executor.run_once(),
ProgressIndicator::CanProgress(Some(State::Progressed))
);
assert_eq!(executor.run_once(), ProgressIndicator::NoWork);
assert_eq!(executor.number_of_sleepers(), 0);
let count_list = counts.lock().unwrap().clone();
assert_eq!(
count_list,
vec![TaskStatus::Ready(1), TaskStatus::Ready(2),]
);
}
#[test]
#[traced_test]
fn scenario_2_task_a_goes_to_sleep_as_only_task_in_queue() {
let global: SharedTaskQueue = Arc::new(ConcurrentQueue::bounded(10));
let counts: Arc<Mutex<Vec<TaskStatus<usize, time::Duration, NoSpawner>>>> =
Arc::new(Mutex::new(Vec::new()));
let seed = foundation_compact::rng::rng().next_u64();
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
None,
None,
);
let count_clone = Arc::clone(&counts);
panic_if_failed!(global.push(Box::new(OnNext::on_next(
Counter("Counter1", 10, 20, 12),
move |next, _engine| { count_clone.lock().unwrap().push(next) }
))));
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(counts.lock().unwrap().clone(), vec![TaskStatus::Ready(11),]);
assert_eq!(
executor.run_once(),
ProgressIndicator::SpinWait(time::Duration::from_millis(5))
);
assert_eq!(counts.lock().unwrap().clone(), vec![TaskStatus::Ready(11),]);
thread::sleep(time::Duration::from_millis(5));
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![TaskStatus::Ready(11), TaskStatus::Ready(13),]
);
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![
TaskStatus::Ready(11),
TaskStatus::Ready(13),
TaskStatus::Ready(14),
]
);
}
#[test]
#[traced_test]
fn scenario_3_task_goes_to_sleep_as_highest_priority_on_wakeup_with_other_tasks() {
let global: SharedTaskQueue = Arc::new(ConcurrentQueue::bounded(10));
let counts: Arc<Mutex<Vec<(&'static str, TaskStatus<usize, time::Duration, NoSpawner>)>>> =
Arc::new(Mutex::new(Vec::new()));
let seed = foundation_compact::rng::rng().next_u64();
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Top,
NoYielder,
time::Duration::from_secs(10),
None,
None,
);
let count_clone = Arc::clone(&counts);
panic_if_failed!(global.push(Box::new(OnNext::on_next(
Counter("Counter1", 0, 4, 2),
move |next, _| count_clone.lock().unwrap().push(("Counter1", next))
))));
let count_clone2 = Arc::clone(&counts);
panic_if_failed!(global.push(
OnNext::on_next(Counter("Counter2", 0, 20, 10), move |next, _| count_clone2
.lock()
.unwrap()
.push(("Counter2", next)))
.into()
));
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![("Counter1", TaskStatus::Ready(1)),]
);
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![("Counter1", TaskStatus::Ready(1)),]
);
assert_eq!(executor.number_of_sleepers(), 1);
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![
("Counter1", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(1)),
]
);
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![
("Counter1", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(2)),
]
);
tracing::debug!("Sleeping thread for 5ms");
thread::sleep(time::Duration::from_millis(5));
tracing::debug!("Finished sleeping thread for 5ms");
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![
("Counter1", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(2)),
("Counter1", TaskStatus::Ready(3)),
]
);
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![
("Counter1", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(2)),
("Counter1", TaskStatus::Ready(3)),
]
);
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![
("Counter1", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(2)),
("Counter1", TaskStatus::Ready(3)),
("Counter2", TaskStatus::Ready(3)),
]
);
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![
("Counter1", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(2)),
("Counter1", TaskStatus::Ready(3)),
("Counter2", TaskStatus::Ready(3)),
("Counter2", TaskStatus::Ready(4)),
]
);
}
#[test]
#[traced_test]
fn scenario_4_task_goes_to_sleep_as_lowest_priority_on_wakeup_with_other_tasks() {
let global: SharedTaskQueue = Arc::new(ConcurrentQueue::bounded(10));
let counts: Arc<Mutex<Vec<(&'static str, TaskStatus<usize, time::Duration, NoSpawner>)>>> =
Arc::new(Mutex::new(Vec::new()));
let seed = foundation_compact::rng::rng().next_u64();
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
None,
None,
);
let count_clone = Arc::clone(&counts);
panic_if_failed!(global.push(
OnNext::on_next(Counter("Counter1", 0, 4, 2), move |next, _| count_clone
.lock()
.unwrap()
.push(("Counter1", next)))
.into()
));
let count_clone2 = Arc::clone(&counts);
panic_if_failed!(global.push(
OnNext::on_next(Counter("Counter2", 0, 5, 10), move |next, _| count_clone2
.lock()
.unwrap()
.push(("Counter2", next)))
.into()
));
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![("Counter1", TaskStatus::Ready(1)),]
);
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![("Counter1", TaskStatus::Ready(1)),]
);
assert_eq!(executor.number_of_sleepers(), 1);
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![
("Counter1", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(1)),
]
);
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![
("Counter1", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(2)),
]
);
tracing::debug!("Sleeping thread for 5ms");
thread::sleep(time::Duration::from_millis(5));
tracing::debug!("Finished sleeping thread for 5ms");
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![
("Counter1", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(2)),
("Counter2", TaskStatus::Ready(3)),
]
);
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![
("Counter1", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(2)),
("Counter2", TaskStatus::Ready(3)),
("Counter2", TaskStatus::Ready(4)),
]
);
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![
("Counter1", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(2)),
("Counter2", TaskStatus::Ready(3)),
("Counter2", TaskStatus::Ready(4)),
]
);
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(_)
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![
("Counter1", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(2)),
("Counter2", TaskStatus::Ready(3)),
("Counter2", TaskStatus::Ready(4)),
("Counter1", TaskStatus::Ready(3)),
]
);
assert!(matches!(executor.run_once(), ProgressIndicator::NoWork));
assert_eq!(
counts.lock().unwrap().clone(),
vec![
("Counter1", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(2)),
("Counter2", TaskStatus::Ready(3)),
("Counter2", TaskStatus::Ready(4)),
("Counter1", TaskStatus::Ready(3)),
]
);
}
#[derive(Clone, Default)]
enum DaemonSpawner {
#[default]
NoSpawning,
InThread,
TopOfThread,
OutofThread,
}
impl ExecutionAction for DaemonSpawner {
fn apply(
&mut self,
key: Option<Entry>,
executor: BoxedExecutionEngine,
) -> crate::valtron::GenericResult<SpawnInfo> {
match self {
DaemonSpawner::NoSpawning => Ok(SpawnInfo::new(SpawnType::None, None, key)),
DaemonSpawner::TopOfThread => {
tracing::debug!("Spawning task as TopOfThread");
match any_task(executor)
.maybe_parent(key)
.with_task(SimpleCounter("SubTask1", 0, 5))
.as_lifted()
.spawn()
{
Ok(info) => Ok(info),
Err(err) => Err(Box::new(err)),
}
}
DaemonSpawner::InThread => {
tracing::debug!("Spawning task as InThread");
match any_task(executor)
.with_task(SimpleCounter("SubTask2", 0, 5))
.as_scheduled()
.spawn()
{
Ok(info) => Ok(info),
Err(err) => Err(Box::new(err)),
}
}
DaemonSpawner::OutofThread => {
tracing::debug!("Spawning task as OutOfThread");
match send_any_task(executor)
.with_task(SimpleCounter("SubTask3", 0, 5))
.as_broadcast()
.spawn()
{
Ok(info) => Ok(info),
Err(err) => Err(Box::new(err)),
}
}
}
}
}
struct DaemonCounter(Arc<AtomicUsize>);
impl TaskIterator for DaemonCounter {
type Ready = ();
type Pending = ();
type Spawner = DaemonSpawner;
fn next_status(&mut self) -> Option<TaskStatus<Self::Ready, Self::Pending, Self::Spawner>> {
let current = self.0.load(Ordering::SeqCst);
println!("Current state: {:?}", current);
if current == 0 {
return Some(TaskStatus::Pending(()));
}
if current == 1 {
return Some(TaskStatus::Ready(()));
}
if current == 2 {
return Some(TaskStatus::Spawn(DaemonSpawner::InThread));
}
if current == 3 {
return Some(TaskStatus::Spawn(DaemonSpawner::TopOfThread));
}
if current == 5 {
return None;
}
println!("Spawn out of thread task");
Some(TaskStatus::Spawn(DaemonSpawner::OutofThread))
}
}
struct SimpleCounter(&'static str, usize, usize);
impl TaskIterator for SimpleCounter {
type Ready = usize;
type Pending = ();
type Spawner = NoSpawner;
fn next_status(&mut self) -> Option<TaskStatus<Self::Ready, Self::Pending, Self::Spawner>> {
let old_count = self.1;
let new_count = old_count + 1;
self.1 = new_count;
tracing::debug!(
"Counter({}) has current count {} from old count {}",
self.0,
new_count,
old_count,
);
if new_count == self.2 {
tracing::debug!("Counter({}) ending task at {}", self.0, new_count,);
return None;
}
if new_count == 3 {
tracing::debug!("Counter({}) going to sleep at {}", self.0, new_count,);
return Some(TaskStatus::Delayed(time::Duration::from_millis(10)));
}
Some(TaskStatus::Ready(new_count))
}
}
#[test]
#[traced_test]
fn scenario_5_task_can_spawn_task_via_actions() {
let global: SharedTaskQueue = Arc::new(ConcurrentQueue::bounded(10));
let counts: Arc<Mutex<Vec<(&'static str, TaskStatus<(), (), DaemonSpawner>)>>> =
Arc::new(Mutex::new(Vec::new()));
let seed = foundation_compact::rng::rng().next_u64();
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
None,
None,
);
let gen_state = Arc::new(AtomicUsize::new(0));
let count_clone = counts.clone();
panic_if_failed!(send_typed_task(executor.boxed_engine())
.with_task(DaemonCounter(gen_state.clone()))
.on_next(move |next, _| count_clone.lock().unwrap().push(("DaemonCounter", next)))
.as_broadcast()
.spawn());
assert_eq!(executor.run_once(), ProgressIndicator::CanProgress(None));
assert_eq!(counts.lock().unwrap().clone(), vec![]);
assert_eq!(executor.number_of_sleepers(), 0);
assert_eq!(executor.number_of_local_tasks(), 1);
gen_state.store(1, Ordering::SeqCst);
assert_eq!(
executor.run_once(),
ProgressIndicator::CanProgress(Some(State::Progressed))
);
assert_eq!(
counts.lock().unwrap().clone(),
vec![("DaemonCounter", TaskStatus::Ready(())),]
);
assert_eq!(executor.number_of_sleepers(), 0);
assert_eq!(executor.number_of_local_tasks(), 1);
gen_state.store(2, Ordering::SeqCst);
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(Some(_))
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![("DaemonCounter", TaskStatus::Ready(())),]
);
assert_eq!(executor.number_of_sleepers(), 0);
assert_eq!(executor.number_of_local_tasks(), 2);
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(Some(_))
));
assert_eq!(
counts.lock().unwrap().clone(),
vec![("DaemonCounter", TaskStatus::Ready(())),]
);
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(Some(_))
));
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(Some(_))
));
assert_eq!(executor.number_of_sleepers(), 0);
assert_eq!(executor.number_of_local_tasks(), 5);
gen_state.store(0, Ordering::SeqCst);
assert!(matches!(
executor.run_once(),
ProgressIndicator::CanProgress(None)
));
assert_eq!(executor.number_of_sleepers(), 0);
assert_eq!(executor.number_of_local_tasks(), 5);
}
#[test]
#[traced_test]
fn scenario_5_task_a_spawns_task_b_that_goes_to_sleep_but_also_ties_task_a_to_its_readiness() {
let global: SharedTaskQueue = Arc::new(ConcurrentQueue::bounded(10));
let counts: Arc<Mutex<Vec<(&'static str, TaskStatus<(), (), DaemonSpawner>)>>> =
Arc::new(Mutex::new(Vec::new()));
let seed = foundation_compact::rng::rng().next_u64();
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
None,
None,
);
let gen_state = Arc::new(AtomicUsize::new(0));
let count_clone = counts.clone();
panic_if_failed!(send_typed_task(executor.boxed_engine())
.with_task(DaemonCounter(gen_state.clone()))
.on_next(move |next, _| count_clone.lock().unwrap().push(("DaemonCounter", next)))
.as_broadcast()
.spawn());
assert_eq!(executor.run_once(), ProgressIndicator::CanProgress(None));
assert_eq!(counts.lock().unwrap().clone(), vec![]);
assert_eq!(executor.number_of_sleepers(), 0);
assert_eq!(executor.number_of_local_tasks(), 1);
gen_state.store(1, Ordering::SeqCst);
let mut count = 5;
while count > 0 {
executor.run_once();
count -= 1;
}
assert_eq!(executor.number_of_local_tasks(), 1);
assert_eq!(executor.number_of_sleepers(), 0);
gen_state.store(2, Ordering::SeqCst);
count = 5;
while count > 0 {
executor.run_once();
count -= 1;
}
gen_state.store(3, Ordering::SeqCst);
count = 5;
while count > 0 {
executor.run_once();
count -= 1;
}
gen_state.store(4, Ordering::SeqCst);
count = 2;
while count > 0 {
executor.run_once();
count -= 1;
}
gen_state.store(5, Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(100 * 6));
executor.run_until(|state| state == ProgressIndicator::NoWork);
assert_eq!(executor.number_of_local_tasks(), 0);
assert_eq!(executor.number_of_sleepers(), 0);
assert_eq!(
counts.lock().unwrap().clone(),
vec![
("DaemonCounter", TaskStatus::Ready(())),
("DaemonCounter", TaskStatus::Ready(())),
("DaemonCounter", TaskStatus::Ready(())),
("DaemonCounter", TaskStatus::Ready(())),
("DaemonCounter", TaskStatus::Ready(())),
]
);
}
#[test]
#[traced_test]
fn scenario_6_can_run_until_ready_signal_is_seen_and_no_work_remains_when_condition_hits() {
let global: SharedTaskQueue = Arc::new(ConcurrentQueue::bounded(10));
let counts: Arc<Mutex<Vec<(&'static str, TaskStatus<usize, time::Duration, NoSpawner>)>>> =
Arc::new(Mutex::new(Vec::new()));
let seed = foundation_compact::rng::rng().next_u64();
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
None,
None,
);
let count_clone = Arc::clone(&counts);
panic_if_failed!(global.push(
OnNext::on_next(Counter("Counter1", 0, 4, 2), move |next, _| count_clone
.lock()
.unwrap()
.push(("Counter1", next)))
.into()
));
let count_clone2 = Arc::clone(&counts);
panic_if_failed!(global.push(
OnNext::on_next(Counter("Counter2", 0, 5, 10), move |next, _| count_clone2
.lock()
.unwrap()
.push(("Counter2", next)))
.into()
));
let count_clone3 = Arc::clone(&counts);
executor.run_until(move |_| {
let items = count_clone3.lock().unwrap();
if items.len() >= 6 {
return true;
}
false
});
assert_eq!(
counts.lock().unwrap().clone(),
vec![
("Counter1", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(2)),
("Counter2", TaskStatus::Ready(3)),
("Counter2", TaskStatus::Ready(4)),
("Counter1", TaskStatus::Ready(3)),
]
);
assert_eq!(executor.run_once(), ProgressIndicator::NoWork);
}
#[test]
#[traced_test]
fn scenario_6_can_run_until_ready_signal_is_seen_and_no_work_remains_when_condition_doesnt_hits(
) {
let global: SharedTaskQueue = Arc::new(ConcurrentQueue::bounded(10));
let counts: Arc<Mutex<Vec<(&'static str, TaskStatus<usize, time::Duration, NoSpawner>)>>> =
Arc::new(Mutex::new(Vec::new()));
let seed = foundation_compact::rng::rng().next_u64();
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
None,
None,
);
let count_clone = Arc::clone(&counts);
panic_if_failed!(global.push(
OnNext::on_next(Counter("Counter1", 0, 4, 2), move |next, _| count_clone
.lock()
.unwrap()
.push(("Counter1", next)))
.into()
));
let count_clone2 = Arc::clone(&counts);
panic_if_failed!(global.push(
OnNext::on_next(Counter("Counter2", 0, 5, 10), move |next, _| count_clone2
.lock()
.unwrap()
.push(("Counter2", next)))
.into()
));
let count_clone3 = Arc::clone(&counts);
executor.run_until(move |_| {
let items = count_clone3.lock().unwrap();
if items.len() >= 7 {
return true;
}
false
});
assert_eq!(
counts.lock().unwrap().clone(),
vec![
("Counter1", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(2)),
("Counter2", TaskStatus::Ready(3)),
("Counter2", TaskStatus::Ready(4)),
("Counter1", TaskStatus::Ready(3)),
]
);
assert_eq!(executor.run_once(), ProgressIndicator::NoWork);
}
#[test]
#[traced_test]
fn scenario_6_can_run_until_ready_signal_is_seen_with_pending_work() {
let global: SharedTaskQueue = Arc::new(ConcurrentQueue::bounded(10));
let counts: Arc<Mutex<Vec<(&'static str, TaskStatus<usize, time::Duration, NoSpawner>)>>> =
Arc::new(Mutex::new(Vec::new()));
let seed = foundation_compact::rng::rng().next_u64();
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
None,
None,
);
let count_clone = Arc::clone(&counts);
panic_if_failed!(global.push(
OnNext::on_next(Counter("Counter1", 0, 4, 2), move |next, _| count_clone
.lock()
.unwrap()
.push(("Counter1", next)))
.into()
));
let count_clone2 = Arc::clone(&counts);
panic_if_failed!(global.push(
OnNext::on_next(Counter("Counter2", 0, 5, 10), move |next, _| count_clone2
.lock()
.unwrap()
.push(("Counter2", next)))
.into()
));
let count_clone3 = Arc::clone(&counts);
executor.run_until(move |_| {
let items = count_clone3.lock().unwrap();
if items.len() >= 4 {
return true;
}
false
});
assert_eq!(
counts.lock().unwrap().clone(),
vec![
("Counter1", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(1)),
("Counter2", TaskStatus::Ready(2)),
("Counter2", TaskStatus::Ready(3)),
]
);
}
struct DependsCounter {
count: usize,
max: usize,
trigger_at: usize,
depends_returned: bool,
signal: BoolSignal,
}
impl DependsCounter {
fn new(trigger_at: usize, signal: BoolSignal) -> Self {
Self {
count: 0,
max: 10,
trigger_at,
depends_returned: false,
signal,
}
}
}
impl TaskIterator for DependsCounter {
type Ready = usize;
type Spawner = NoSpawner;
type Pending = time::Duration;
fn next_status(&mut self) -> Option<TaskStatus<Self::Ready, Self::Pending, Self::Spawner>> {
self.count += 1;
if self.count >= self.max {
return None;
}
if self.count == self.trigger_at && !self.depends_returned {
self.depends_returned = true;
return Some(TaskStatus::Depends(sync::Arc::new(self.signal.clone())));
}
Some(TaskStatus::Ready(self.count))
}
}
struct CounterSignal {
polls: AtomicUsize,
threshold: usize,
}
impl CounterSignal {
fn new(threshold: usize) -> Self {
Self {
polls: AtomicUsize::new(0),
threshold,
}
}
}
impl EventReadiness for CounterSignal {
fn is_ready(&self, _dur: Option<time::Duration>) -> bool {
self.polls.fetch_add(1, Ordering::SeqCst) >= self.threshold
}
}
#[test]
#[traced_test]
fn depends_false_registers_as_sleeper() {
let global: SharedTaskQueue = sync::Arc::new(ConcurrentQueue::bounded(10));
let signal = BoolSignal::new(false);
let seed = foundation_compact::rng::rng().next_u64();
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
None,
None,
);
let signal_clone = signal.clone();
panic_if_failed!(global.push(Box::new(OnNext::on_next(
DependsCounter::new(1, signal),
move |_next: TaskStatus<usize, time::Duration, NoSpawner>, _engine| {
}
))));
let result = executor.run_once();
assert!(matches!(result, ProgressIndicator::CanProgress(_)));
assert_eq!(executor.number_of_sleepers(), 1);
signal_clone.clone_inner().store(true, Ordering::SeqCst);
let result2 = executor.run_once();
assert!(matches!(result2, ProgressIndicator::CanProgress(_)));
assert_eq!(executor.number_of_sleepers(), 0);
}
#[test]
#[traced_test]
fn depends_signal_wakes_task_and_continues() {
let global: SharedTaskQueue = sync::Arc::new(ConcurrentQueue::bounded(10));
let signal = BoolSignal::new(false);
let seed = foundation_compact::rng::rng().next_u64();
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
None,
None,
);
let signal_clone = signal.clone_inner();
panic_if_failed!(global.push(Box::new(OnNext::on_next(
DependsCounter::new(1, signal),
move |_next: TaskStatus<usize, time::Duration, NoSpawner>, _engine| {}
))));
let r1 = executor.run_once();
assert!(matches!(r1, ProgressIndicator::CanProgress(_)));
assert_eq!(executor.number_of_sleepers(), 1);
signal_clone.store(true, Ordering::SeqCst);
let r2 = executor.run_once();
assert!(matches!(r2, ProgressIndicator::CanProgress(_)));
assert_eq!(executor.number_of_sleepers(), 0);
}
#[test]
#[traced_test]
fn depends_true_violation_pushed_to_back() {
let global: SharedTaskQueue = sync::Arc::new(ConcurrentQueue::bounded(10));
let signal = BoolSignal::new(true);
let seed = foundation_compact::rng::rng().next_u64();
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
None,
None,
);
panic_if_failed!(global.push(Box::new(OnNext::on_next(
DependsCounter::new(1, signal),
move |_next: TaskStatus<usize, time::Duration, NoSpawner>, _engine| {}
))));
let result = executor.run_once();
assert!(matches!(result, ProgressIndicator::CanProgress(_)));
assert_eq!(executor.number_of_sleepers(), 0);
assert_eq!(
executor.state.inner.depends_true_violations.borrow().len(),
1
);
}
#[test]
#[traced_test]
fn custom_event_readiness_flips_after_polls() {
let global: SharedTaskQueue = sync::Arc::new(ConcurrentQueue::bounded(10));
let signal = sync::Arc::new(CounterSignal::new(2));
let seed = foundation_compact::rng::rng().next_u64();
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
None,
None,
);
panic_if_failed!(global.push(Box::new(OnNext::on_next(
DependsCounterCustom {
count: 0,
max: 3,
signal: signal.clone(),
},
move |_next: TaskStatus<usize, time::Duration, NoSpawner>, _engine| {}
))));
let r1 = executor.run_once();
assert!(matches!(r1, ProgressIndicator::CanProgress(_)));
assert_eq!(executor.number_of_sleepers(), 1);
let r2 = executor.run_once();
assert!(matches!(r2, ProgressIndicator::CanProgress(_)));
assert_eq!(executor.number_of_sleepers(), 1);
let r3 = executor.run_once();
assert!(matches!(r3, ProgressIndicator::CanProgress(_)));
assert_eq!(executor.number_of_sleepers(), 0);
}
struct DependsCounterCustom {
count: usize,
max: usize,
signal: sync::Arc<CounterSignal>,
}
impl TaskIterator for DependsCounterCustom {
type Ready = usize;
type Spawner = NoSpawner;
type Pending = time::Duration;
fn next_status(&mut self) -> Option<TaskStatus<Self::Ready, Self::Pending, Self::Spawner>> {
self.count += 1;
if self.count >= self.max {
return None;
}
Some(TaskStatus::Depends(self.signal.clone()))
}
}
#[test]
#[traced_test]
fn bool_signal_clone_and_flip() {
let global: SharedTaskQueue = sync::Arc::new(ConcurrentQueue::bounded(10));
let signal = BoolSignal::new(false);
let atomic = signal.clone_inner();
let seed = foundation_compact::rng::rng().next_u64();
let executor = LocalThreadExecutor::from_seed(
seed,
"1".into(),
global.clone(),
IdleMan::new(
3,
None,
SleepyMan::new(3, ExponentialBackoffDecider::default()),
),
PriorityOrder::Bottom,
NoYielder,
time::Duration::from_secs(10),
None,
None,
);
panic_if_failed!(global.push(Box::new(OnNext::on_next(
DependsCounter::new(1, signal.clone()),
move |_next: TaskStatus<usize, time::Duration, NoSpawner>, _engine| {}
))));
assert!(!signal.is_ready(None));
let r1 = executor.run_once();
assert!(matches!(r1, ProgressIndicator::CanProgress(_)));
assert_eq!(executor.number_of_sleepers(), 1);
atomic.store(true, Ordering::SeqCst);
assert!(signal.is_ready(None));
let r2 = executor.run_once();
assert!(matches!(r2, ProgressIndicator::CanProgress(_)));
assert_eq!(executor.number_of_sleepers(), 0);
}
}