Skip to main content

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}