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}