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. In particular, unwinding a
195 // stopped execution runs drop handlers, which can then panic if they interact with
196 // shuttle atomics, causing a panic-on-drop abort.
197 //
198 // SAFETY: `force_reset` leaks the coroutine. However, given that the execution is *already* aborting
199 // at this point and will soon exit, this is unlikely to cause issues. Leaking the coroutine here also
200 // avoids most tricky issues with scheduling points in drop handlers during a panic, which can often
201 // result in difficult-to-debug aborts from double-panics.
202 if std::thread::panicking() || ExecutionState::execution_stopped() {
203 unsafe {
204 self.coroutine.force_reset();
205 }
206 } else {
207 self.coroutine.force_unwind();
208 }
209 }
210 ContinuationState::Exited => {
211 // Already exited, nothing to do
212 }
213 }
214 }
215}
216
217/// A `ContinuationPool` just holds on to old `Continuation`s that are reusable, and vends
218/// them back out again. This amortizes the cost of allocating continuations, which involve
219/// allocating new stacks (`mmap`), `mprotect`, etc.
220pub struct ContinuationPool {
221 // invariant: if c is in this queue, c.reusable() == true
222 continuations: Rc<RefCell<VecDeque<Continuation>>>,
223}
224
225impl std::fmt::Debug for ContinuationPool {
226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227 f.debug_struct("ContinuationPool").finish_non_exhaustive()
228 }
229}
230
231impl ContinuationPool {
232 pub fn new() -> Self {
233 Self {
234 continuations: Rc::new(RefCell::new(VecDeque::new())),
235 }
236 }
237
238 /// Acquire a new continuation from the global pool. Panics if that pool was not yet initialized.
239 pub fn acquire(stack_size: usize) -> PooledContinuation {
240 CONTINUATION_POOL.with(|p| p.acquire_inner(stack_size))
241 }
242
243 fn acquire_inner(&self, stack_size: usize) -> PooledContinuation {
244 // TODO add a check to ensure that if we recycled a continuation, its
245 // TODO allocated stack size is at least the requested `stack_size`
246 let continuation = self
247 .continuations
248 .borrow_mut()
249 .pop_front()
250 .unwrap_or_else(move || Continuation::new(stack_size));
251
252 PooledContinuation {
253 continuation: Some(continuation),
254 queue: self.continuations.clone(),
255 }
256 }
257}
258
259/// A thin wrapper around a `Continuation` that returns it to a `ContinuationPool`
260/// when dropped, but only if it's reusable.
261pub struct PooledContinuation {
262 continuation: Option<Continuation>,
263 queue: Rc<RefCell<VecDeque<Continuation>>>,
264}
265
266impl Drop for PooledContinuation {
267 fn drop(&mut self) {
268 let mut c = self.continuation.take().unwrap();
269 if c.reusable() {
270 self.queue.borrow_mut().push_back(c);
271 } else if matches!(c.state, ContinuationState::Initialized) {
272 // A continuation which has been initialized but not run cannot be immediately reused.
273 // This is because arguments and captures may already have been moved into the function,
274 // and thus these moved objects won't be dropped until the function itself has been
275 // dropped. Thus we must drop the inner function before reusing it.
276 let old = c.function.0.replace(None);
277 c.state = ContinuationState::NotReady;
278 if std::thread::panicking() {
279 match UNGRACEFUL_SHUTDOWN_CONFIG.get().continuation_function_behavior {
280 ContinuationFunctionBehavior::Drop => drop(old),
281 ContinuationFunctionBehavior::Leak => std::mem::forget(old),
282 }
283 } else {
284 drop(old);
285 }
286 self.queue.borrow_mut().push_back(c);
287 }
288 }
289}
290
291impl Deref for PooledContinuation {
292 type Target = Continuation;
293
294 fn deref(&self) -> &Self::Target {
295 self.continuation.as_ref().unwrap()
296 }
297}
298
299impl DerefMut for PooledContinuation {
300 fn deref_mut(&mut self) -> &mut Self::Target {
301 self.continuation.as_mut().unwrap()
302 }
303}
304
305impl std::fmt::Debug for PooledContinuation {
306 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
307 f.debug_struct("PooledContinuation").finish()
308 }
309}
310
311// Safety: these aren't sent across real threads
312unsafe impl Send for PooledContinuation {}
313
314/// Possibly yield back to the executor to perform a context switch. This function should be
315/// called *before* any visible operation. If each visible operation has a scheduling point
316/// before it, then there will be a potential context switch *in between* any pair of visible
317/// operations, which is a necessary condition for completeness.
318///
319/// Putting scheduling points before visible operations, rather than after, has the advantage
320/// of giving the scheduling algorithm additional information to make scheduling decisions
321/// based on what is about to happen on each task. The disadvantage of this approach is that it
322/// is more difficult to avoid double-yields for blocking operations, explained below.
323///
324/// In addition to the scheduling point before the operation begins, blocking operations will
325/// result in a *second* context switch if the current thread is blocked. As an optimization,
326/// the switch *before* the blocking operation can be conditionally omitted to avoid switching
327/// twice for the same operation iff (1) the operation *will* block and (2) if the act of
328/// blocking *commutes* with all other operations on that resource.
329///
330/// Reasoning: We can consider a blocking operation (`Y`) such as acquiring a mutex as two
331/// sub-operations (`Y1`) blocking and (`Y2`) proceeding after being unblocked. The double-yield
332/// optimization omits the scheduling point before `Y1`. For arbitrary events `X` and `Z` and
333/// intra-thread orderings `T1: X Y1 Y2` and `T2: Z`, we have four interleavings:
334///
335/// `X Z Y1 Y2`
336/// `X Y1 Z Y2`
337/// `Z X Y1 Y2`
338/// `X Y1 Y2 Z`
339///
340/// Note that the first interleaving is *not observable* if we omit the scheduling point before `Y1`.
341/// Thus to maintain behavioral completeness when omitting this scheduling point, all states
342/// observable from the first schedule must also be observable in one of the other schedules.
343///
344/// Observe that if `Y1` and `Z` commute, then the first two schedules are behaviorally equivalent,
345/// thus the optimization is safe. So, to ensure the safety of the double-yield optimization for an
346/// operation `Y1`, it suffices to check that `Y1` commutes with all operations `Z` on the same resource,
347/// as operations on other resources should commute trivially.
348#[track_caller]
349pub fn switch() {
350 crate::annotations::record_tick();
351 trace!("switch from {}", Location::caller());
352 if ExecutionState::maybe_yield() {
353 let yielder = ExecutionState::with(|state| state.current().yielder);
354
355 // SAFETY: A yielder reference will be valid for the lifetime of the continuation (see `corosensei::Coroutine::with_stack`)
356 // The yielder field is stored on the Task, whose lifetime is necessarily subsumed by the lifetime of the continuation which contains it.
357 // As a result, the task struct cannot contain an invalidated pointer to it's yielder. There are no mutable references to the yielder.
358 match unsafe { &(*yielder) }.suspend(ContinuationOutput::Yielded) {
359 ContinuationInput::Exit => panic!("unexpected exit continuation"),
360 ContinuationInput::Resume => {}
361 };
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use crate::config::Config;
369
370 #[test]
371 fn reusable_continuation_drop() {
372 let pool = ContinuationPool::new();
373 let config: Config = Default::default();
374
375 let mut c = pool.acquire_inner(config.stack_size);
376 c.initialize(Box::new(|| {
377 let _ = 1 + 1;
378 }));
379 let yielder = c.yielder;
380
381 let r = c.resume();
382 assert!(r, "continuation only has one step");
383
384 drop(c);
385 assert_eq!(
386 pool.continuations.borrow().len(),
387 1,
388 "continuation should be reusable because the function finished"
389 );
390
391 let mut c = pool.acquire_inner(config.stack_size);
392 c.initialize(Box::new(move || {
393 // SAFETY: We must use the yielder directly here because we are not in a Shuttle execution
394 // for `continuation::switch`, which requires access to `ExecutionState`. This is safe because
395 // the yielder's lifetime is valid as long as the continuation has not `Exited`, and the
396 // continuation cannot have exited if it is executing its current function.
397 unsafe { &(*yielder) }.suspend(ContinuationOutput::Yielded);
398 let _ = 1 + 1;
399 }));
400
401 let r = c.resume();
402 assert!(!r, "continuation yields once, shouldn't be finished yet");
403
404 drop(c);
405 assert_eq!(
406 pool.continuations.borrow().len(),
407 0,
408 "continuation should not be reusable because the function wasn't finished"
409 );
410
411 let c = pool.acquire_inner(config.stack_size);
412
413 // Check that it's safe for a continuation to outlive the pool
414 drop(pool);
415 drop(c);
416 }
417}