shuttle_engine/runtime/thread/continuation.rs
1use crate::config::UNGRACEFUL_SHUTDOWN_CONFIG;
2use crate::runtime::execution::ExecutionState;
3use corosensei::Yielder;
4use corosensei::{stack::DefaultStack, Coroutine, CoroutineResult};
5use scoped_tls::scoped_thread_local;
6use std::cell::{Cell, RefCell};
7use std::collections::VecDeque;
8use std::ops::Deref;
9use std::ops::DerefMut;
10use std::panic::Location;
11use std::rc::Rc;
12use tracing::trace;
13
14scoped_thread_local! {
15 pub static CONTINUATION_POOL: ContinuationPool
16}
17
18/// A continuation is a green thread that can be resumed and yielded at will. We use it to
19/// execute a "thread" from within a Future.
20///
21/// For efficiency, we reuse continuations. The continuation can be provided a new function
22/// to run via `initialize`. A continuation is only reusable if the previous function it was
23/// executing completed.
24pub struct Continuation {
25 coroutine: Coroutine<ContinuationInput, ContinuationOutput, ContinuationOutput>,
26 function: ContinuationFunction,
27 state: ContinuationState,
28 pub yielder: *const Yielder<ContinuationInput, ContinuationOutput>,
29}
30
31impl std::fmt::Debug for Continuation {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 f.debug_struct("Continuation")
34 .field("state", &self.state)
35 .finish_non_exhaustive()
36 }
37}
38
39/// A cell to pass functions into continuations
40#[allow(clippy::type_complexity)]
41#[derive(Clone)]
42struct ContinuationFunction(Rc<Cell<Option<Box<dyn FnOnce()>>>>);
43
44// Safety: we arrange for the `function` field of `Continuation` to only be accessed by one thread
45// at a time: Shuttle tests are single threaded, and continuations are never shared across threads
46// by the ContinuationPool, which is thread-local.
47unsafe impl Send for ContinuationFunction {}
48
49/// Inputs that we can pass to a continuation.
50#[derive(Debug, PartialEq, Eq, Clone, Copy)]
51pub enum ContinuationInput {
52 Resume,
53 Exit,
54 /// Drop the function instead of running it, or stop the future it polls (see `Continuation::cancel`).
55 Cancel,
56}
57
58/// Outputs that a continuation can pass back to us
59#[derive(Debug, PartialEq, Eq, Clone, Copy)]
60pub enum ContinuationOutput {
61 Yielded,
62 Finished(*const Yielder<ContinuationInput, ContinuationOutput>),
63 Exited,
64}
65
66/// The current state of a continuation. Lifecycle runs from top to bottom.
67#[derive(Debug, PartialEq, Eq, Clone, Copy)]
68enum ContinuationState {
69 NotReady, // has no function in its cell; waiting for input about what to do next
70 Initialized, // has a function in its cell, but hasn't started running yet
71 Ready, // has a suspended function in its cell; waiting for input about what to do next
72 Running, // currently inside a user-provided function
73 FinishedIteration, // has finished the previous function, can be initialized with a new one
74 Exited, // the internal coroutine has exited its loop and cannot receive new functions to execute
75}
76
77impl Continuation {
78 pub fn new(stack_size: usize) -> Self {
79 let function = ContinuationFunction(Rc::new(Cell::new(None)));
80
81 let mut coroutine = {
82 let function = function.clone();
83
84 Coroutine::with_stack(DefaultStack::new(stack_size).unwrap(), move |yielder, input| {
85 if let ContinuationInput::Exit = input {
86 return ContinuationOutput::Exited;
87 }
88
89 // Move the whole `ContinuationFunction`, not just its field (Rust 2021 thing)
90 let _ = &function;
91
92 loop {
93 // Tell the caller we've finished the previous user function (or if this is our
94 // first time around the loop, the caller below expects us to pretend we've
95 // finished the previous function).
96 let input = yielder.suspend(ContinuationOutput::Finished(yielder as *const _));
97 let f = match input {
98 ContinuationInput::Exit => break,
99 ContinuationInput::Resume | ContinuationInput::Cancel => {
100 function.0.take().expect("must have a function to run")
101 }
102 };
103
104 if input == ContinuationInput::Cancel {
105 // The function never ran: drop it here, on this stack, so that the destructors
106 // of what it owns run like a running task's (see `Continuation::cancel`).
107 drop(f);
108 } else {
109 f();
110 }
111 }
112
113 ContinuationOutput::Exited
114 })
115 };
116
117 // Resume the coroutine once to get it into the loop. Because the continuations are kept in a pool and reused,
118 // this requires us to exfiltrate the yielder from the closure passed in to Coroutine::with_stack and persist it
119 // for the lifetime of the continuation's inner function. The yielder exfiltration happens below on the first suspend
120 // of the coroutine
121 let yielder = match coroutine.resume(ContinuationInput::Resume) {
122 CoroutineResult::Yield(ContinuationOutput::Finished(yielder)) => yielder,
123 _ => panic!("Coroutine should yield a pointer to its `corosensei::Yielder` from the first resume"),
124 };
125
126 Self {
127 coroutine,
128 yielder,
129 function,
130 state: ContinuationState::NotReady,
131 }
132 }
133
134 /// Provide a new function for the continuation to execute. The continuation must
135 /// be in reusable state.
136 pub fn initialize(&mut self, fun: Box<dyn FnOnce()>) {
137 debug_assert!(self.reusable(), "shouldn't replace a function before it completes");
138
139 let old = self.function.0.replace(Some(fun));
140 debug_assert!(old.is_none(), "shouldn't replace a function before it runs");
141
142 self.state = ContinuationState::Initialized;
143 }
144
145 /// Resume the continuation, and returns true if the function it was executing has finished.
146 pub fn resume(&mut self) -> bool {
147 debug_assert!(self.state == ContinuationState::Ready || self.state == ContinuationState::Initialized);
148
149 let ret = self.resume_with_input(ContinuationInput::Resume);
150 debug_assert_ne!(
151 ret,
152 ContinuationOutput::Exited,
153 "continuation should not exit if resumed from user code"
154 );
155
156 matches!(ret, ContinuationOutput::Finished(_))
157 }
158
159 /// Resume the continuation to cancel its function, and return true if the function has
160 /// finished, like `resume`. This is how execution teardown drops a task that is unfinished at
161 /// the end of an execution, on the task's own stack (see `ExecutionState::tear_down`). A
162 /// function that never ran is dropped. A function suspended between polls of a future (see
163 /// `switch_between_polls`) drops the future and returns. Only these two are cancelled: any other
164 /// suspended function is in the middle of user code, which only unwinding its stack can stop.
165 pub fn cancel(&mut self) -> bool {
166 debug_assert!(self.state == ContinuationState::Ready || self.state == ContinuationState::Initialized);
167
168 let ret = self.resume_with_input(ContinuationInput::Cancel);
169 debug_assert_ne!(
170 ret,
171 ContinuationOutput::Exited,
172 "continuation should not exit when cancelled"
173 );
174
175 matches!(ret, ContinuationOutput::Finished(_))
176 }
177
178 /// Whether the continuation has a function that has not started running yet.
179 pub(crate) fn never_ran(&self) -> bool {
180 self.state == ContinuationState::Initialized
181 }
182
183 /// Whether the continuation's function is suspended, and can be resumed.
184 pub(crate) fn suspended(&self) -> bool {
185 self.state == ContinuationState::Ready
186 }
187
188 /// Leak the function the continuation was given, which has not started running, so that the
189 /// continuation can be reused.
190 pub(crate) fn leak_function(&mut self) {
191 debug_assert!(self.never_ran());
192 std::mem::forget(self.function.0.take());
193 self.state = ContinuationState::NotReady;
194 }
195
196 /// Unwind the stack of a continuation that is suspended in its function, which drops what is on
197 /// it, as a panic would. If the function catches the unwind, it starts again where the function
198 /// next switches (see `Coroutine::force_unwind`).
199 pub(crate) fn unwind_stack(&mut self) {
200 debug_assert!(self.state == ContinuationState::Ready);
201 self.coroutine.force_unwind();
202 self.state = ContinuationState::Exited;
203 }
204
205 /// Leak the stack of a continuation that is suspended in its function, or whose function
206 /// panicked out of it: nothing on it is dropped, and the stack is freed with the continuation.
207 pub(crate) fn leak_stack(&mut self) {
208 debug_assert!(self.state == ContinuationState::Ready || self.state == ContinuationState::Running);
209 // SAFETY: this leaks the objects on the coroutine's stack without dropping them, so anything
210 // that still refers into the stack is left dangling (see `Coroutine::force_reset`). Shuttle
211 // has accepted that for stopped and failed executions, whose tasks never run again (see
212 // `ExecutionState::tear_down`). Teardown drops the functions of the tasks that never ran,
213 // which can borrow from another task's stack (scoped threads), before it leaks any stack.
214 unsafe {
215 self.coroutine.force_reset();
216 }
217 self.state = ContinuationState::Exited;
218 }
219
220 fn resume_with_input(&mut self, input: ContinuationInput) -> ContinuationOutput {
221 self.state = ContinuationState::Running;
222 match self.coroutine.resume(input) {
223 CoroutineResult::Yield(output) => {
224 self.state = match output {
225 ContinuationOutput::Finished(_) => ContinuationState::FinishedIteration,
226 ContinuationOutput::Yielded => ContinuationState::Ready,
227 ContinuationOutput::Exited => ContinuationState::Exited,
228 };
229 output
230 }
231 CoroutineResult::Return(output) => {
232 self.state = ContinuationState::Exited;
233 output
234 }
235 }
236 }
237
238 /// A continuation is reusable if it has completed running a user function and is waiting
239 /// to be initialized with a new one. A continuation isn't reusable if it's still inside the user
240 /// function `f` (Ready or Running), as changing it's inner function would leak the currently
241 /// executing context. We also do not consider Initialized coroutines to be reusable to avoid
242 /// accidentally overwriting a continuation before it has had a chance to run. Continuations which
243 /// have Exited, are not reusable as they have broken out of the loop where their inner functions
244 /// can be replaced.
245 fn reusable(&self) -> bool {
246 self.state == ContinuationState::NotReady || self.state == ContinuationState::FinishedIteration
247 }
248}
249
250impl Drop for Continuation {
251 fn drop(&mut self) {
252 // If the continuation is reusable, we tell it to exit and gracefully clean up its
253 // resources. If not, we can't send it an exit message because it might be stopped in
254 // arbitrary user code. Its resources will still be cleaned up when the underlying
255 // generator is dropped, but doing so is slower (the generator impl invokes a panic
256 // inside the continuation), so this drop handler exists to avoid it when possible.
257 match self.state {
258 ContinuationState::Initialized | ContinuationState::FinishedIteration | ContinuationState::NotReady => {
259 let ret = self.resume_with_input(ContinuationInput::Exit);
260 debug_assert_eq!(ret, ContinuationOutput::Exited);
261 }
262 ContinuationState::Running | ContinuationState::Ready => {
263 // Execution teardown unwinds or leaks the stacks of the tasks that are unfinished at
264 // the end of an execution itself (see `ExecutionState::tear_down`), so this is only
265 // reached for a continuation dropped some other way. If already panicking or the
266 // execution stopped, don't worry about cleaning up resources on individual coroutines
267 // which are still in-flight: unwinding them runs drop handlers, which can then panic
268 // if they interact with shuttle atomics, causing a panic-on-drop abort.
269 //
270 // SAFETY: `force_reset` leaks the coroutine. However, given that the execution is *already* aborting
271 // at this point and will soon exit, this is unlikely to cause issues. Leaking the coroutine here also
272 // avoids most tricky issues with scheduling points in drop handlers during a panic, which can often
273 // result in difficult-to-debug aborts from double-panics.
274 if std::thread::panicking() || ExecutionState::execution_stopped() {
275 unsafe {
276 self.coroutine.force_reset();
277 }
278 } else {
279 self.coroutine.force_unwind();
280 }
281 }
282 ContinuationState::Exited => {
283 // Already exited, nothing to do
284 }
285 }
286 }
287}
288
289/// A `ContinuationPool` just holds on to old `Continuation`s that are reusable, and vends
290/// them back out again. This amortizes the cost of allocating continuations, which involve
291/// allocating new stacks (`mmap`), `mprotect`, etc.
292pub struct ContinuationPool {
293 // invariant: if c is in this queue, c.reusable() == true
294 continuations: Rc<RefCell<VecDeque<Continuation>>>,
295}
296
297impl std::fmt::Debug for ContinuationPool {
298 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299 f.debug_struct("ContinuationPool").finish_non_exhaustive()
300 }
301}
302
303impl ContinuationPool {
304 pub fn new() -> Self {
305 Self {
306 continuations: Rc::new(RefCell::new(VecDeque::new())),
307 }
308 }
309
310 /// Acquire a new continuation from the global pool. Panics if that pool was not yet initialized.
311 pub fn acquire(stack_size: usize) -> PooledContinuation {
312 CONTINUATION_POOL.with(|p| p.acquire_inner(stack_size))
313 }
314
315 fn acquire_inner(&self, stack_size: usize) -> PooledContinuation {
316 // TODO add a check to ensure that if we recycled a continuation, its
317 // TODO allocated stack size is at least the requested `stack_size`
318 let continuation = self
319 .continuations
320 .borrow_mut()
321 .pop_front()
322 .unwrap_or_else(move || Continuation::new(stack_size));
323
324 PooledContinuation {
325 continuation: Some(continuation),
326 queue: self.continuations.clone(),
327 }
328 }
329}
330
331/// A thin wrapper around a `Continuation` that returns it to a `ContinuationPool`
332/// when dropped, but only if it's reusable.
333pub struct PooledContinuation {
334 continuation: Option<Continuation>,
335 queue: Rc<RefCell<VecDeque<Continuation>>>,
336}
337
338impl Drop for PooledContinuation {
339 fn drop(&mut self) {
340 let mut c = self.continuation.take().unwrap();
341 if c.reusable() {
342 self.queue.borrow_mut().push_back(c);
343 } else if matches!(c.state, ContinuationState::Initialized) {
344 // A continuation which has been initialized but not run cannot be immediately reused.
345 // This is because arguments and captures may already have been moved into the function,
346 // and thus these moved objects won't be dropped until the function itself has been
347 // dropped. Thus we must drop the inner function before reusing it.
348 //
349 // Execution teardown deals with the functions of the tasks that never ran itself (see
350 // `ExecutionState::tear_down`), so this is only reached for a continuation dropped some
351 // other way.
352 if std::thread::panicking() && UNGRACEFUL_SHUTDOWN_CONFIG.get().continuation_function_behavior.leaks() {
353 c.leak_function();
354 } else {
355 drop(c.function.0.replace(None));
356 c.state = ContinuationState::NotReady;
357 }
358 self.queue.borrow_mut().push_back(c);
359 }
360 }
361}
362
363impl Deref for PooledContinuation {
364 type Target = Continuation;
365
366 fn deref(&self) -> &Self::Target {
367 self.continuation.as_ref().unwrap()
368 }
369}
370
371impl DerefMut for PooledContinuation {
372 fn deref_mut(&mut self) -> &mut Self::Target {
373 self.continuation.as_mut().unwrap()
374 }
375}
376
377impl std::fmt::Debug for PooledContinuation {
378 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
379 f.debug_struct("PooledContinuation").finish()
380 }
381}
382
383// Safety: these aren't sent across real threads
384unsafe impl Send for PooledContinuation {}
385
386/// Possibly yield back to the executor to perform a context switch. This function should be
387/// called *before* any visible operation. If each visible operation has a scheduling point
388/// before it, then there will be a potential context switch *in between* any pair of visible
389/// operations, which is a necessary condition for completeness.
390///
391/// Putting scheduling points before visible operations, rather than after, has the advantage
392/// of giving the scheduling algorithm additional information to make scheduling decisions
393/// based on what is about to happen on each task. The disadvantage of this approach is that it
394/// is more difficult to avoid double-yields for blocking operations, explained below.
395///
396/// In addition to the scheduling point before the operation begins, blocking operations will
397/// result in a *second* context switch if the current thread is blocked. As an optimization,
398/// the switch *before* the blocking operation can be conditionally omitted to avoid switching
399/// twice for the same operation iff (1) the operation *will* block and (2) if the act of
400/// blocking *commutes* with all other operations on that resource.
401///
402/// Reasoning: We can consider a blocking operation (`Y`) such as acquiring a mutex as two
403/// sub-operations (`Y1`) blocking and (`Y2`) proceeding after being unblocked. The double-yield
404/// optimization omits the scheduling point before `Y1`. For arbitrary events `X` and `Z` and
405/// intra-thread orderings `T1: X Y1 Y2` and `T2: Z`, we have four interleavings:
406///
407/// `X Z Y1 Y2`
408/// `X Y1 Z Y2`
409/// `Z X Y1 Y2`
410/// `X Y1 Y2 Z`
411///
412/// Note that the first interleaving is *not observable* if we omit the scheduling point before `Y1`.
413/// Thus to maintain behavioral completeness when omitting this scheduling point, all states
414/// observable from the first schedule must also be observable in one of the other schedules.
415///
416/// Observe that if `Y1` and `Z` commute, then the first two schedules are behaviorally equivalent,
417/// thus the optimization is safe. So, to ensure the safety of the double-yield optimization for an
418/// operation `Y1`, it suffices to check that `Y1` commutes with all operations `Z` on the same resource,
419/// as operations on other resources should commute trivially.
420#[track_caller]
421pub fn switch() {
422 crate::annotations::record_tick();
423 trace!("switch from {}", Location::caller());
424 if ExecutionState::maybe_yield() {
425 match suspend() {
426 ContinuationInput::Exit => panic!("unexpected exit continuation"),
427 ContinuationInput::Resume => {}
428 ContinuationInput::Cancel => unreachable!("only a task that is between polls is cancelled"),
429 }
430 }
431}
432
433/// Like `switch`, for the loop that polls a future task's future (see `Task::from_future`), between
434/// two polls. Returns true if execution teardown cancelled the task while it was switched out (see
435/// `Continuation::cancel`), in which case the loop drops the future instead of polling it again.
436pub(crate) fn switch_between_polls() -> bool {
437 crate::annotations::record_tick();
438 trace!("switch between polls");
439 ExecutionState::maybe_yield() && suspend() == ContinuationInput::Cancel
440}
441
442/// Suspend the current task's continuation, and return what it is resumed with.
443#[inline(always)]
444fn suspend() -> ContinuationInput {
445 let yielder = ExecutionState::with(|state| state.current().yielder);
446 assert!(!yielder.is_null(), "a task without a stack cannot switch");
447
448 // SAFETY: A yielder reference is valid for the lifetime of the continuation (see
449 // `corosensei::Coroutine::with_stack`), and the task's `yielder` is its continuation's. A task
450 // owns its continuation until execution teardown takes it, to unwind or leak its stack, and
451 // teardown clears `yielder` once the stack is gone. `maybe_yield` only asks a task to switch
452 // while it runs on its continuation, which a task whose stack teardown unwinds still does. There
453 // are no mutable references to the yielder.
454 unsafe { &(*yielder) }.suspend(ContinuationOutput::Yielded)
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460 use crate::config::Config;
461
462 #[test]
463 fn reusable_continuation_drop() {
464 let pool = ContinuationPool::new();
465 let config: Config = Default::default();
466
467 let mut c = pool.acquire_inner(config.stack_size);
468 c.initialize(Box::new(|| {
469 let _ = 1 + 1;
470 }));
471 let yielder = c.yielder;
472
473 let r = c.resume();
474 assert!(r, "continuation only has one step");
475
476 drop(c);
477 assert_eq!(
478 pool.continuations.borrow().len(),
479 1,
480 "continuation should be reusable because the function finished"
481 );
482
483 let mut c = pool.acquire_inner(config.stack_size);
484 c.initialize(Box::new(move || {
485 // SAFETY: We must use the yielder directly here because we are not in a Shuttle execution
486 // for `continuation::switch`, which requires access to `ExecutionState`. This is safe because
487 // the yielder's lifetime is valid as long as the continuation has not `Exited`, and the
488 // continuation cannot have exited if it is executing its current function.
489 unsafe { &(*yielder) }.suspend(ContinuationOutput::Yielded);
490 let _ = 1 + 1;
491 }));
492
493 let r = c.resume();
494 assert!(!r, "continuation yields once, shouldn't be finished yet");
495
496 drop(c);
497 assert_eq!(
498 pool.continuations.borrow().len(),
499 0,
500 "continuation should not be reusable because the function wasn't finished"
501 );
502
503 let c = pool.acquire_inner(config.stack_size);
504
505 // Check that it's safe for a continuation to outlive the pool
506 drop(pool);
507 drop(c);
508 }
509}