Add scheduler crate with unified TestScheduler and ForegroundExecutor

Nathan Sobo created

- Created new shared crate at crates/scheduler/
- Implemented minimal Scheduler trait with schedule_foreground and is_main_thread
- Added TestScheduler with deterministic execution and basic task queuing
- Added ForegroundExecutor wrapper for !Send futures
- Included test_basic_spawn_and_run passing test
- Follows Zed Rust guidelines: parking_lot Mutex, shadowing in scopes

Change summary

Cargo.lock                        |  11 ++
Cargo.toml                        |   1 
crates/scheduler/Cargo.toml       |  17 +++
crates/scheduler/src/scheduler.rs | 144 +++++++++++++++++++++++++++++++++
4 files changed, 173 insertions(+)

Detailed changes

Cargo.lock 🔗

@@ -14349,6 +14349,17 @@ dependencies = [
  "windows-sys 0.59.0",
 ]
 
+[[package]]
+name = "scheduler"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-task",
+ "parking_lot",
+ "rand 0.8.5",
+ "rand_chacha 0.3.1",
+]
+
 [[package]]
 name = "schema_generator"
 version = "0.1.0"

Cargo.toml 🔗

@@ -140,6 +140,7 @@ members = [
     "crates/rpc",
     "crates/rules_library",
     "crates/schema_generator",
+    "crates/scheduler",
     "crates/search",
     "crates/semantic_index",
     "crates/semantic_version",

crates/scheduler/Cargo.toml 🔗

@@ -0,0 +1,17 @@
+[package]
+name = "scheduler"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/scheduler.rs"
+
+[dependencies]
+anyhow = "1.0"
+async-task = "4.5"
+rand = { version = "0.8", features = ["small_rng"] }
+rand_chacha = "0.3"
+parking_lot = "0.12"
+
+[features]
+test-support = []

crates/scheduler/src/scheduler.rs 🔗

@@ -0,0 +1,144 @@
+use anyhow::Result;
+use async_task::Runnable;
+use parking_lot::Mutex;
+use rand_chacha::rand_core::SeedableRng;
+use rand_chacha::ChaCha8Rng;
+use std::any::Any;
+use std::collections::VecDeque;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::sync::Arc;
+
+use std::thread::{self, ThreadId};
+
+pub trait Scheduler: Send + Sync + Any {
+    fn schedule_foreground(&self, runnable: Runnable);
+    fn is_main_thread(&self) -> bool;
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, Hash)]
+pub struct TaskId(usize);
+
+pub struct Task<R>(async_task::Task<R>);
+
+impl<R> Task<R> {
+    pub fn id(&self) -> TaskId {
+        TaskId(0) // Placeholder
+    }
+}
+
+pub struct SchedulerConfig {
+    pub randomize_order: bool,
+    pub seed: u64,
+}
+
+impl Default for SchedulerConfig {
+    fn default() -> Self {
+        Self {
+            randomize_order: true,
+            seed: 0,
+        }
+    }
+}
+
+pub struct TestScheduler {
+    inner: Mutex<TestSchedulerInner>,
+}
+
+struct TestSchedulerInner {
+    rng: ChaCha8Rng,
+    foreground_queue: VecDeque<Runnable>,
+    creation_thread_id: ThreadId,
+}
+
+impl TestScheduler {
+    pub fn new(config: SchedulerConfig) -> Self {
+        Self {
+            inner: Mutex::new(TestSchedulerInner {
+                rng: ChaCha8Rng::seed_from_u64(config.seed),
+                foreground_queue: VecDeque::new(),
+                creation_thread_id: thread::current().id(),
+            }),
+        }
+    }
+
+    pub fn tick(&self, background_only: bool) -> bool {
+        let mut inner = self.inner.lock();
+        if !background_only {
+            if let Some(runnable) = inner.foreground_queue.pop_front() {
+                drop(inner); // Unlock while running
+                runnable.run();
+                return true;
+            }
+        }
+        false
+    }
+
+    pub fn run(&self) {
+        while self.tick(false) {}
+    }
+}
+
+impl Scheduler for TestScheduler {
+    fn schedule_foreground(&self, runnable: Runnable) {
+        self.inner.lock().foreground_queue.push_back(runnable);
+    }
+
+    fn is_main_thread(&self) -> bool {
+        thread::current().id() == self.inner.lock().creation_thread_id
+    }
+}
+
+pub struct ForegroundExecutor {
+    scheduler: Arc<dyn Scheduler>,
+    _phantom: PhantomData<()>,
+}
+
+impl ForegroundExecutor {
+    pub fn new(scheduler: Arc<dyn Scheduler>) -> Result<Self> {
+        Ok(Self {
+            scheduler,
+            _phantom: PhantomData,
+        })
+    }
+
+    pub fn spawn<R: 'static + Send>(
+        &self,
+        future: impl Future<Output = R> + Send + 'static,
+    ) -> Task<R> {
+        let scheduler = self.scheduler.clone();
+        let (runnable, task) = async_task::spawn_local(future, move |runnable| {
+            scheduler.schedule_foreground(runnable);
+        });
+        runnable.schedule();
+        Task(task)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::sync::atomic::{AtomicBool, Ordering};
+    use std::sync::Arc;
+
+    #[test]
+    fn test_basic_spawn_and_run() {
+        let scheduler = Arc::new(TestScheduler::new(SchedulerConfig::default()));
+        let executor = ForegroundExecutor::new(scheduler.clone()).unwrap();
+
+        let flag = Arc::new(AtomicBool::new(false));
+        assert!(!flag.load(Ordering::SeqCst));
+        let _task = executor.spawn({
+            let flag = flag.clone();
+            async move {
+                flag.store(true, Ordering::SeqCst);
+            }
+        });
+
+        assert!(!flag.load(Ordering::SeqCst));
+
+        scheduler.run();
+
+        assert!(flag.load(Ordering::SeqCst));
+    }
+}