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