Skip to main content

shuttle_engine/runtime/
execution.rs

1use crate::runtime::failure::{init_panic_hook, persist_failure};
2use crate::runtime::storage::{StorageKey, StorageMap};
3use crate::runtime::task::clock::VectorClock;
4use crate::runtime::task::labels::Labels;
5use crate::runtime::task::{ChildLabelFn, Task, TaskId, TaskName, TaskSignature, DEFAULT_INLINE_TASKS};
6use crate::runtime::thread;
7use crate::runtime::thread::continuation::PooledContinuation;
8use crate::scheduler::{Schedule, Scheduler};
9use crate::sync_types::{ResourceSignature, ResourceType};
10use crate::thread_support::thread_fn;
11use crate::{backtrace_enabled, Config, MaxSteps, UNGRACEFUL_SHUTDOWN_CONFIG};
12use scoped_tls::scoped_thread_local;
13use smallvec::SmallVec;
14use std::any::Any;
15use std::cell::RefCell;
16use std::collections::HashMap;
17use std::fmt::Debug;
18use std::future::Future;
19use std::panic::{self, Location};
20use std::rc::Rc;
21use std::sync::Arc;
22use tracing::{trace, Span};
23
24#[allow(deprecated)]
25use super::task::Tag;
26
27// We use this scoped TLS to smuggle the ExecutionState, which is not 'static, across tasks that
28// need access to it (to spawn new tasks, interrogate task status, etc).
29scoped_thread_local! {
30    static EXECUTION_STATE: RefCell<ExecutionState>
31}
32
33// The reason this is separated out from `ExecutionState` is to ensure that we're always able to persist the schedule.
34// If we don't do this, then we may panic while borrowing `ExecutionState`, and then not be able to emit the schedule.
35// If we then panic again while trying to handle the panic, such that the panic becomes an abort, we will never log
36// the schedule.
37//
38// It is expected that if the `ExecutionState` exists, then this will exist, and any usage of this happens through the
39// `ExecutionState`, or at a point where it is known that the `ExecutionState` must exist (eg. when serializing on a panic).
40thread_local! {
41    static CURRENT_SCHEDULE: CurrentSchedule = CurrentSchedule::default();
42}
43
44#[derive(Debug, Default)]
45pub struct CurrentSchedule {
46    current_schedule: RefCell<Schedule>,
47}
48
49impl CurrentSchedule {
50    fn init(schedule: Schedule) {
51        CURRENT_SCHEDULE.with(|cs| *cs.current_schedule.borrow_mut() = schedule)
52    }
53
54    /// Add the given task ID as the next step of the schedule.
55    fn push_task(tid: TaskId) {
56        CURRENT_SCHEDULE.with(|cs| cs.current_schedule.borrow_mut().push_task(tid))
57    }
58
59    /// Add a choice of a random u64 value as the next step of the schedule
60    fn push_random() {
61        CURRENT_SCHEDULE.with(|cs| cs.current_schedule.borrow_mut().push_random())
62    }
63
64    /// Return the number of steps in the schedule
65    pub fn len() -> usize {
66        CURRENT_SCHEDULE.with(|cs| (*cs.current_schedule.borrow()).len())
67    }
68
69    /// Returns a clone of the inner schedule
70    pub fn get_schedule() -> Schedule {
71        CURRENT_SCHEDULE.with(|cs| (*cs.current_schedule.borrow()).clone())
72    }
73}
74
75thread_local! {
76    #[allow(clippy::complexity)]
77    #[allow(deprecated)]
78    pub static TASK_ID_TO_TAGS: RefCell<HashMap<TaskId, Arc<dyn Tag>>> = RefCell::new(HashMap::new());
79}
80
81thread_local! {
82    pub static LABELS: RefCell<HashMap<TaskId, Labels>> = RefCell::new(HashMap::new());
83}
84
85/// An `Execution` encapsulates a single run of a function under test against a chosen scheduler.
86/// Its only useful method is `Execution::run`, which executes the function to completion.
87///
88/// The key thing that an `Execution` manages is the `ExecutionState`, which contains all the
89/// mutable state a test's tasks might need access to during execution (to block/unblock tasks,
90/// spawn new tasks, etc). The `Execution` makes this state available through the `EXECUTION_STATE`
91/// static variable, but clients get access to it by calling `ExecutionState::with`.
92pub struct Execution {
93    scheduler: Rc<RefCell<dyn Scheduler>>,
94    initial_schedule: Schedule,
95}
96
97impl std::fmt::Debug for Execution {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        f.debug_struct("Execution").finish_non_exhaustive()
100    }
101}
102
103impl Execution {
104    /// Construct a new execution that will use the given scheduler. The execution should then be
105    /// invoked via its `run` method, which takes as input the closure for task 0.
106    pub fn new(scheduler: Rc<RefCell<dyn Scheduler>>, initial_schedule: Schedule) -> Self {
107        Self {
108            scheduler,
109            initial_schedule,
110        }
111    }
112}
113
114#[derive(Debug)]
115enum StepError {
116    // Contains the panic payload of the task that failed.
117    TaskFailure(Box<dyn Any + Send>),
118    // The scheduler didn't make a decision. Indicates a scheduler error.
119    SchedulingError,
120    // Scheduling deetected a deadlock.
121    Deadlock,
122    // We exceeded the step bound.
123    StepBoundExceeded,
124    // Task panic and `config.immediately_return_on_panic` is set to `true`.
125    TaskPanicEarlyReturn,
126}
127
128impl StepError {
129    fn persist_failure(&self, config: &Config) {
130        if let StepError::StepBoundExceeded = self {
131            if let MaxSteps::ContinueAfter(_) = config.max_steps {
132                return;
133            }
134        }
135        persist_failure(config);
136    }
137}
138
139impl Execution {
140    /// Run a function to be tested, taking control of scheduling it and any tasks it might spawn.
141    /// This function runs until `f` and all tasks spawned by `f` have terminated, or until the
142    /// scheduler returns `None`, indicating the execution should not be explored any further.
143    pub fn run<F>(mut self, config: &Config, f: F, caller: &'static Location<'static>)
144    where
145        F: FnOnce() + Send + 'static,
146    {
147        let state = RefCell::new(ExecutionState::new(config.clone(), Rc::clone(&self.scheduler)));
148
149        init_panic_hook(config.clone());
150        CurrentSchedule::init(self.initial_schedule.clone());
151        UNGRACEFUL_SHUTDOWN_CONFIG.set(config.ungraceful_shutdown_config);
152
153        EXECUTION_STATE.set(&state, move || {
154            // Spawn `f` as the first task
155            ExecutionState::spawn_main_thread(
156                Box::new(move || thread_fn(f, true, Default::default())),
157                config.stack_size,
158                caller,
159            );
160
161                // Run the test to completion
162                match self.run_to_completion(UNGRACEFUL_SHUTDOWN_CONFIG.get().immediately_return_on_panic) {
163                    Ok(()) => {},
164                    Err(e) => {
165                        e.persist_failure(config);
166
167                        match e {
168                            StepError::TaskFailure(payload) => {
169                                eprintln!("test panicked in task '{}'", ExecutionState::failing_task());
170
171                                panic::resume_unwind(payload);
172                            }
173                            StepError::Deadlock => {
174                                let blocked_tasks = ExecutionState::with(|state|
175                                    state
176                                    .tasks
177                                    .iter()
178                                    .filter(|t| !t.finished())
179                                    .map(|t| t.format_for_deadlock())
180                                    .collect::<Vec<_>>());
181
182                                // Collecting backtraces is expensive, so we only want to do it if the user opts in to collecting them.
183                                if !backtrace_enabled() {
184                                    eprintln!("Test deadlocked, and {} is not set. If either of those are set then the backtrace of each task will be collected and printed as part of the panic message.", crate::CAPTURE_BACKTRACE)
185                                }
186
187                                panic!("deadlock! blocked tasks: [{}]", blocked_tasks.join(", "));
188                            }
189                            StepError::SchedulingError => panic!("no task was scheduled\nThis indicates an issue with the scheduler."),
190                            StepError::StepBoundExceeded => {
191                                if let MaxSteps::FailAfter(max_steps) = config.max_steps {
192                                    panic!("exceeded max_steps bound {max_steps}. this might be caused by an unfair schedule (e.g., a spin loop)?");
193                                }
194                            }
195                            StepError::TaskPanicEarlyReturn => panic::resume_unwind(Box::new("Task panicked, and early return is enabled.")),
196                        }
197                    }}
198
199
200                // Cleanup the state before it goes out of `EXECUTION_STATE` scope
201                ExecutionState::cleanup();
202            });
203    }
204
205    fn enter_task_span() {
206        // Enter the Task's span
207        // (Note that if any issues arise with spans and tracing, then
208        // 1) calling `exit` until `None` before entering the `Task`s `Span`,
209        // 2) storing the entirety of the `span_stack` when creating the `Task`, and
210        // 3) storing `top_level_span` as a stack
211        // should be tried.)
212        ExecutionState::with(|state| {
213            tracing::dispatcher::get_default(|subscriber| {
214                if let Some(span_id) = tracing::Span::current().id().as_ref() {
215                    subscriber.exit(span_id);
216                }
217
218                // The `span_stack` stores `Span`s such that the top of the stack is the outermost `Span`,
219                // meaning that parents (left-most when printed) are entered first.
220                while let Some(span) = state.current_mut().span_stack.pop() {
221                    if let Some(span_id) = span.id().as_ref() {
222                        subscriber.enter(span_id)
223                    }
224                }
225
226                if state.config.record_steps_in_span {
227                    state.current().step_span.record("i", CurrentSchedule::len());
228                }
229            });
230        });
231    }
232
233    fn exit_task_span() {
234        // Leave the Task's span and store the exited `Span` stack in order to restore it the next time the Task is run
235        ExecutionState::with(|state| {
236            tracing::dispatcher::get_default(|subscriber| {
237                debug_assert!(state.current().span_stack.is_empty());
238                while let Some(span_id) = tracing::Span::current().id().as_ref() {
239                    state.current_mut().span_stack.push(tracing::Span::current().clone());
240                    subscriber.exit(span_id);
241                }
242
243                if let Some(span_id) = state.top_level_span.id().as_ref() {
244                    subscriber.enter(span_id)
245                }
246            });
247        });
248    }
249
250    /// Run the execution to completion.
251    #[inline]
252    fn run_to_completion(&mut self, immediately_return_on_panic: bool) -> Result<(), StepError> {
253        loop {
254            let next_step: Option<Rc<RefCell<PooledContinuation>>> = ExecutionState::with(|state| {
255                state.schedule()?;
256                state.advance_to_next_task();
257
258                match state.current_task {
259                    ScheduledTask::Some(tid) => {
260                        let task = state.get(tid);
261                        Ok(Some(task.continuation.clone()))
262                    }
263                    ScheduledTask::Finished => {
264                        // The scheduler decided we're finished, so there are either no runnable tasks,
265                        // or all runnable tasks are detached and there are no unfinished attached
266                        // tasks. Therefore, it's a deadlock if there are unfinished attached tasks.
267                        if state.tasks.iter().any(|t| !t.finished() && !t.detached) {
268                            Err(StepError::Deadlock)
269                        } else {
270                            Ok(None)
271                        }
272                    }
273                    ScheduledTask::Stopped => Ok(None),
274                    ScheduledTask::None => Err(StepError::SchedulingError),
275                }
276            })?;
277
278            // Run a single step of the chosen task.
279            let ret = match next_step {
280                Some(continuation) => {
281                    Execution::enter_task_span();
282
283                    let result = panic::catch_unwind(panic::AssertUnwindSafe(|| continuation.borrow_mut().resume()));
284
285                    Execution::exit_task_span();
286
287                    result
288                }
289                None => return Ok(()),
290            };
291
292            match ret {
293                // Task finished
294                Ok(true) => {
295                    crate::annotations::record_task_terminated();
296                    ExecutionState::with(|state| state.current_mut().finish());
297                }
298                // Task yielded
299                Ok(false) => {
300                    // We may have `switch`ed out of the task before we finished unwinding the stack (ie. a `drop` handler calls `switch`).
301                    // If `immediately_return_on_panic` is set, we will then return. If we don't do this, then we run the risk of panicking
302                    // again in some other task, which would result in the test aborting.
303                    if immediately_return_on_panic && std::thread::panicking() {
304                        ExecutionState::with(|state| state.current_task = ScheduledTask::Stopped);
305                        return Err(StepError::TaskPanicEarlyReturn);
306                    }
307                }
308                // Task failed
309                Err(e) => return Err(StepError::TaskFailure(e)),
310            }
311        }
312    }
313}
314
315/// `ExecutionState` contains the portion of a single execution's state that needs to be reachable
316/// from within a task's execution. It tracks which tasks exist and their states, as well as which
317/// tasks are pending spawn.
318pub struct ExecutionState {
319    pub config: Config,
320    // invariant: tasks are never removed from this list
321    tasks: SmallVec<[Task; DEFAULT_INLINE_TASKS]>,
322    // invariant: if this transitions to Stopped or Finished, it can never change again
323    current_task: ScheduledTask,
324    // the task the scheduler has chosen to run next
325    next_task: ScheduledTask,
326    // whether the current task has asked to yield
327    has_yielded: bool,
328    // the number of scheduling decisions made so far
329    context_switches: usize,
330    // the schedule length last time `reset_stop_bound()` was called
331    pub steps_reset_at: usize,
332
333    // static values for the current execution
334    storage: StorageMap,
335
336    scheduler: Rc<RefCell<dyn Scheduler>>,
337
338    in_cleanup: bool,
339
340    #[cfg(debug_assertions)]
341    has_cleaned_up: bool,
342
343    // The `Span` which the `ExecutionState` was created under. Will be the parent of all `Task` `Span`s
344    pub top_level_span: Span,
345
346    // Persistent Vec used as a bump allocator for references to runnable tasks to avoid slow allocation
347    // on each scheduling decision. Should not be used outside of the `schedule` function
348    runnable_tasks: Vec<*const Task>,
349}
350
351impl std::fmt::Debug for ExecutionState {
352    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353        f.debug_struct("ExecutionState").finish_non_exhaustive()
354    }
355}
356
357#[derive(Debug, PartialEq, Eq, Clone, Copy)]
358enum ScheduledTask {
359    None,         // no task has ever been scheduled
360    Some(TaskId), // this task is running
361    Stopped,      // the scheduler asked us to stop running
362    Finished,     // all tasks have finished running
363}
364
365impl ScheduledTask {
366    fn id(&self) -> Option<TaskId> {
367        match self {
368            ScheduledTask::Some(tid) => Some(*tid),
369            _ => None,
370        }
371    }
372
373    fn take(&mut self) -> Self {
374        std::mem::replace(self, ScheduledTask::None)
375    }
376}
377
378/// Error type for when an `ExecutionState::with` fails
379#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
380pub enum ExecutionStateBorrowError {
381    /// `ExecutionState` is currently not set
382    NotSet,
383    /// We are trying to borrow `ExecutionState` while it is already borrowed
384    AlreadyBorrowed,
385}
386
387impl ExecutionState {
388    fn new(config: Config, scheduler: Rc<RefCell<dyn Scheduler>>) -> Self {
389        Self {
390            config,
391            tasks: SmallVec::new(),
392            current_task: ScheduledTask::None,
393            next_task: ScheduledTask::None,
394            has_yielded: false,
395            context_switches: 0,
396            steps_reset_at: 0,
397            storage: StorageMap::new(),
398            scheduler,
399            in_cleanup: false,
400            #[cfg(debug_assertions)]
401            has_cleaned_up: false,
402            top_level_span: tracing::Span::current(),
403            runnable_tasks: Vec::with_capacity(DEFAULT_INLINE_TASKS),
404        }
405    }
406
407    /// Invoke a closure with access to the current execution state. Library code uses this to gain
408    /// access to the state of the execution to influence scheduling (e.g. to register a task as
409    /// blocked).
410    #[inline]
411    #[track_caller]
412    pub fn with<F, T>(f: F) -> T
413    where
414        F: FnOnce(&mut ExecutionState) -> T,
415    {
416        Self::try_with(f).unwrap_or_else(|e| {
417            eprintln!("`ExecutionState::try_with` failed with error: {e:?}");
418            eprintln!(
419                "Backtrace for `with`: {:#?}",
420                std::backtrace::Backtrace::force_capture()
421            );
422            match e {
423                ExecutionStateBorrowError::AlreadyBorrowed => panic!("`ExecutionState::with` panicked because `ExecutionState` is already borrowed."),
424                ExecutionStateBorrowError::NotSet => panic!("`ExecutionState::with` panicked because `ExecutionState` is not set. Are you accessing a Shuttle primitive outside of a Shuttle test?"),
425            }
426        })
427    }
428
429    /// Like `with`, but returns None instead of panicking if there is no current ExecutionState or
430    /// if the current ExecutionState is already borrowed.
431    #[inline]
432    #[track_caller]
433    pub fn try_with<F, T>(f: F) -> Result<T, ExecutionStateBorrowError>
434    where
435        F: FnOnce(&mut ExecutionState) -> T,
436    {
437        trace!(
438            "ExecutionState::try_with called from {:?}",
439            std::panic::Location::caller()
440        );
441        if EXECUTION_STATE.is_set() {
442            EXECUTION_STATE.with(|cell| {
443                if let Ok(mut state) = cell.try_borrow_mut() {
444                    Ok(f(&mut state))
445                } else {
446                    Err(ExecutionStateBorrowError::AlreadyBorrowed)
447                }
448            })
449        } else {
450            Err(ExecutionStateBorrowError::NotSet)
451        }
452    }
453
454    /// A shortcut to get the current task ID
455    pub fn me() -> TaskId {
456        Self::with(|s| s.current().id())
457    }
458
459    /// If there is only one attached, unfinished task and there is at least one detached, unfinished task
460    /// then exiting the attached task will cause the whole execution to exit. As a result, the unfinished
461    /// detached tasks are truncated -- their remaining events will not be executed because the program itself
462    /// has exited. This is relevant because it means that *exiting* a task can be a visible operation
463    /// in that it affects which events are executed.
464    pub fn exit_current_truncates_execution(&self) -> bool {
465        // Strictly speaking, this is only true if there are other runnable detached tasks, but always making the main thread
466        // exit a scheduling point is simpler conceptually
467        if self.current().id() == TaskId::from(0) {
468            return true;
469        }
470
471        // If the current task is detached, then it definitely doesn't truncate the execution
472        if self.current().is_detached() {
473            return false;
474        }
475
476        let mut single_unfinished_attached = false;
477        let mut has_unfinished_detached = false;
478        for t in self.tasks.iter() {
479            let unfinished_attached = !t.finished() && !t.detached;
480            if single_unfinished_attached && unfinished_attached {
481                // there are more than one unfinished attached tasks, so one exiting won't truncate
482                return false;
483            }
484
485            single_unfinished_attached |= unfinished_attached;
486            has_unfinished_detached |= !t.finished() && t.detached;
487        }
488        has_unfinished_detached && single_unfinished_attached
489    }
490
491    fn set_labels_for_new_task(state: &ExecutionState, task_id: TaskId, name: Option<String>) {
492        LABELS.with(|cell| {
493            let mut map = cell.borrow_mut();
494
495            // If parent has labels, inherit them
496            if let Some(parent_task_id) = state.try_current().map(|t| t.id()) {
497                let parent_map = map.get(&parent_task_id);
498                if let Some(parent_map) = parent_map {
499                    let mut child_map = parent_map.clone();
500
501                    // If the parent has a `ChildLabelFn` set, use that to update the child's Labels
502                    if let Some(gen) = parent_map.get::<ChildLabelFn>() {
503                        (gen.0)(task_id, &mut child_map);
504                    }
505
506                    map.insert(task_id, child_map);
507                }
508            }
509
510            // Add any name assigned to the task to its set of Labels
511            if let Some(name) = name {
512                let m = map.entry(task_id).or_default();
513                m.insert(TaskName::from(name));
514            }
515        });
516    }
517
518    // Note: `spawn_thread`, `spawn_main_thread`, and `spawn_future` share some similar logic.
519    // Changes to one of these functions likely need to be propagated to the other two as well.
520    pub fn spawn_main_thread(
521        f: Box<dyn FnOnce() + 'static>,
522        stack_size: usize,
523        caller: &'static Location<'static>,
524    ) -> TaskId {
525        let name = "main-thread".to_string();
526        let mut clock = VectorClock::new();
527
528        let task_id = Self::with(|state| {
529            let parent_span_id = state.top_level_span.id();
530            let task_id = TaskId(state.tasks.len());
531            let tag = state.get_tag_or_default_for_current_task();
532
533            Self::set_labels_for_new_task(state, task_id, Some(name.clone()));
534
535            clock.extend(task_id); // and extend it with an entry for the new thread
536
537            let schedule_len = CurrentSchedule::len();
538
539            let task = Task::from_closure(
540                f,
541                stack_size,
542                task_id,
543                Some(name),
544                clock,
545                parent_span_id,
546                schedule_len,
547                tag,
548                None,
549                TaskSignature::new_parentless(caller),
550            );
551            state.tasks.push(task);
552
553            task_id
554        });
555        crate::annotations::record_task_created(task_id, false);
556        task_id
557    }
558
559    // Note: `spawn_thread`, `spawn_main_thread`, and `spawn_future` share some similar logic.
560    // Changes to one of these functions likely need to be propagated to the other two as well.
561    /// Spawn a new task for a future. This doesn't create a yield point; the caller should do that
562    /// if it wants to give the new task a chance to run immediately.
563    pub fn spawn_future<F>(
564        future: F,
565        stack_size: usize,
566        name: Option<String>,
567        caller: &'static Location<'static>,
568    ) -> TaskId
569    where
570        F: Future<Output = ()> + 'static,
571    {
572        thread::switch();
573        let task_id = Self::with(|state| {
574            let schedule_len = CurrentSchedule::len();
575            let parent_span_id = state.top_level_span.id();
576
577            let task_id = TaskId(state.tasks.len());
578            let tag = state.get_tag_or_default_for_current_task();
579
580            Self::set_labels_for_new_task(state, task_id, name.clone());
581
582            let clock = state.increment_clock_mut(); // Increment the parent's clock
583            clock.extend(task_id); // and extend it with an entry for the new task
584
585            let task = Task::from_future(
586                future,
587                stack_size,
588                task_id,
589                name,
590                clock.clone(),
591                parent_span_id,
592                schedule_len,
593                tag,
594                Some(state.current().id()),
595                state.current_mut().signature.new_child(caller),
596            );
597
598            state.tasks.push(task);
599
600            task_id
601        });
602        crate::annotations::record_task_created(task_id, true);
603        task_id
604    }
605
606    // Note: `spawn_thread`, `spawn_main_thread`, and `spawn_future` share some similar logic.
607    // Changes to one of these functions likely need to be propagated to the other two as well.
608    pub fn spawn_thread(
609        f: Box<dyn FnOnce() + 'static>,
610        stack_size: usize,
611        name: Option<String>,
612        mut initial_clock: Option<VectorClock>,
613        caller: &'static Location<'static>,
614    ) -> TaskId {
615        thread::switch();
616        let task_id = Self::with(|state| {
617            let parent_span_id = state.top_level_span.id();
618            let task_id = TaskId(state.tasks.len());
619            let tag = state.get_tag_or_default_for_current_task();
620
621            Self::set_labels_for_new_task(state, task_id, name.clone());
622
623            let clock = if let Some(ref mut clock) = initial_clock {
624                clock
625            } else {
626                // Inherit the clock of the parent thread (which spawned this task)
627                state.increment_clock_mut()
628            };
629            clock.extend(task_id); // and extend it with an entry for the new thread
630            let clock = clock.clone();
631
632            let task = Task::from_closure(
633                f,
634                stack_size,
635                task_id,
636                name,
637                clock,
638                parent_span_id,
639                CurrentSchedule::len(),
640                tag,
641                Some(state.current().id()),
642                state.current_mut().signature.new_child(caller),
643            );
644            state.tasks.push(task);
645
646            task_id
647        });
648        crate::annotations::record_task_created(task_id, false);
649        task_id
650    }
651
652    /// Prepare this ExecutionState to be dropped. Call this before dropping so that the tasks have
653    /// a chance to run their drop handlers while `EXECUTION_STATE` is still in scope.
654    fn cleanup() {
655        // A slightly delicate dance here: we need to drop the tasks from outside of `Self::with`,
656        // because a task's Drop impl might want to call back into `ExecutionState` (to check
657        // `should_stop()`). So we pull the tasks out of the `ExecutionState`, leaving it in an
658        // invalid state, but no one should still be accessing the tasks anyway.
659        let (mut tasks, final_state) = Self::with(|state| {
660            state.in_cleanup = true;
661            assert!(state.current_task == ScheduledTask::Stopped || state.current_task == ScheduledTask::Finished);
662            (std::mem::take(&mut state.tasks), state.current_task)
663        });
664
665        for task in tasks.drain(..) {
666            assert!(
667                final_state == ScheduledTask::Stopped || task.finished() || task.detached,
668                "execution finished but task is not"
669            );
670            Rc::try_unwrap(task.continuation)
671                .map_err(|_| ())
672                .expect("couldn't cleanup a future");
673        }
674
675        while Self::with(|state| state.storage.pop()).is_some() {}
676
677        TASK_ID_TO_TAGS.with(|cell| cell.borrow_mut().clear());
678        LABELS.with(|cell| cell.borrow_mut().clear());
679
680        #[cfg(debug_assertions)]
681        Self::with(|state| state.has_cleaned_up = true);
682
683        Self::with(|state| state.in_cleanup = false);
684    }
685
686    /// Determine whether the execution has finished.
687    pub fn is_finished(&self) -> bool {
688        self.current_task == ScheduledTask::Stopped || self.current_task == ScheduledTask::Finished
689    }
690
691    /// Invoke the scheduler to decide which task to schedule next. Returns true if the chosen task
692    /// is different from the currently running task, indicating that the current task should yield
693    /// its execution.
694    pub fn maybe_yield() -> bool {
695        Self::with(|state| {
696            if std::thread::panicking() && !state.in_cleanup {
697                return true;
698            }
699
700            debug_assert!(
701                matches!(state.current_task, ScheduledTask::Some(_) | ScheduledTask::Finished)
702                    && state.next_task == ScheduledTask::None,
703                "we're inside a task and scheduler should not yet have run"
704            );
705
706            let result = state.schedule();
707            // If scheduling failed, yield so that the outer scheduling loop can handle it.
708            if result.is_err() {
709                return true;
710            }
711
712            // If the next task is the same as the current one, we can skip the context switch
713            // and just advance to the next task immediately.
714            if state.current_task == state.next_task {
715                state.advance_to_next_task();
716                false
717            } else {
718                true
719            }
720        })
721    }
722
723    /// Tell the scheduler that the next context switch is an explicit yield requested by the
724    /// current task. Some schedulers use this as a hint to influence scheduling.
725    pub fn request_yield() {
726        Self::with(|state| {
727            state.has_yielded = true;
728        });
729    }
730
731    /// Check whether the current execution has stopped. Call from `Drop` handlers to early exit if
732    /// they are being invoked because an execution has stopped.
733    ///
734    /// We also stop if we are currently panicking (e.g., perhaps we're unwinding the stack for a
735    /// panic triggered while someone held a Mutex, and so are executing the Drop handler for
736    /// MutexGuard). This avoids calling back into the scheduler during a panic, because the state
737    /// may be poisoned or otherwise invalid.
738    pub fn should_stop() -> bool {
739        std::thread::panicking()
740            || Self::with(|s| {
741                assert_ne!(s.current_task, ScheduledTask::Finished);
742                s.current_task == ScheduledTask::Stopped
743            })
744    }
745
746    /// Generate some diagnostic information used when persisting failures.
747    ///
748    /// Because this method may be called from a panic hook, it must not panic.
749    pub fn failing_task() -> String {
750        Self::try_with(|state| {
751            if let Some(task) = state.try_current() {
752                task.name().unwrap_or_else(|| format!("task-{:?}", task.id().0))
753            } else {
754                "<unknown>".into()
755            }
756        })
757        .unwrap_or_else(|e| format!("Tried to get ExecutionState, but got the following error: {e:?}"))
758    }
759
760    /// Generate a random u64 from the current scheduler and return it.
761    #[inline]
762    pub fn next_u64() -> u64 {
763        Self::with(|state| {
764            CurrentSchedule::push_random();
765            state.scheduler.borrow_mut().next_u64()
766        })
767    }
768
769    pub fn current(&self) -> &Task {
770        self.get(self.current_task.id().unwrap())
771    }
772
773    pub fn current_mut(&mut self) -> &mut Task {
774        self.get_mut(self.current_task.id().unwrap())
775    }
776
777    pub fn try_current(&self) -> Option<&Task> {
778        self.try_get(self.current_task.id()?)
779    }
780
781    pub fn get(&self, id: TaskId) -> &Task {
782        self.try_get(id).unwrap()
783    }
784
785    pub fn get_mut(&mut self, id: TaskId) -> &mut Task {
786        self.tasks.get_mut(id.0).unwrap()
787    }
788
789    pub fn try_get(&self, id: TaskId) -> Option<&Task> {
790        self.tasks.get(id.0)
791    }
792
793    pub fn in_cleanup(&self) -> bool {
794        self.in_cleanup
795    }
796
797    pub fn context_switches() -> usize {
798        Self::with(|state| state.context_switches)
799    }
800
801    #[track_caller]
802    pub fn new_resource_signature(resource_type: ResourceType) -> ResourceSignature {
803        ExecutionState::with(|s| s.current_mut().signature.new_resource(resource_type))
804    }
805
806    pub fn get_storage<K: Into<StorageKey>, T: 'static>(&self, key: K) -> Option<&T> {
807        self.storage
808            .get(key.into())
809            .map(|result| result.expect("global storage is never destructed"))
810    }
811
812    pub fn init_storage<K: Into<StorageKey>, T: 'static>(&mut self, key: K, value: T) {
813        self.storage.init(key.into(), value);
814    }
815
816    pub fn get_clock(&self, id: TaskId) -> &VectorClock {
817        &self.tasks.get(id.0).unwrap().clock
818    }
819
820    pub fn get_clock_mut(&mut self, id: TaskId) -> &mut VectorClock {
821        &mut self.tasks.get_mut(id.0).unwrap().clock
822    }
823
824    /// Increment the current thread's clock entry and update its clock with the one provided.
825    pub fn update_clock(&mut self, clock: &VectorClock) {
826        let task = self.current_mut();
827        task.clock.increment(task.id);
828        task.clock.update(clock);
829    }
830
831    /// Increment the current thread's clock and return a shared reference to it
832    pub fn increment_clock(&mut self) -> &VectorClock {
833        let task = self.current_mut();
834        task.clock.increment(task.id);
835        &task.clock
836    }
837
838    /// Increment the current thread's clock and return a mutable reference to it
839    pub fn increment_clock_mut(&mut self) -> &mut VectorClock {
840        let task = self.current_mut();
841        task.clock.increment(task.id);
842        &mut task.clock
843    }
844
845    /// Returns `true` if the test has exceeded the step bound, and `false` otherwise.
846    fn is_step_bound_exceeded(&self, max_steps: usize) -> bool {
847        CurrentSchedule::len() - self.steps_reset_at >= max_steps
848    }
849
850    /// Run the scheduler to choose the next task to run. `has_yielded` should be false if the
851    /// scheduler is being invoked from within a running task. If scheduling fails, returns an Err
852    /// with a String describing the failure.
853    fn schedule(&mut self) -> Result<(), StepError> {
854        // Don't schedule twice. If `maybe_yield` ran the scheduler, we don't want to run it
855        // again at the top of `step`.
856        if self.next_task != ScheduledTask::None {
857            return Ok(());
858        }
859
860        self.context_switches += 1;
861
862        match self.config.max_steps {
863            MaxSteps::FailAfter(max_steps) => {
864                if self.is_step_bound_exceeded(max_steps) {
865                    return Err(StepError::StepBoundExceeded);
866                }
867            }
868            MaxSteps::ContinueAfter(max_steps) => {
869                if self.is_step_bound_exceeded(max_steps) {
870                    // TODO: We have to set `Stopped` and return `Ok` here, else assertions will fail. This should probably be cleaned up.
871                    self.next_task = ScheduledTask::Stopped;
872                    return Ok(());
873                }
874            }
875            MaxSteps::None => {}
876        }
877
878        let mut unfinished_attached = false;
879        let mut all_runnable_detached = true;
880        let mut any_runnable = false;
881
882        for task in &self.tasks {
883            if task.finished() {
884                continue;
885            }
886            unfinished_attached |= !task.detached;
887            let is_runnable = task.runnable();
888            any_runnable |= is_runnable;
889
890            if is_runnable {
891                all_runnable_detached &= task.detached;
892                self.runnable_tasks.push(task as *const Task);
893            } else if task.can_spuriously_wakeup() {
894                // Some blocked tasks can be woken up spuriously, even though the condition the task is
895                // blocked on hasn't happened yet. We'll add such tasks to the list of runnable tasks, but
896                // they won't contribute to the check on `any_runnable`; if the only runnable tasks
897                // are ones that are waiting for a potential spurious wakeup, it should still be treated as
898                // a deadlock since there's no guarantee that spurious wakeups will ever occur.
899                self.runnable_tasks.push(task as *const Task);
900            }
901        }
902
903        // We should finish execution when either
904        // (1) There are no runnable tasks, or
905        // (2) All runnable tasks have been detached AND there are no unfinished attached tasks
906        // If there are some unfinished attached tasks and all runnable tasks are detached, we must
907        // run some detached task to give them a chance to unblock some unfinished attached task.
908        if !any_runnable || (!unfinished_attached && all_runnable_detached) {
909            self.next_task = ScheduledTask::Finished;
910            return Ok(());
911        }
912
913        let is_yielding = std::mem::replace(&mut self.has_yielded, false);
914
915        // Cast the slice of raw pointers to a slice of references in place to provide schedulers with a safe API
916        //
917        // SAFETY: This is safe because the tasks themselves are only being accessed through this shared reference by the
918        // schedulers, and all references are always cleared from the runnable_tasks Vec at the end of this function.
919        // The transmute itself is safe because *const and & have the same layout, and the pointer is created from a
920        // reference earlier in this function.
921        let task_refs = unsafe { std::mem::transmute::<&[*const Task], &[&Task]>(&self.runnable_tasks) };
922
923        self.next_task = self
924            .scheduler
925            .borrow_mut()
926            .next_task(task_refs, self.current_task.id(), is_yielding)
927            .map(ScheduledTask::Some)
928            .unwrap_or(ScheduledTask::Stopped);
929
930        // Tracing this `in_scope` is purely a matter of taste. We do it because
931        // 1) It is an action taken by the scheduler, and should thus be traced under the scheduler's span
932        // 2) It creates a visual separation of scheduling decisions and `Task`-induced tracing.
933        // Note that there is a case to be made for not `in_scope`-ing it, as that makes seeing the context
934        // of the context switch clearer.
935        //
936        // Note also that changing this trace! statement requires changing the test `basic::labels::test_tracing_with_label_fn`
937        // which relies on this trace reporting the `runnable` tasks.
938        self.top_level_span.in_scope(|| {
939            trace!(
940                i=CurrentSchedule::len(),
941                next_task=?self.next_task,
942                runnable=?task_refs.iter().map(|task| task.id()).collect::<SmallVec<[_; DEFAULT_INLINE_TASKS]>>(),
943                "scheduling decision"
944            );
945        });
946
947        // If the task chosen by the scheduler is blocked, then it should be one that can be
948        // spuriously woken up, and we need to unblock it here so that it can execute.
949        if let Some(tid) = self.next_task.id() {
950            let task = self.get_mut(tid);
951            assert!(task.runnable() || task.blocked());
952            if task.blocked() {
953                assert!(task.can_spuriously_wakeup());
954                task.unblock();
955            }
956        }
957
958        // Retains the capacity of `runnable_tasks` for future calls of `schedule`
959        self.runnable_tasks.clear();
960
961        Ok(())
962    }
963
964    /// Set the next task as the current task
965    fn advance_to_next_task(&mut self) {
966        debug_assert_ne!(self.next_task, ScheduledTask::None);
967        self.current_task = self.next_task.take();
968
969        if let ScheduledTask::Some(tid) = self.current_task {
970            CurrentSchedule::push_task(tid);
971        }
972    }
973
974    // Sets the `tag` field of the current task.
975    // Returns the `tag` which was there previously.
976    #[allow(deprecated)]
977    pub fn set_tag_for_current_task(tag: Arc<dyn Tag>) -> Option<Arc<dyn Tag>> {
978        ExecutionState::with(|s| s.current_mut().set_tag(tag))
979    }
980
981    #[allow(deprecated)]
982    fn get_tag_or_default_for_current_task(&self) -> Option<Arc<dyn Tag>> {
983        self.try_current().and_then(|current| current.get_tag())
984    }
985
986    #[allow(deprecated)]
987    pub fn get_tag_for_current_task() -> Option<Arc<dyn Tag>> {
988        ExecutionState::with(|s| s.get_tag_or_default_for_current_task())
989    }
990
991    #[allow(deprecated)]
992    pub fn set_tag_for_task(task: TaskId, tag: Arc<dyn Tag>) -> Option<Arc<dyn Tag>> {
993        ExecutionState::with(|s| s.get_mut(task).set_tag(tag))
994    }
995}
996
997#[cfg(debug_assertions)]
998impl Drop for ExecutionState {
999    fn drop(&mut self) {
1000        assert!(self.has_cleaned_up || std::thread::panicking());
1001    }
1002}