Skip to main content

shuttle_engine/runtime/thread/
continuation.rs

1use crate::config::{ContinuationFunctionBehavior, 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}
55
56/// Outputs that a continuation can pass back to us
57#[derive(Debug, PartialEq, Eq, Clone, Copy)]
58pub enum ContinuationOutput {
59    Yielded,
60    Finished(*const Yielder<ContinuationInput, ContinuationOutput>),
61    Exited,
62}
63
64/// The current state of a continuation. Lifecycle runs from top to bottom.
65#[derive(Debug, PartialEq, Eq, Clone, Copy)]
66enum ContinuationState {
67    NotReady,          // has no function in its cell; waiting for input about what to do next
68    Initialized,       // has a function in its cell, but hasn't started running yet
69    Ready,             // has a suspended function in its cell; waiting for input about what to do next
70    Running,           // currently inside a user-provided function
71    FinishedIteration, // has finished the previous function, can be initialized with a new one
72    Exited,            // the internal coroutine has exited its loop and cannot receive new functions to execute
73}
74
75impl Continuation {
76    pub fn new(stack_size: usize) -> Self {
77        let function = ContinuationFunction(Rc::new(Cell::new(None)));
78
79        let mut coroutine = {
80            let function = function.clone();
81
82            Coroutine::with_stack(DefaultStack::new(stack_size).unwrap(), move |yielder, input| {
83                if let ContinuationInput::Exit = input {
84                    return ContinuationOutput::Exited;
85                }
86
87                // Move the whole `ContinuationFunction`, not just its field (Rust 2021 thing)
88                let _ = &function;
89
90                loop {
91                    // Tell the caller we've finished the previous user function (or if this is our
92                    // first time around the loop, the caller below expects us to pretend we've
93                    // finished the previous function).
94                    match yielder.suspend(ContinuationOutput::Finished(yielder as *const _)) {
95                        ContinuationInput::Exit => break,
96                        ContinuationInput::Resume => {}
97                    };
98
99                    let f = function.0.take().expect("must have a function to run");
100
101                    f();
102                }
103
104                ContinuationOutput::Exited
105            })
106        };
107
108        // Resume the coroutine once to get it into the loop. Because the continuations are kept in a pool and reused,
109        // this requires us to exfiltrate the yielder from the closure passed in to Coroutine::with_stack and persist it
110        // for the lifetime of the continuation's inner function. The yielder exfiltration happens below on the first suspend
111        // of the coroutine
112        let yielder = match coroutine.resume(ContinuationInput::Resume) {
113            CoroutineResult::Yield(ContinuationOutput::Finished(yielder)) => yielder,
114            _ => panic!("Coroutine should yield a pointer to its `corosensei::Yielder` from the first resume"),
115        };
116
117        Self {
118            coroutine,
119            yielder,
120            function,
121            state: ContinuationState::NotReady,
122        }
123    }
124
125    /// Provide a new function for the continuation to execute. The continuation must
126    /// be in reusable state.
127    pub fn initialize(&mut self, fun: Box<dyn FnOnce()>) {
128        debug_assert!(self.reusable(), "shouldn't replace a function before it completes");
129
130        let old = self.function.0.replace(Some(fun));
131        debug_assert!(old.is_none(), "shouldn't replace a function before it runs");
132
133        self.state = ContinuationState::Initialized;
134    }
135
136    /// Resume the continuation, and returns true if the function it was executing has finished.
137    pub fn resume(&mut self) -> bool {
138        debug_assert!(self.state == ContinuationState::Ready || self.state == ContinuationState::Initialized);
139
140        let ret = self.resume_with_input(ContinuationInput::Resume);
141        debug_assert_ne!(
142            ret,
143            ContinuationOutput::Exited,
144            "continuation should not exit if resumed from user code"
145        );
146
147        matches!(ret, ContinuationOutput::Finished(_))
148    }
149
150    fn resume_with_input(&mut self, input: ContinuationInput) -> ContinuationOutput {
151        self.state = ContinuationState::Running;
152        match self.coroutine.resume(input) {
153            CoroutineResult::Yield(output) => {
154                self.state = match output {
155                    ContinuationOutput::Finished(_) => ContinuationState::FinishedIteration,
156                    ContinuationOutput::Yielded => ContinuationState::Ready,
157                    ContinuationOutput::Exited => ContinuationState::Exited,
158                };
159                output
160            }
161            CoroutineResult::Return(output) => {
162                self.state = ContinuationState::Exited;
163                output
164            }
165        }
166    }
167
168    /// A continuation is reusable if it has completed running a user function and is waiting
169    /// to be initialized with a new one. A continuation isn't reusable if it's still inside the user
170    /// function `f` (Ready or Running), as changing it's inner function would leak the currently
171    /// executing context. We also do not consider Initialized coroutines to be reusable to avoid
172    /// accidentally overwriting a continuation before it has had a chance to run. Continuations which
173    /// have Exited, are not reusable as they have broken out of the loop where their inner functions
174    /// can be replaced.
175    fn reusable(&self) -> bool {
176        self.state == ContinuationState::NotReady || self.state == ContinuationState::FinishedIteration
177    }
178}
179
180impl Drop for Continuation {
181    fn drop(&mut self) {
182        // If the continuation is reusable, we tell it to exit and gracefully clean up its
183        // resources. If not, we can't send it an exit message because it might be stopped in
184        // arbitrary user code. Its resources will still be cleaned up when the underlying
185        // generator is dropped, but doing so is slower (the generator impl invokes a panic
186        // inside the continuation), so this drop handler exists to avoid it when possible.
187        match self.state {
188            ContinuationState::Initialized | ContinuationState::FinishedIteration | ContinuationState::NotReady => {
189                let ret = self.resume_with_input(ContinuationInput::Exit);
190                debug_assert_eq!(ret, ContinuationOutput::Exited);
191            }
192            ContinuationState::Running | ContinuationState::Ready => {
193                // If already panicking or at the end of the execution, don't worry about cleaning up resources
194                // on individual coroutines which are still in-flight
195                if std::thread::panicking() {
196                    // SAFETY: `force_reset` leaks the coroutine. However, given that the execution is *already* panicking
197                    // at this point and will soon exit due to the original panic, this is unlikely to cause issues. Leaking
198                    // the corouting here also avoids most tricky issues with scheduling points in drop handlers during a panic,
199                    // which can often result in difficult-to-debug aborts from double-panics.
200                    unsafe {
201                        self.coroutine.force_reset();
202                    }
203                }
204                self.coroutine.force_unwind();
205            }
206            ContinuationState::Exited => {
207                // Already exited, nothing to do
208            }
209        }
210    }
211}
212
213/// A `ContinuationPool` just holds on to old `Continuation`s that are reusable, and vends
214/// them back out again. This amortizes the cost of allocating continuations, which involve
215/// allocating new stacks (`mmap`), `mprotect`, etc.
216pub struct ContinuationPool {
217    // invariant: if c is in this queue, c.reusable() == true
218    continuations: Rc<RefCell<VecDeque<Continuation>>>,
219}
220
221impl std::fmt::Debug for ContinuationPool {
222    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223        f.debug_struct("ContinuationPool").finish_non_exhaustive()
224    }
225}
226
227impl ContinuationPool {
228    pub fn new() -> Self {
229        Self {
230            continuations: Rc::new(RefCell::new(VecDeque::new())),
231        }
232    }
233
234    /// Acquire a new continuation from the global pool. Panics if that pool was not yet initialized.
235    pub fn acquire(stack_size: usize) -> PooledContinuation {
236        CONTINUATION_POOL.with(|p| p.acquire_inner(stack_size))
237    }
238
239    fn acquire_inner(&self, stack_size: usize) -> PooledContinuation {
240        // TODO add a check to ensure that if we recycled a continuation, its
241        // TODO allocated stack size is at least the requested `stack_size`
242        let continuation = self
243            .continuations
244            .borrow_mut()
245            .pop_front()
246            .unwrap_or_else(move || Continuation::new(stack_size));
247
248        PooledContinuation {
249            continuation: Some(continuation),
250            queue: self.continuations.clone(),
251        }
252    }
253}
254
255/// A thin wrapper around a `Continuation` that returns it to a `ContinuationPool`
256/// when dropped, but only if it's reusable.
257pub struct PooledContinuation {
258    continuation: Option<Continuation>,
259    queue: Rc<RefCell<VecDeque<Continuation>>>,
260}
261
262impl Drop for PooledContinuation {
263    fn drop(&mut self) {
264        let mut c = self.continuation.take().unwrap();
265        if c.reusable() {
266            self.queue.borrow_mut().push_back(c);
267        } else if matches!(c.state, ContinuationState::Initialized) {
268            // A continuation which has been initialized but not run cannot be immediately reused.
269            // This is because arguments and captures may already have been moved into the function,
270            // and thus these moved objects won't be dropped until the function itself has been
271            // dropped. Thus we must drop the inner function before reusing it.
272            let old = c.function.0.replace(None);
273            c.state = ContinuationState::NotReady;
274            if std::thread::panicking() {
275                match UNGRACEFUL_SHUTDOWN_CONFIG.get().continuation_function_behavior {
276                    ContinuationFunctionBehavior::Drop => drop(old),
277                    ContinuationFunctionBehavior::Leak => std::mem::forget(old),
278                }
279            } else {
280                drop(old);
281            }
282            self.queue.borrow_mut().push_back(c);
283        }
284    }
285}
286
287impl Deref for PooledContinuation {
288    type Target = Continuation;
289
290    fn deref(&self) -> &Self::Target {
291        self.continuation.as_ref().unwrap()
292    }
293}
294
295impl DerefMut for PooledContinuation {
296    fn deref_mut(&mut self) -> &mut Self::Target {
297        self.continuation.as_mut().unwrap()
298    }
299}
300
301impl std::fmt::Debug for PooledContinuation {
302    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
303        f.debug_struct("PooledContinuation").finish()
304    }
305}
306
307// Safety: these aren't sent across real threads
308unsafe impl Send for PooledContinuation {}
309
310/// Possibly yield back to the executor to perform a context switch.  This function should be
311/// called *before* any visible operation. If each visible operation has a scheduling point
312/// before it, then there will be a potential context switch *in between* any pair of visible
313/// operations, which is a necessary condition for completeness.
314///
315/// Putting scheduling points before visible operations, rather than after, has the advantage
316/// of giving the scheduling algorithm additional information to make scheduling decisions
317/// based on what is about to happen on each task. The disadvantage of this approach is that it
318/// is more difficult to avoid double-yields for blocking operations, explained below.
319///
320/// In addition to the scheduling point before the operation begins, blocking operations will
321/// result in a *second* context switch if the current thread is blocked. As an optimization,
322/// the switch *before* the blocking operation can be conditionally omitted to avoid switching
323/// twice for the same operation iff (1) the operation *will* block and (2) if the act of
324/// blocking *commutes* with all other operations on that resource.
325///
326/// Reasoning: We can consider a blocking operation (`Y`) such as acquiring a mutex as two
327/// sub-operations (`Y1`) blocking and (`Y2`) proceeding after being unblocked. The double-yield
328/// optimization omits the scheduling point before `Y1`. For arbitrary events `X` and `Z` and
329/// intra-thread orderings `T1: X Y1 Y2` and `T2: Z`, we have four interleavings:
330///
331/// `X Z Y1 Y2`
332/// `X Y1 Z Y2`
333/// `Z X Y1 Y2`
334/// `X Y1 Y2 Z`
335///
336/// Note that the first interleaving is *not observable* if we omit the scheduling point before `Y1`.
337/// Thus to maintain behavioral completeness when omitting this scheduling point, all states
338/// observable from the first schedule must also be observable in one of the other schedules.
339///
340/// Observe that if `Y1` and `Z` commute, then the first two schedules are behaviorally equivalent,
341/// thus the optimization is safe. So, to ensure the safety of the double-yield optimization for an
342/// operation `Y1`, it suffices to check that `Y1` commutes with all operations `Z` on the same resource,
343/// as operations on other resources should commute trivially.
344#[track_caller]
345pub fn switch() {
346    crate::annotations::record_tick();
347    trace!("switch from {}", Location::caller());
348    if ExecutionState::maybe_yield() {
349        let yielder = ExecutionState::with(|state| state.current().yielder);
350
351        // SAFETY: A yielder reference will be valid for the lifetime of the continuation (see `corosensei::Coroutine::with_stack`)
352        // The yielder field is stored on the Task, whose lifetime is necessarily subsumed by the lifetime of the continuation which contains it.
353        // As a result, the task struct cannot contain an invalidated pointer to it's yielder. There are no mutable references to the yielder.
354        match unsafe { &(*yielder) }.suspend(ContinuationOutput::Yielded) {
355            ContinuationInput::Exit => panic!("unexpected exit continuation"),
356            ContinuationInput::Resume => {}
357        };
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364    use crate::config::Config;
365
366    #[test]
367    fn reusable_continuation_drop() {
368        let pool = ContinuationPool::new();
369        let config: Config = Default::default();
370
371        let mut c = pool.acquire_inner(config.stack_size);
372        c.initialize(Box::new(|| {
373            let _ = 1 + 1;
374        }));
375        let yielder = c.yielder;
376
377        let r = c.resume();
378        assert!(r, "continuation only has one step");
379
380        drop(c);
381        assert_eq!(
382            pool.continuations.borrow().len(),
383            1,
384            "continuation should be reusable because the function finished"
385        );
386
387        let mut c = pool.acquire_inner(config.stack_size);
388        c.initialize(Box::new(move || {
389            // SAFETY: We must use the yielder directly here because we are not in a Shuttle execution
390            // for `continuation::switch`, which requires access to `ExecutionState`. This is safe because
391            // the yielder's lifetime is valid as long as the continuation has not `Exited`, and the
392            // continuation cannot have exited if it is executing its current function.
393            unsafe { &(*yielder) }.suspend(ContinuationOutput::Yielded);
394            let _ = 1 + 1;
395        }));
396
397        let r = c.resume();
398        assert!(!r, "continuation yields once, shouldn't be finished yet");
399
400        drop(c);
401        assert_eq!(
402            pool.continuations.borrow().len(),
403            0,
404            "continuation should not be reusable because the function wasn't finished"
405        );
406
407        let c = pool.acquire_inner(config.stack_size);
408
409        // Check that it's safe for a continuation to outlive the pool
410        drop(pool);
411        drop(c);
412    }
413}