seq_runtime/scheduler/lifecycle.rs
1//! Scheduler lifecycle: init, run, shutdown, wait_all_strands.
2
3use crate::stack::Stack;
4use std::sync::atomic::Ordering;
5use std::sync::{Once, OnceLock};
6use std::time::{Duration, Instant};
7
8use super::{ACTIVE_STRANDS, SHUTDOWN_CONDVAR, SHUTDOWN_MUTEX};
9
10static SCHEDULER_INIT: Once = Once::new();
11static SCHEDULER_START_TIME: OnceLock<Instant> = OnceLock::new();
12
13/// Default coroutine stack size: 128KB (0x20000 bytes)
14/// Reduced from 1MB for better spawn performance (~16% faster in benchmarks).
15/// Can be overridden via SEQ_STACK_SIZE environment variable.
16pub(super) const DEFAULT_STACK_SIZE: usize = 0x20000;
17
18/// Default coroutine pool capacity.
19/// May reuses completed coroutine stacks from this pool to avoid allocations.
20/// Default of 1000 is often too small for spawn-heavy workloads.
21const DEFAULT_POOL_CAPACITY: usize = 10000;
22
23/// Parse stack size from an optional string value.
24/// Returns the parsed size, or DEFAULT_STACK_SIZE if the value is missing, zero, or invalid.
25/// Prints a warning to stderr for invalid values.
26pub(super) fn parse_stack_size(env_value: Option<String>) -> usize {
27 match env_value {
28 Some(val) => match val.parse::<usize>() {
29 Ok(0) => {
30 eprintln!(
31 "Warning: SEQ_STACK_SIZE=0 is invalid, using default {}",
32 DEFAULT_STACK_SIZE
33 );
34 DEFAULT_STACK_SIZE
35 }
36 Ok(size) => size,
37 Err(_) => {
38 eprintln!(
39 "Warning: SEQ_STACK_SIZE='{}' is not a valid number, using default {}",
40 val, DEFAULT_STACK_SIZE
41 );
42 DEFAULT_STACK_SIZE
43 }
44 },
45 None => DEFAULT_STACK_SIZE,
46 }
47}
48
49/// Get elapsed time since scheduler was initialized
50pub fn scheduler_elapsed() -> Option<Duration> {
51 SCHEDULER_START_TIME.get().map(|start| start.elapsed())
52}
53
54/// Initialize the scheduler.
55///
56/// # Safety
57/// Safe to call multiple times (idempotent via Once).
58/// Configures May coroutines with appropriate stack size for LLVM-generated code.
59#[unsafe(no_mangle)]
60pub unsafe extern "C" fn patch_seq_scheduler_init() {
61 SCHEDULER_INIT.call_once(|| {
62 // Configure stack size for coroutines
63 // Default is 128KB, reduced from 1MB for better spawn performance.
64 // Can be overridden via SEQ_STACK_SIZE environment variable (in bytes)
65 // Example: SEQ_STACK_SIZE=2097152 for 2MB
66 // Invalid values (non-numeric, zero) are warned and ignored.
67 let stack_size = parse_stack_size(std::env::var("SEQ_STACK_SIZE").ok());
68
69 // Configure coroutine pool capacity
70 // May reuses coroutine stacks from this pool to reduce allocation overhead.
71 // Default 10000 is 10x May's default (1000), better for spawn-heavy workloads.
72 // Can be overridden via SEQ_POOL_CAPACITY environment variable.
73 let pool_capacity = std::env::var("SEQ_POOL_CAPACITY")
74 .ok()
75 .and_then(|s| s.parse().ok())
76 .filter(|&v| v > 0)
77 .unwrap_or(DEFAULT_POOL_CAPACITY);
78
79 may::config()
80 .set_stack_size(stack_size)
81 .set_pool_capacity(pool_capacity);
82
83 // Record scheduler start time (for at-exit reporting)
84 SCHEDULER_START_TIME.get_or_init(Instant::now);
85
86 // Install SIGINT handler for Ctrl-C (unconditional - basic expected behavior)
87 // Without this, tight loops won't respond to Ctrl-C because signals
88 // are only delivered at syscall boundaries, and TCO loops may never syscall.
89 #[cfg(unix)]
90 {
91 use std::sync::atomic::AtomicBool;
92 static SIGINT_RECEIVED: AtomicBool = AtomicBool::new(false);
93
94 extern "C" fn sigint_handler(_: libc::c_int) {
95 // If we receive SIGINT twice, force exit (user is insistent)
96 if SIGINT_RECEIVED.swap(true, Ordering::SeqCst) {
97 // Second SIGINT - exit immediately
98 unsafe { libc::_exit(130) }; // 128 + 2 (SIGINT)
99 }
100 // First SIGINT - exit cleanly
101 std::process::exit(130);
102 }
103
104 unsafe {
105 libc::signal(
106 libc::SIGINT,
107 sigint_handler as *const () as libc::sighandler_t,
108 );
109 }
110 }
111
112 // Install SIGQUIT handler for runtime diagnostics (kill -3)
113 #[cfg(feature = "diagnostics")]
114 crate::diagnostics::install_signal_handler();
115
116 // Install watchdog timer (if enabled via SEQ_WATCHDOG_SECS)
117 #[cfg(feature = "diagnostics")]
118 crate::watchdog::install_watchdog();
119 });
120}
121
122/// Run the scheduler and wait for all coroutines to complete
123///
124/// # Safety
125/// Returns the final stack (always null for now since May handles all scheduling).
126/// This function blocks until all spawned strands have completed.
127///
128/// Uses a condition variable for event-driven shutdown synchronization rather than
129/// polling. The mutex is only held during the wait protocol, not during strand
130/// execution, so there's no contention on the hot path.
131#[unsafe(no_mangle)]
132pub unsafe extern "C" fn patch_seq_scheduler_run() -> Stack {
133 let mut guard = SHUTDOWN_MUTEX.lock().expect(
134 "scheduler_run: shutdown mutex poisoned - strand panicked during shutdown synchronization",
135 );
136
137 // Wait for all strands to complete
138 // The condition variable will be notified when the last strand exits
139 while ACTIVE_STRANDS.load(Ordering::Acquire) > 0 {
140 guard = SHUTDOWN_CONDVAR
141 .wait(guard)
142 .expect("scheduler_run: condvar wait failed - strand panicked during shutdown wait");
143 }
144
145 // All strands have completed
146 std::ptr::null_mut()
147}
148
149/// Shutdown the scheduler
150///
151/// # Safety
152/// Safe to call. May doesn't require explicit shutdown, so this is a no-op.
153#[unsafe(no_mangle)]
154pub unsafe extern "C" fn patch_seq_scheduler_shutdown() {
155 // May doesn't require explicit shutdown
156 // This function exists for API symmetry with init
157}
158
159/// Wait for all strands to complete
160///
161/// # Safety
162/// Always safe to call. Blocks until all spawned strands have completed.
163///
164/// Uses event-driven synchronization via condition variable - no polling overhead.
165#[unsafe(no_mangle)]
166pub unsafe extern "C" fn patch_seq_wait_all_strands() {
167 let mut guard = SHUTDOWN_MUTEX.lock()
168 .expect("wait_all_strands: shutdown mutex poisoned - strand panicked during shutdown synchronization");
169
170 // Wait for all strands to complete
171 // The condition variable will be notified when the last strand exits
172 while ACTIVE_STRANDS.load(Ordering::Acquire) > 0 {
173 guard = SHUTDOWN_CONDVAR
174 .wait(guard)
175 .expect("wait_all_strands: condvar wait failed - strand panicked during shutdown wait");
176 }
177}