Skip to main content

aft/executor/
mod.rs

1mod single_flight;
2
3#[cfg(test)]
4mod tests;
5
6use std::{
7    collections::{HashMap, VecDeque},
8    sync::{
9        atomic::{AtomicUsize, Ordering},
10        Arc,
11    },
12    thread::{self, JoinHandle},
13    time::{Duration, Instant},
14};
15
16use crossbeam_channel::{Receiver, RecvError, RecvTimeoutError, Sender};
17use parking_lot::{Mutex, RwLock};
18use tokio::sync::oneshot;
19
20use crate::{context::AppContext, path_identity::ProjectRootId, protocol::Response};
21
22pub use single_flight::SingleFlight;
23
24const JOB_COST: isize = 1;
25
26/// Scheduler lane for command-handler execution.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum Lane {
29    /// Pure read-only work. Runs under the actor epoch read gate and is capped
30    /// per actor.
31    PureRead,
32    /// LSP/status work. Serialized per actor by scheduler admission while still
33    /// using the shared epoch read gate.
34    SerialLspStatus,
35    /// Heavy lazy initialization. The scheduler acquires a process-wide heavy
36    /// permit before dispatch; the worker runs the build outside the epoch and
37    /// then takes a short write gate for the install point.
38    HeavyInit,
39    /// Mutating work. Becomes a writer barrier at the actor queue head, drains
40    /// in-flight reads, and runs under the actor epoch write gate.
41    Mutating,
42}
43
44/// Scheduler class used to keep deferrable maintenance from occupying the
45/// workers reserved for interactive route binds, tool calls, and bash.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub enum JobClass {
48    Interactive,
49    Maintenance,
50}
51
52pub type ExecutorJob = Box<dyn FnOnce(&AppContext) -> Response + Send + 'static>;
53
54#[derive(Debug, Clone)]
55pub struct ExecutorConfig {
56    pub pool_size: usize,
57    pub read_cap: usize,
58    pub actor_cap: usize,
59    pub heavy_permits: usize,
60    pub drr_quantum: isize,
61}
62
63impl Default for ExecutorConfig {
64    fn default() -> Self {
65        let available = thread::available_parallelism()
66            .map(usize::from)
67            .unwrap_or(2);
68        let pool_size = available.saturating_sub(1).clamp(2, 8);
69        let actor_cap = pool_size.saturating_sub(1).clamp(1, 4);
70        let read_cap = actor_cap.clamp(1, 4);
71        let heavy_permits = pool_size.saturating_sub(1).clamp(2, 3);
72
73        Self {
74            pool_size,
75            read_cap,
76            actor_cap,
77            heavy_permits,
78            drr_quantum: 1,
79        }
80    }
81}
82
83#[derive(Debug, Clone)]
84struct EffectiveConfig {
85    pool_size: usize,
86    read_cap: usize,
87    actor_cap: usize,
88    heavy_permits: usize,
89    drr_quantum: isize,
90    deficit_cap: isize,
91    interactive_reserve: usize,
92    maintenance_cap: usize,
93}
94
95impl ExecutorConfig {
96    fn effective(&self) -> EffectiveConfig {
97        let pool_size = self.pool_size.clamp(2, 8);
98        let max_actor_cap = pool_size.saturating_sub(1).max(1);
99        let actor_cap = self.actor_cap.max(1).min(max_actor_cap);
100        let read_cap = self.read_cap.max(1).min(actor_cap).min(4);
101        let heavy_permits = self.heavy_permits.clamp(2, 3);
102        let drr_quantum = self.drr_quantum.max(1);
103        let deficit_cap = (actor_cap.max(1) as isize) * 4;
104        let interactive_reserve = if pool_size >= 4 { 2 } else { 1 };
105        let maintenance_cap = pool_size.saturating_sub(interactive_reserve).max(1);
106
107        EffectiveConfig {
108            pool_size,
109            read_cap,
110            actor_cap,
111            heavy_permits,
112            drr_quantum,
113            deficit_cap,
114            interactive_reserve,
115            maintenance_cap,
116        }
117    }
118}
119
120/// Synchronous completion handle used by the executor tests and the
121/// future standalone bridge.
122pub struct CompletionHandle {
123    rx: Receiver<Response>,
124}
125
126impl CompletionHandle {
127    pub fn recv(self) -> Result<Response, RecvError> {
128        self.rx.recv()
129    }
130
131    pub fn recv_timeout(&self, timeout: Duration) -> Result<Response, RecvTimeoutError> {
132        self.rx.recv_timeout(timeout)
133    }
134
135    pub fn into_receiver(self) -> Receiver<Response> {
136        self.rx
137    }
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct DispatchClassQueueSnapshot {
142    pub queued: usize,
143    pub oldest_age_ms: Option<u64>,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct DispatchRunningSnapshot {
148    pub interactive: usize,
149    pub maintenance: usize,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct DispatchLivenessSnapshot {
154    pub interactive: DispatchClassQueueSnapshot,
155    pub maintenance: DispatchClassQueueSnapshot,
156    pub running: DispatchRunningSnapshot,
157    pub interactive_reserve: usize,
158    pub maintenance_cap: usize,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct MutatingLaneSnapshot {
163    pub root_id: ProjectRootId,
164    pub request_id: String,
165    pub command: String,
166    pub started_age_ms: u64,
167}
168
169#[derive(Debug, Clone)]
170struct RunningMutatingJob {
171    request_id: String,
172    command: String,
173    started_at: Instant,
174}
175
176/// Concurrent scheduler-dispatch executor.
177pub struct Executor {
178    inner: Arc<ExecutorInner>,
179}
180
181impl Executor {
182    pub fn new() -> Self {
183        Self::with_config(ExecutorConfig::default())
184    }
185
186    pub fn with_config(config: ExecutorConfig) -> Self {
187        let effective = config.effective();
188        let state = Arc::new(Mutex::new(SchedulerState::new(effective.clone())));
189        let heavy = Arc::new(HeavySemaphore::new(effective.heavy_permits));
190        let nonrunnable_dispatches = Arc::new(AtomicUsize::new(0));
191        let (run_tx, run_rx) = crossbeam_channel::unbounded();
192        let (event_tx, event_rx) = crossbeam_channel::unbounded();
193
194        let scheduler_state = Arc::clone(&state);
195        let scheduler_heavy = Arc::clone(&heavy);
196        let scheduler_violations = Arc::clone(&nonrunnable_dispatches);
197        let scheduler_handle = thread::Builder::new()
198            .name("aft-executor-scheduler".to_string())
199            .spawn(move || {
200                scheduler_loop(
201                    scheduler_state,
202                    scheduler_heavy,
203                    run_tx,
204                    event_rx,
205                    scheduler_violations,
206                );
207            })
208            .expect("spawn AFT executor scheduler");
209
210        let mut worker_handles = Vec::with_capacity(effective.pool_size);
211        for worker_id in 0..effective.pool_size {
212            let worker_rx = run_rx.clone();
213            let worker_events = event_tx.clone();
214            let handle = thread::Builder::new()
215                .name(format!("aft-executor-worker-{worker_id}"))
216                .spawn(move || worker_loop(worker_rx, worker_events))
217                .expect("spawn AFT executor worker");
218            worker_handles.push(handle);
219        }
220
221        Self {
222            inner: Arc::new(ExecutorInner {
223                state,
224                event_tx,
225                scheduler_handle: Mutex::new(Some(scheduler_handle)),
226                worker_handles: Mutex::new(worker_handles),
227                config: effective,
228                nonrunnable_dispatches,
229            }),
230        }
231    }
232
233    /// Register an actor if one is not already present.
234    ///
235    /// Existing actors keep their current context and scheduler state; subc
236    /// routing reuses them and reconfigures through the Mutating lane
237    /// rather than replacing the per-root [`AppContext`]. Returns `true` when a
238    /// new actor was inserted.
239    pub fn register_actor(&self, root_id: ProjectRootId, ctx: Arc<AppContext>) -> bool {
240        let inserted = {
241            let mut state = self.inner.state.lock();
242            if state.actors.contains_key(&root_id) {
243                false
244            } else {
245                state.actor_order.push(root_id.clone());
246                state.actors.insert(root_id, ActorState::new(ctx));
247                true
248            }
249        };
250        self.wake_scheduler();
251        inserted
252    }
253
254    /// Remove an actor from scheduler state.
255    ///
256    /// This is intentionally minimal: subc uses it only for a just-created
257    /// RouteBind actor whose configure failed before any route was installed, so
258    /// there is no in-flight work to quiesce. The removed [`AppContext`] is
259    /// dropped after releasing the scheduler lock so watcher/LSP teardown never
260    /// runs under that mutex.
261    pub fn remove_actor(&self, root_id: &ProjectRootId) {
262        let removed = {
263            let mut state = self.inner.state.lock();
264            state.actor_order.retain(|actor_root| actor_root != root_id);
265            state.actors.remove(root_id)
266        };
267        drop(removed);
268        self.wake_scheduler();
269    }
270
271    /// Snapshot the registered actor contexts.
272    ///
273    /// The returned [`Arc`]s keep contexts alive after the scheduler lock is
274    /// released, so callers can run teardown without holding executor state.
275    pub fn actor_contexts(&self) -> Vec<Arc<AppContext>> {
276        let state = self.inner.state.lock();
277        state
278            .actors
279            .values()
280            .map(|actor_state| Arc::clone(&actor_state.ctx))
281            .collect()
282    }
283
284    /// Snapshot the registered root ids paired with their actor contexts.
285    pub fn actor_entries(&self) -> Vec<(ProjectRootId, Arc<AppContext>)> {
286        let state = self.inner.state.lock();
287        state
288            .actors
289            .iter()
290            .map(|(root_id, actor_state)| (root_id.clone(), Arc::clone(&actor_state.ctx)))
291            .collect()
292    }
293
294    /// Non-blocking variant for the health path: the probe reply must stay
295    /// cheap under any load, so it skips the actor list (reported as busy)
296    /// rather than waiting on the scheduler state lock.
297    pub fn try_actor_entries(&self) -> Option<Vec<(ProjectRootId, Arc<AppContext>)>> {
298        let state = self.inner.state.try_lock()?;
299        Some(
300            state
301                .actors
302                .iter()
303                .map(|(root_id, actor_state)| (root_id.clone(), Arc::clone(&actor_state.ctx)))
304                .collect(),
305        )
306    }
307
308    pub fn submit(
309        &self,
310        root_id: ProjectRootId,
311        lane: Lane,
312        request_id: String,
313        job: ExecutorJob,
314    ) -> CompletionHandle {
315        let (completion_tx, completion_rx) = crossbeam_channel::bounded(1);
316        self.submit_with_completion(
317            root_id,
318            JobClass::Interactive,
319            lane,
320            request_id,
321            job,
322            CompletionSender::Sync(completion_tx),
323        );
324        CompletionHandle { rx: completion_rx }
325    }
326
327    pub fn submit_async(
328        &self,
329        root_id: ProjectRootId,
330        lane: Lane,
331        request_id: String,
332        job: ExecutorJob,
333    ) -> oneshot::Receiver<Response> {
334        let (completion_tx, completion_rx) = oneshot::channel();
335        self.submit_with_completion(
336            root_id,
337            JobClass::Interactive,
338            lane,
339            request_id,
340            job,
341            CompletionSender::Async(completion_tx),
342        );
343        completion_rx
344    }
345
346    pub fn submit_maintenance_async(
347        &self,
348        root_id: ProjectRootId,
349        lane: Lane,
350        request_id: String,
351        job: ExecutorJob,
352    ) -> oneshot::Receiver<Response> {
353        let (completion_tx, completion_rx) = oneshot::channel();
354        self.submit_with_completion(
355            root_id,
356            JobClass::Maintenance,
357            lane,
358            request_id,
359            job,
360            CompletionSender::Async(completion_tx),
361        );
362        completion_rx
363    }
364
365    fn submit_with_completion(
366        &self,
367        root_id: ProjectRootId,
368        job_class: JobClass,
369        lane: Lane,
370        request_id: String,
371        job: ExecutorJob,
372        completion: CompletionSender,
373    ) {
374        let command = job_command(job_class, lane);
375        let mut job = Some(job);
376        let mut completion = Some(completion);
377
378        let response = {
379            let mut state = self.inner.state.lock();
380            match state.actors.get_mut(&root_id) {
381                Some(actor) if actor.fatal => Some(actor_fatal_response(request_id.clone())),
382                Some(actor) => {
383                    actor.push_job(
384                        job_class,
385                        lane,
386                        QueuedJob {
387                            job: job.take().expect("executor job already queued"),
388                            completion: completion
389                                .take()
390                                .expect("executor completion already queued"),
391                            request_id: request_id.clone(),
392                            command,
393                            queued_at: Instant::now(),
394                        },
395                    );
396                    None
397                }
398                None => Some(Response::error(
399                    request_id.clone(),
400                    "actor_not_registered",
401                    "executor actor is not registered",
402                )),
403            }
404        };
405
406        if let Some(response) = response {
407            if let Some(completion) = completion {
408                completion.send(response);
409            }
410            return;
411        }
412
413        self.wake_scheduler();
414    }
415
416    pub fn pool_size(&self) -> usize {
417        self.inner.config.pool_size
418    }
419
420    pub fn actor_cap(&self) -> usize {
421        self.inner.config.actor_cap
422    }
423
424    pub fn read_cap(&self) -> usize {
425        self.inner.config.read_cap
426    }
427
428    pub fn heavy_permits(&self) -> usize {
429        self.inner.config.heavy_permits
430    }
431
432    pub fn interactive_reserve(&self) -> usize {
433        self.inner.config.interactive_reserve
434    }
435
436    pub fn maintenance_cap(&self) -> usize {
437        self.inner.config.maintenance_cap
438    }
439
440    pub fn try_dispatch_liveness_snapshot(&self) -> Option<DispatchLivenessSnapshot> {
441        self.inner
442            .state
443            .try_lock()
444            .map(|state| state.dispatch_liveness_snapshot())
445    }
446
447    pub fn try_mutating_lane_snapshots(&self) -> Option<Vec<MutatingLaneSnapshot>> {
448        self.inner
449            .state
450            .try_lock()
451            .map(|state| state.mutating_lane_snapshots())
452    }
453
454    pub fn try_mutating_job_state_label(
455        &self,
456        root_id: &ProjectRootId,
457        request_id: &str,
458    ) -> Option<&'static str> {
459        self.inner
460            .state
461            .try_lock()
462            .map(|state| state.mutating_job_state_label(root_id, request_id))
463    }
464
465    pub fn nonrunnable_dispatch_count(&self) -> usize {
466        self.inner.nonrunnable_dispatches.load(Ordering::Acquire)
467    }
468
469    pub fn actor_is_fatal(&self, root_id: &ProjectRootId) -> bool {
470        self.inner
471            .state
472            .lock()
473            .actors
474            .get(root_id)
475            .map(|actor| actor.fatal)
476            .unwrap_or(false)
477    }
478
479    fn wake_scheduler(&self) {
480        let _ = self.inner.event_tx.send(SchedulerEvent::Wake);
481    }
482}
483
484impl Default for Executor {
485    fn default() -> Self {
486        Self::new()
487    }
488}
489
490struct ExecutorInner {
491    state: Arc<Mutex<SchedulerState>>,
492    event_tx: Sender<SchedulerEvent>,
493    scheduler_handle: Mutex<Option<JoinHandle<()>>>,
494    worker_handles: Mutex<Vec<JoinHandle<()>>>,
495    config: EffectiveConfig,
496    nonrunnable_dispatches: Arc<AtomicUsize>,
497}
498
499impl Drop for ExecutorInner {
500    fn drop(&mut self) {
501        let _ = self.event_tx.send(SchedulerEvent::Shutdown);
502
503        if let Some(handle) = self.scheduler_handle.lock().take() {
504            let _ = handle.join();
505        }
506
507        let mut workers = self.worker_handles.lock();
508        for handle in workers.drain(..) {
509            let _ = handle.join();
510        }
511    }
512}
513
514struct SchedulerState {
515    actors: HashMap<ProjectRootId, ActorState>,
516    actor_order: Vec<ProjectRootId>,
517    cursor: usize,
518    idle_workers: usize,
519    interactive_inflight: usize,
520    maintenance_inflight: usize,
521    config: EffectiveConfig,
522}
523
524impl SchedulerState {
525    fn new(config: EffectiveConfig) -> Self {
526        Self {
527            actors: HashMap::new(),
528            actor_order: Vec::new(),
529            cursor: 0,
530            idle_workers: config.pool_size,
531            interactive_inflight: 0,
532            maintenance_inflight: 0,
533            config,
534        }
535    }
536
537    fn dispatch_liveness_snapshot(&self) -> DispatchLivenessSnapshot {
538        let now = Instant::now();
539        let mut interactive = QueueSnapshotAccumulator::default();
540        let mut maintenance = QueueSnapshotAccumulator::default();
541        for actor in self.actors.values() {
542            interactive.add(actor.class_queues(JobClass::Interactive), now);
543            maintenance.add(actor.class_queues(JobClass::Maintenance), now);
544        }
545
546        DispatchLivenessSnapshot {
547            interactive: interactive.finish(),
548            maintenance: maintenance.finish(),
549            running: DispatchRunningSnapshot {
550                interactive: self.interactive_inflight,
551                maintenance: self.maintenance_inflight,
552            },
553            interactive_reserve: self.config.interactive_reserve,
554            maintenance_cap: self.config.maintenance_cap,
555        }
556    }
557
558    fn mutating_lane_snapshots(&self) -> Vec<MutatingLaneSnapshot> {
559        let now = Instant::now();
560        let mut snapshots: Vec<_> = self
561            .actors
562            .iter()
563            .filter_map(|(root_id, actor)| {
564                actor
565                    .mutating_inflight
566                    .as_ref()
567                    .map(|job| MutatingLaneSnapshot {
568                        root_id: root_id.clone(),
569                        request_id: job.request_id.clone(),
570                        command: job.command.clone(),
571                        started_age_ms: duration_millis_u64(
572                            now.saturating_duration_since(job.started_at),
573                        ),
574                    })
575            })
576            .collect();
577        snapshots.sort_by(|left, right| left.root_id.as_path().cmp(right.root_id.as_path()));
578        snapshots
579    }
580
581    fn mutating_job_state_label(&self, root_id: &ProjectRootId, request_id: &str) -> &'static str {
582        let Some(actor) = self.actors.get(root_id) else {
583            return "actor_missing";
584        };
585        if actor
586            .mutating_inflight
587            .as_ref()
588            .is_some_and(|job| job.request_id == request_id)
589        {
590            return "running";
591        }
592        if actor.has_queued_mutating_job(request_id) {
593            return "queued";
594        }
595        if actor.writer_inflight {
596            return "blocked_by_other_mutating";
597        }
598        "not_found"
599    }
600}
601
602#[derive(Default)]
603struct QueueSnapshotAccumulator {
604    queued: usize,
605    oldest_age_ms: Option<u64>,
606}
607
608impl QueueSnapshotAccumulator {
609    fn add(&mut self, queues: &ClassQueues, now: Instant) {
610        self.queued += queues.queued_count();
611        if let Some(queued_at) = queues.oldest_queued_at() {
612            let age_ms = duration_millis_u64(now.saturating_duration_since(queued_at));
613            self.oldest_age_ms = Some(
614                self.oldest_age_ms
615                    .map_or(age_ms, |oldest| oldest.max(age_ms)),
616            );
617        }
618    }
619
620    fn finish(self) -> DispatchClassQueueSnapshot {
621        DispatchClassQueueSnapshot {
622            queued: self.queued,
623            oldest_age_ms: self.oldest_age_ms,
624        }
625    }
626}
627
628fn duration_millis_u64(duration: Duration) -> u64 {
629    duration.as_millis().min(u128::from(u64::MAX)) as u64
630}
631
632struct ActorState {
633    ctx: Arc<AppContext>,
634    epoch: Arc<RwLock<()>>,
635    read_inflight: usize,
636    lsp_inflight: bool,
637    actor_total_inflight: usize,
638    writer_inflight: bool,
639    mutating_inflight: Option<RunningMutatingJob>,
640    deficit: isize,
641    interactive: ClassQueues,
642    maintenance: ClassQueues,
643    fatal: bool,
644}
645
646impl ActorState {
647    fn new(ctx: Arc<AppContext>) -> Self {
648        Self {
649            ctx,
650            epoch: Arc::new(RwLock::new(())),
651            read_inflight: 0,
652            lsp_inflight: false,
653            actor_total_inflight: 0,
654            writer_inflight: false,
655            mutating_inflight: None,
656            deficit: 0,
657            interactive: ClassQueues::new(),
658            maintenance: ClassQueues::new(),
659            fatal: false,
660        }
661    }
662
663    fn push_job(&mut self, job_class: JobClass, lane: Lane, job: QueuedJob) {
664        self.class_queues_mut(job_class).push_job(lane, job);
665    }
666
667    fn has_queued_jobs(&self) -> bool {
668        self.interactive.has_queued_jobs() || self.maintenance.has_queued_jobs()
669    }
670
671    fn has_queued_jobs_for(&self, job_class: JobClass) -> bool {
672        self.class_queues(job_class).has_queued_jobs()
673    }
674
675    fn front_lane(&self, job_class: JobClass) -> Option<Lane> {
676        self.class_queues(job_class).front_lane()
677    }
678
679    fn pop_front_job(&mut self, job_class: JobClass, lane: Lane) -> Option<QueuedJob> {
680        self.class_queues_mut(job_class).pop_front_job(lane)
681    }
682
683    fn higher_priority_writer_barrier_blocks(&self, job_class: JobClass) -> bool {
684        matches!(job_class, JobClass::Maintenance)
685            && self.front_lane(JobClass::Interactive) == Some(Lane::Mutating)
686    }
687
688    fn class_queues(&self, job_class: JobClass) -> &ClassQueues {
689        match job_class {
690            JobClass::Interactive => &self.interactive,
691            JobClass::Maintenance => &self.maintenance,
692        }
693    }
694
695    fn class_queues_mut(&mut self, job_class: JobClass) -> &mut ClassQueues {
696        match job_class {
697            JobClass::Interactive => &mut self.interactive,
698            JobClass::Maintenance => &mut self.maintenance,
699        }
700    }
701
702    fn fail_queued_jobs(&mut self) {
703        self.interactive.fail_queued_jobs();
704        self.maintenance.fail_queued_jobs();
705    }
706
707    fn has_queued_mutating_job(&self, request_id: &str) -> bool {
708        self.interactive.has_queued_mutating_job(request_id)
709            || self.maintenance.has_queued_mutating_job(request_id)
710    }
711}
712
713struct ClassQueues {
714    order: VecDeque<Lane>,
715    pure_reads: VecDeque<QueuedJob>,
716    lsp_status: VecDeque<QueuedJob>,
717    heavy_init: VecDeque<QueuedJob>,
718    mutating: VecDeque<QueuedJob>,
719}
720
721impl ClassQueues {
722    fn new() -> Self {
723        Self {
724            order: VecDeque::new(),
725            pure_reads: VecDeque::new(),
726            lsp_status: VecDeque::new(),
727            heavy_init: VecDeque::new(),
728            mutating: VecDeque::new(),
729        }
730    }
731
732    fn push_job(&mut self, lane: Lane, job: QueuedJob) {
733        self.order.push_back(lane);
734        self.queue_mut(lane).push_back(job);
735    }
736
737    fn has_queued_jobs(&self) -> bool {
738        !self.order.is_empty()
739    }
740
741    fn front_lane(&self) -> Option<Lane> {
742        self.order.front().copied()
743    }
744
745    fn pop_front_job(&mut self, lane: Lane) -> Option<QueuedJob> {
746        let order_lane = self.order.pop_front()?;
747        debug_assert_eq!(order_lane, lane);
748        self.queue_mut(lane).pop_front()
749    }
750
751    fn queued_count(&self) -> usize {
752        self.order.len()
753    }
754
755    fn oldest_queued_at(&self) -> Option<Instant> {
756        self.front_lane()
757            .and_then(|lane| self.queue(lane).front().map(|job| job.queued_at))
758    }
759
760    fn fail_queued_jobs(&mut self) {
761        self.order.clear();
762        fail_queued_job_queue(&mut self.pure_reads);
763        fail_queued_job_queue(&mut self.lsp_status);
764        fail_queued_job_queue(&mut self.heavy_init);
765        fail_queued_job_queue(&mut self.mutating);
766    }
767
768    fn has_queued_mutating_job(&self, request_id: &str) -> bool {
769        self.mutating.iter().any(|job| job.request_id == request_id)
770    }
771
772    fn queue(&self, lane: Lane) -> &VecDeque<QueuedJob> {
773        match lane {
774            Lane::PureRead => &self.pure_reads,
775            Lane::SerialLspStatus => &self.lsp_status,
776            Lane::HeavyInit => &self.heavy_init,
777            Lane::Mutating => &self.mutating,
778        }
779    }
780
781    fn queue_mut(&mut self, lane: Lane) -> &mut VecDeque<QueuedJob> {
782        match lane {
783            Lane::PureRead => &mut self.pure_reads,
784            Lane::SerialLspStatus => &mut self.lsp_status,
785            Lane::HeavyInit => &mut self.heavy_init,
786            Lane::Mutating => &mut self.mutating,
787        }
788    }
789}
790
791struct QueuedJob {
792    job: ExecutorJob,
793    completion: CompletionSender,
794    request_id: String,
795    command: String,
796    queued_at: Instant,
797}
798
799fn fail_queued_job_queue(queue: &mut VecDeque<QueuedJob>) {
800    for queued in queue.drain(..) {
801        queued
802            .completion
803            .send(actor_fatal_response(queued.request_id));
804    }
805}
806
807fn job_command(job_class: JobClass, lane: Lane) -> String {
808    format!("executor::{job_class:?}::{lane:?}")
809}
810
811fn actor_fatal_response(request_id: impl Into<String>) -> Response {
812    Response::error(
813        request_id,
814        "actor_fatal",
815        "executor actor is fatal after a mutating job panic",
816    )
817}
818
819fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> String {
820    if let Some(message) = payload.downcast_ref::<&'static str>() {
821        (*message).to_string()
822    } else if let Some(message) = payload.downcast_ref::<String>() {
823        message.clone()
824    } else {
825        "unknown panic payload".to_string()
826    }
827}
828
829fn panic_response(
830    request_id: impl Into<String>,
831    command: &str,
832    payload: &(dyn std::any::Any + Send),
833) -> Response {
834    let panic_message = panic_payload_message(payload);
835    Response::error(
836        request_id,
837        "actor_fatal",
838        format!("command '{command}' panicked: {panic_message}"),
839    )
840}
841
842enum CompletionSender {
843    Sync(Sender<Response>),
844    Async(oneshot::Sender<Response>),
845}
846
847impl CompletionSender {
848    fn send(self, response: Response) {
849        match self {
850            Self::Sync(tx) => {
851                let _ = tx.send(response);
852            }
853            Self::Async(tx) => {
854                let _ = tx.send(response);
855            }
856        }
857    }
858}
859
860struct RunJob {
861    root_id: ProjectRootId,
862    job_class: JobClass,
863    lane: Lane,
864    ctx: Arc<AppContext>,
865    epoch: Arc<RwLock<()>>,
866    job: ExecutorJob,
867    completion: Option<CompletionSender>,
868    request_id: String,
869    command: String,
870    heavy_permit: Option<HeavyPermit>,
871}
872
873struct CompletionEvent {
874    root_id: ProjectRootId,
875    job_class: JobClass,
876    lane: Lane,
877    heavy_permit: Option<HeavyPermit>,
878    panicked: bool,
879}
880
881enum SchedulerEvent {
882    Wake,
883    Completed(CompletionEvent),
884    Shutdown,
885}
886
887fn scheduler_loop(
888    state: Arc<Mutex<SchedulerState>>,
889    heavy: Arc<HeavySemaphore>,
890    run_tx: Sender<RunJob>,
891    event_rx: Receiver<SchedulerEvent>,
892    nonrunnable_dispatches: Arc<AtomicUsize>,
893) {
894    while let Ok(event) = event_rx.recv() {
895        let mut shutdown = false;
896        {
897            let mut state = state.lock();
898            shutdown |= process_scheduler_event(event, &mut state);
899            while !shutdown {
900                match event_rx.try_recv() {
901                    Ok(event) => shutdown |= process_scheduler_event(event, &mut state),
902                    Err(_) => break,
903                }
904            }
905
906            if !shutdown {
907                dispatch_runnable(&mut state, &heavy, &run_tx, &nonrunnable_dispatches);
908            }
909        }
910
911        if shutdown {
912            break;
913        }
914    }
915}
916
917fn process_scheduler_event(event: SchedulerEvent, state: &mut SchedulerState) -> bool {
918    match event {
919        SchedulerEvent::Wake => false,
920        SchedulerEvent::Completed(event) => {
921            complete_job(state, event);
922            false
923        }
924        SchedulerEvent::Shutdown => true,
925    }
926}
927
928fn complete_job(state: &mut SchedulerState, event: CompletionEvent) {
929    let CompletionEvent {
930        root_id,
931        job_class,
932        lane,
933        heavy_permit,
934        panicked,
935    } = event;
936
937    match job_class {
938        JobClass::Interactive => {
939            state.interactive_inflight = state.interactive_inflight.saturating_sub(1);
940        }
941        JobClass::Maintenance => {
942            state.maintenance_inflight = state.maintenance_inflight.saturating_sub(1);
943        }
944    }
945
946    if let Some(actor) = state.actors.get_mut(&root_id) {
947        actor.actor_total_inflight = actor.actor_total_inflight.saturating_sub(1);
948        match lane {
949            Lane::PureRead => {
950                actor.read_inflight = actor.read_inflight.saturating_sub(1);
951            }
952            Lane::SerialLspStatus => {
953                actor.lsp_inflight = false;
954            }
955            Lane::HeavyInit => {}
956            Lane::Mutating => {
957                actor.writer_inflight = false;
958                actor.mutating_inflight = None;
959            }
960        }
961
962        if panicked && lane == Lane::Mutating {
963            actor.fatal = true;
964            actor.fail_queued_jobs();
965        }
966    }
967
968    drop(heavy_permit);
969    state.idle_workers += 1;
970}
971
972fn dispatch_runnable(
973    state: &mut SchedulerState,
974    heavy: &Arc<HeavySemaphore>,
975    run_tx: &Sender<RunJob>,
976    nonrunnable_dispatches: &AtomicUsize,
977) {
978    while state.idle_workers > 0 && !state.actor_order.is_empty() {
979        let mut made_progress = false;
980        let mut dispatch_failed = false;
981
982        made_progress |= dispatch_runnable_class(
983            state,
984            JobClass::Interactive,
985            heavy,
986            run_tx,
987            nonrunnable_dispatches,
988            &mut dispatch_failed,
989        );
990        if dispatch_failed || state.idle_workers == 0 {
991            return;
992        }
993
994        if can_dispatch_class(state, JobClass::Maintenance) {
995            made_progress |= dispatch_runnable_class(
996                state,
997                JobClass::Maintenance,
998                heavy,
999                run_tx,
1000                nonrunnable_dispatches,
1001                &mut dispatch_failed,
1002            );
1003            if dispatch_failed {
1004                return;
1005            }
1006        }
1007
1008        if !made_progress {
1009            break;
1010        }
1011    }
1012}
1013
1014fn dispatch_runnable_class(
1015    state: &mut SchedulerState,
1016    job_class: JobClass,
1017    heavy: &Arc<HeavySemaphore>,
1018    run_tx: &Sender<RunJob>,
1019    nonrunnable_dispatches: &AtomicUsize,
1020    dispatch_failed: &mut bool,
1021) -> bool {
1022    if !can_dispatch_class(state, job_class) || state.actor_order.is_empty() {
1023        return false;
1024    }
1025
1026    let actor_count = state.actor_order.len();
1027    let mut made_progress = false;
1028
1029    for _ in 0..actor_count {
1030        if !can_dispatch_class(state, job_class) || state.actor_order.is_empty() {
1031            break;
1032        }
1033
1034        if state.cursor >= state.actor_order.len() {
1035            state.cursor = 0;
1036        }
1037        let root_id = state.actor_order[state.cursor].clone();
1038        state.cursor = (state.cursor + 1) % state.actor_order.len();
1039
1040        let run_job = {
1041            let Some(actor) = state.actors.get_mut(&root_id) else {
1042                continue;
1043            };
1044
1045            if actor.fatal {
1046                actor.fail_queued_jobs();
1047                actor.deficit = 0;
1048                continue;
1049            }
1050
1051            if !actor.has_queued_jobs() {
1052                actor.deficit = 0;
1053                continue;
1054            }
1055
1056            if !actor.has_queued_jobs_for(job_class) {
1057                continue;
1058            }
1059
1060            actor.deficit =
1061                (actor.deficit + state.config.drr_quantum).min(state.config.deficit_cap);
1062            if actor.deficit < JOB_COST {
1063                continue;
1064            }
1065
1066            try_admit_actor(&root_id, actor, job_class, &state.config, heavy)
1067        };
1068
1069        if let Some(run_job) = run_job {
1070            state.idle_workers -= 1;
1071            match job_class {
1072                JobClass::Interactive => state.interactive_inflight += 1,
1073                JobClass::Maintenance => state.maintenance_inflight += 1,
1074            }
1075            made_progress = true;
1076            if run_tx.send(run_job).is_err() {
1077                nonrunnable_dispatches.fetch_add(1, Ordering::AcqRel);
1078                *dispatch_failed = true;
1079                return made_progress;
1080            }
1081        }
1082    }
1083
1084    made_progress
1085}
1086
1087fn can_dispatch_class(state: &SchedulerState, job_class: JobClass) -> bool {
1088    if state.idle_workers == 0 {
1089        return false;
1090    }
1091    match job_class {
1092        JobClass::Interactive => true,
1093        JobClass::Maintenance => {
1094            state.maintenance_inflight < state.config.maintenance_cap
1095                && state.idle_workers > state.config.interactive_reserve
1096        }
1097    }
1098}
1099
1100fn try_admit_actor(
1101    root_id: &ProjectRootId,
1102    actor: &mut ActorState,
1103    job_class: JobClass,
1104    config: &EffectiveConfig,
1105    heavy: &Arc<HeavySemaphore>,
1106) -> Option<RunJob> {
1107    let lane = actor.front_lane(job_class)?;
1108    let mut heavy_permit = None;
1109
1110    if actor.writer_inflight || actor.higher_priority_writer_barrier_blocks(job_class) {
1111        return None;
1112    }
1113
1114    let has_epoch_reader = actor.read_inflight > 0 || actor.lsp_inflight;
1115    let actor_has_capacity = actor.actor_total_inflight < config.actor_cap;
1116    let runnable = match lane {
1117        Lane::PureRead => actor.read_inflight < config.read_cap && actor_has_capacity,
1118        Lane::SerialLspStatus => !actor.lsp_inflight && actor_has_capacity,
1119        Lane::HeavyInit => {
1120            if !actor_has_capacity {
1121                false
1122            } else if let Some(permit) = heavy.try_acquire() {
1123                heavy_permit = Some(permit);
1124                true
1125            } else {
1126                false
1127            }
1128        }
1129        Lane::Mutating => !has_epoch_reader && actor_has_capacity,
1130    };
1131
1132    if !runnable {
1133        return None;
1134    }
1135
1136    let queued = actor.pop_front_job(job_class, lane)?;
1137    actor.deficit -= JOB_COST;
1138    if lane == Lane::Mutating {
1139        actor.mutating_inflight = Some(RunningMutatingJob {
1140            request_id: queued.request_id.clone(),
1141            command: queued.command.clone(),
1142            started_at: Instant::now(),
1143        });
1144    }
1145    match lane {
1146        Lane::PureRead => {
1147            actor.read_inflight += 1;
1148            actor.actor_total_inflight += 1;
1149        }
1150        Lane::SerialLspStatus => {
1151            actor.lsp_inflight = true;
1152            actor.actor_total_inflight += 1;
1153        }
1154        Lane::HeavyInit => {
1155            actor.actor_total_inflight += 1;
1156        }
1157        Lane::Mutating => {
1158            actor.writer_inflight = true;
1159            actor.actor_total_inflight += 1;
1160        }
1161    }
1162
1163    Some(RunJob {
1164        root_id: root_id.clone(),
1165        job_class,
1166        lane,
1167        ctx: Arc::clone(&actor.ctx),
1168        epoch: Arc::clone(&actor.epoch),
1169        job: queued.job,
1170        completion: Some(queued.completion),
1171        request_id: queued.request_id,
1172        command: queued.command,
1173        heavy_permit,
1174    })
1175}
1176
1177fn worker_loop(run_rx: Receiver<RunJob>, event_tx: Sender<SchedulerEvent>) {
1178    while let Ok(mut run_job) = run_rx.recv() {
1179        let response =
1180            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run_lane_job(&mut run_job)));
1181        let panicked = response.is_err();
1182        let response = match response {
1183            Ok(response) => response,
1184            Err(payload) => panic_response(
1185                run_job.request_id.clone(),
1186                &run_job.command,
1187                payload.as_ref(),
1188            ),
1189        };
1190
1191        if let Some(completion) = run_job.completion.take() {
1192            completion.send(response);
1193        }
1194        let completion = CompletionEvent {
1195            root_id: run_job.root_id,
1196            job_class: run_job.job_class,
1197            lane: run_job.lane,
1198            heavy_permit: run_job.heavy_permit.take(),
1199            panicked,
1200        };
1201        let _ = event_tx.send(SchedulerEvent::Completed(completion));
1202    }
1203}
1204
1205fn run_lane_job(run_job: &mut RunJob) -> Response {
1206    let missing_request_id = run_job.request_id.clone();
1207    let job = std::mem::replace(
1208        &mut run_job.job,
1209        Box::new(move |_| {
1210            Response::error(
1211                missing_request_id,
1212                "job_missing",
1213                "executor job already taken",
1214            )
1215        }),
1216    );
1217
1218    match run_job.lane {
1219        Lane::PureRead | Lane::SerialLspStatus => {
1220            let _epoch = run_job.epoch.read();
1221            job(&run_job.ctx)
1222        }
1223        Lane::HeavyInit => {
1224            let response = job(&run_job.ctx);
1225            {
1226                let _install = run_job.epoch.write();
1227            }
1228            response
1229        }
1230        Lane::Mutating => {
1231            let _epoch = run_job.epoch.write();
1232            job(&run_job.ctx)
1233        }
1234    }
1235}
1236
1237#[derive(Debug)]
1238struct HeavySemaphore {
1239    available: AtomicUsize,
1240    max: usize,
1241}
1242
1243impl HeavySemaphore {
1244    fn new(permits: usize) -> Self {
1245        Self {
1246            available: AtomicUsize::new(permits),
1247            max: permits,
1248        }
1249    }
1250
1251    fn try_acquire(self: &Arc<Self>) -> Option<HeavyPermit> {
1252        loop {
1253            let available = self.available.load(Ordering::Acquire);
1254            if available == 0 {
1255                return None;
1256            }
1257            if self
1258                .available
1259                .compare_exchange(
1260                    available,
1261                    available - 1,
1262                    Ordering::AcqRel,
1263                    Ordering::Acquire,
1264                )
1265                .is_ok()
1266            {
1267                return Some(HeavyPermit {
1268                    semaphore: Arc::clone(self),
1269                });
1270            }
1271        }
1272    }
1273}
1274
1275struct HeavyPermit {
1276    semaphore: Arc<HeavySemaphore>,
1277}
1278
1279impl Drop for HeavyPermit {
1280    fn drop(&mut self) {
1281        let previous = self.semaphore.available.fetch_add(1, Ordering::Release);
1282        debug_assert!(previous < self.semaphore.max);
1283    }
1284}