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}