1mod single_flight;
2
3#[cfg(test)]
4mod tests;
5
6use std::{
7 collections::{HashMap, VecDeque},
8 sync::{
9 atomic::{AtomicU64, AtomicU8, 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum Lane {
29 PureRead,
32 SerialLspStatus,
35 HeavyInit,
39 Mutating,
44 MaintenanceCommit,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub enum JobClass {
56 Interactive,
57 Maintenance,
58}
59
60pub type ExecutorJob = Box<dyn FnOnce(&AppContext) -> Response + Send + 'static>;
61
62const INTERACTIVE_WRITER_PROMOTION_AGE: Duration = Duration::from_secs(6);
69
70#[derive(Debug, Clone)]
71pub struct ExecutorConfig {
72 pub pool_size: usize,
73 pub read_cap: usize,
74 pub actor_cap: usize,
75 pub heavy_permits: usize,
76 pub drr_quantum: isize,
77}
78
79impl Default for ExecutorConfig {
80 fn default() -> Self {
81 let available = thread::available_parallelism()
82 .map(usize::from)
83 .unwrap_or(2);
84 let pool_size = available.saturating_sub(1).clamp(2, 8);
85 let actor_cap = pool_size.saturating_sub(1).clamp(1, 4);
86 let read_cap = actor_cap.clamp(1, 4);
87 let heavy_permits = pool_size.saturating_sub(1).clamp(2, 3);
88
89 Self {
90 pool_size,
91 read_cap,
92 actor_cap,
93 heavy_permits,
94 drr_quantum: 1,
95 }
96 }
97}
98
99#[derive(Debug, Clone)]
100struct EffectiveConfig {
101 pool_size: usize,
102 read_cap: usize,
103 actor_cap: usize,
104 heavy_permits: usize,
105 drr_quantum: isize,
106 deficit_cap: isize,
107 interactive_reserve: usize,
108 maintenance_cap: usize,
109}
110
111impl ExecutorConfig {
112 fn effective(&self) -> EffectiveConfig {
113 let pool_size = self.pool_size.clamp(2, 8);
114 let max_actor_cap = pool_size.saturating_sub(1).max(1);
115 let actor_cap = self.actor_cap.max(1).min(max_actor_cap);
116 let read_cap = self.read_cap.max(1).min(actor_cap).min(4);
117 let heavy_permits = self
121 .heavy_permits
122 .max(1)
123 .min(pool_size.saturating_sub(1).max(1))
124 .min(3);
125 let drr_quantum = self.drr_quantum.max(1);
126 let deficit_cap = (actor_cap.max(1) as isize) * 4;
127 let interactive_reserve = if pool_size >= 4 { 2 } else { 1 };
128 let maintenance_cap = pool_size.saturating_sub(interactive_reserve).max(1);
129
130 EffectiveConfig {
131 pool_size,
132 read_cap,
133 actor_cap,
134 heavy_permits,
135 drr_quantum,
136 deficit_cap,
137 interactive_reserve,
138 maintenance_cap,
139 }
140 }
141}
142
143pub struct CompletionHandle {
146 rx: Receiver<Response>,
147}
148
149impl CompletionHandle {
150 pub fn recv(self) -> Result<Response, RecvError> {
151 self.rx.recv()
152 }
153
154 pub fn recv_timeout(&self, timeout: Duration) -> Result<Response, RecvTimeoutError> {
155 self.rx.recv_timeout(timeout)
156 }
157
158 pub fn into_receiver(self) -> Receiver<Response> {
159 self.rx
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct DispatchClassQueueSnapshot {
165 pub queued: usize,
166 pub oldest_age_ms: Option<u64>,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct DispatchRunningSnapshot {
171 pub interactive: usize,
172 pub maintenance: usize,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct DispatchLivenessSnapshot {
177 pub interactive: DispatchClassQueueSnapshot,
178 pub maintenance: DispatchClassQueueSnapshot,
179 pub running: DispatchRunningSnapshot,
180 pub interactive_reserve: usize,
181 pub maintenance_cap: usize,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct MutatingLaneSnapshot {
186 pub root_id: ProjectRootId,
187 pub request_id: String,
188 pub command: String,
189 pub started_age_ms: u64,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct BindBlockerSnapshot {
195 pub configure_state: &'static str,
196 pub configure_phase_timings: Option<String>,
197 pub blockers: Vec<String>,
198}
199
200#[derive(Debug, Clone)]
211pub struct JobCancellation {
212 inner: Arc<JobCancellationInner>,
213}
214
215#[derive(Debug)]
216struct JobCancellationInner {
217 state: AtomicU8,
218}
219
220const JOB_CANCEL_STATE_PENDING: u8 = 0;
221const JOB_CANCEL_STATE_RUNNING: u8 = 1;
222const JOB_CANCEL_STATE_COMMITTED: u8 = 2;
223const JOB_CANCEL_STATE_CANCELLED: u8 = 3;
224
225impl JobCancellation {
226 pub(crate) fn new() -> Self {
227 Self {
228 inner: Arc::new(JobCancellationInner {
229 state: AtomicU8::new(JOB_CANCEL_STATE_PENDING),
230 }),
231 }
232 }
233
234 fn mark_running(&self) {
235 let _ = self.inner.state.compare_exchange(
236 JOB_CANCEL_STATE_PENDING,
237 JOB_CANCEL_STATE_RUNNING,
238 Ordering::SeqCst,
239 Ordering::SeqCst,
240 );
241 }
242
243 #[must_use]
247 pub fn try_seal_committed(&self) -> bool {
248 loop {
249 let current = self.state();
250 match current {
251 JOB_CANCEL_STATE_CANCELLED => return false,
252 JOB_CANCEL_STATE_COMMITTED => return true,
253 _ => {
254 if self
255 .inner
256 .state
257 .compare_exchange(
258 current,
259 JOB_CANCEL_STATE_COMMITTED,
260 Ordering::SeqCst,
261 Ordering::SeqCst,
262 )
263 .is_ok()
264 {
265 return true;
266 }
267 }
268 }
269 }
270 }
271
272 fn signal_cancel(&self) -> u8 {
275 loop {
276 let current = self.state();
277 match current {
278 JOB_CANCEL_STATE_COMMITTED | JOB_CANCEL_STATE_CANCELLED => return current,
279 _ => {
280 if self
281 .inner
282 .state
283 .compare_exchange(
284 current,
285 JOB_CANCEL_STATE_CANCELLED,
286 Ordering::SeqCst,
287 Ordering::SeqCst,
288 )
289 .is_ok()
290 {
291 return current;
292 }
293 }
294 }
295 }
296 }
297
298 fn state(&self) -> u8 {
299 self.inner.state.load(Ordering::SeqCst)
300 }
301
302 pub fn cancel_requested_before_commit(&self) -> bool {
304 self.state() == JOB_CANCEL_STATE_CANCELLED
305 }
306
307 fn same_token(&self, other: &JobCancellation) -> bool {
308 Arc::ptr_eq(&self.inner, &other.inner)
309 }
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub enum JobCancelOutcome {
315 QueuedRemoved,
318 RunningSignalled,
321 RunningCommitted,
324 NotFound,
326}
327
328thread_local! {
329 static CURRENT_JOB_CANCELLATION: std::cell::RefCell<Option<JobCancellation>> =
330 const { std::cell::RefCell::new(None) };
331}
332
333pub fn current_job_cancellation() -> Option<JobCancellation> {
336 CURRENT_JOB_CANCELLATION.with(|slot| slot.borrow().clone())
337}
338
339struct CurrentJobCancellationGuard {
340 previous: Option<JobCancellation>,
341}
342
343impl CurrentJobCancellationGuard {
344 fn install(token: Option<JobCancellation>) -> Self {
345 let previous = CURRENT_JOB_CANCELLATION.with(|slot| slot.replace(token));
346 Self { previous }
347 }
348}
349
350impl Drop for CurrentJobCancellationGuard {
351 fn drop(&mut self) {
352 CURRENT_JOB_CANCELLATION.with(|slot| {
353 *slot.borrow_mut() = self.previous.take();
354 });
355 }
356}
357
358#[derive(Debug, Clone)]
359struct RunningJob {
360 root_id: ProjectRootId,
361 request_id: String,
362 command: String,
363 job_class: JobClass,
364 lane: Lane,
365 started_at: Instant,
366}
367
368#[derive(Debug, Clone)]
369struct RunningMutatingJob {
370 request_id: String,
371 command: String,
372 started_at: Instant,
373}
374
375pub struct Executor {
377 inner: Arc<ExecutorInner>,
378}
379
380impl Executor {
381 pub fn new() -> Self {
382 Self::with_config(ExecutorConfig::default())
383 }
384
385 pub fn with_config(config: ExecutorConfig) -> Self {
386 let effective = config.effective();
387 let state = Arc::new(Mutex::new(SchedulerState::new(effective.clone())));
388 let heavy = Arc::new(HeavySemaphore::new(effective.heavy_permits));
389 let nonrunnable_dispatches = Arc::new(AtomicUsize::new(0));
390 let completed_interactive = Arc::new(AtomicU64::new(0));
391 let completed_maintenance = Arc::new(AtomicU64::new(0));
392 let (run_tx, run_rx) = crossbeam_channel::unbounded();
393 let (event_tx, event_rx) = crossbeam_channel::unbounded();
394
395 let scheduler_state = Arc::clone(&state);
396 let scheduler_heavy = Arc::clone(&heavy);
397 let scheduler_violations = Arc::clone(&nonrunnable_dispatches);
398 let scheduler_completed_interactive = Arc::clone(&completed_interactive);
399 let scheduler_completed_maintenance = Arc::clone(&completed_maintenance);
400 let scheduler_handle = thread::Builder::new()
401 .name("aft-executor-scheduler".to_string())
402 .spawn(move || {
403 scheduler_loop(
404 scheduler_state,
405 scheduler_heavy,
406 run_tx,
407 event_rx,
408 scheduler_violations,
409 scheduler_completed_interactive,
410 scheduler_completed_maintenance,
411 );
412 })
413 .expect("spawn AFT executor scheduler");
414
415 let mut worker_handles = Vec::with_capacity(effective.pool_size);
416 for worker_id in 0..effective.pool_size {
417 let worker_rx = run_rx.clone();
418 let worker_events = event_tx.clone();
419 let handle = thread::Builder::new()
420 .name(format!("aft-executor-worker-{worker_id}"))
421 .spawn(move || worker_loop(worker_rx, worker_events))
422 .expect("spawn AFT executor worker");
423 worker_handles.push(handle);
424 }
425
426 Self {
427 inner: Arc::new(ExecutorInner {
428 state,
429 event_tx,
430 scheduler_handle: Mutex::new(Some(scheduler_handle)),
431 worker_handles: Mutex::new(worker_handles),
432 config: effective,
433 nonrunnable_dispatches,
434 completed_interactive,
435 completed_maintenance,
436 }),
437 }
438 }
439
440 pub fn register_actor(&self, root_id: ProjectRootId, ctx: Arc<AppContext>) -> bool {
447 let memory_root = root_id.as_path().to_path_buf();
448 let inserted = {
449 let mut state = self.inner.state.lock();
450 if state.actors.contains_key(&root_id) {
451 false
452 } else {
453 state.actor_order.push(root_id.clone());
454 state
455 .actors
456 .insert(root_id, ActorState::new(Arc::clone(&ctx)));
457 true
458 }
459 };
460 if inserted {
461 let app = ctx.app();
462 app.register_memory_context(memory_root, &ctx);
463 app.actor_root_registered();
464 }
465 self.wake_scheduler();
466 inserted
467 }
468
469 pub fn remove_actor(&self, root_id: &ProjectRootId) {
477 let removed = {
478 let mut state = self.inner.state.lock();
479 state.actor_order.retain(|actor_root| actor_root != root_id);
480 state.actors.remove(root_id)
481 };
482 if let Some(actor) = removed.as_ref() {
483 let app = actor.ctx.app();
484 app.unregister_memory_context(root_id.as_path(), &actor.ctx);
485 app.actor_root_unregistered();
486 }
487 drop(removed);
488 self.wake_scheduler();
489 }
490
491 pub fn actor_is_idle(&self, root_id: &ProjectRootId) -> bool {
493 let state = self.inner.state.lock();
494 state.actors.get(root_id).is_some_and(ActorState::is_idle)
495 }
496
497 pub fn try_actor_is_idle(&self, root_id: &ProjectRootId) -> Option<bool> {
501 let state = self.inner.state.try_lock()?;
502 Some(state.actors.get(root_id).is_some_and(ActorState::is_idle))
503 }
504
505 pub fn retire_idle_actor_in_background(&self, root_id: &ProjectRootId) -> bool {
509 let removed = {
510 let mut state = self.inner.state.lock();
511 if !state.actors.get(root_id).is_some_and(ActorState::is_idle) {
512 return false;
513 }
514 state.actor_order.retain(|actor_root| actor_root != root_id);
515 state.actors.remove(root_id)
516 };
517 let Some(actor) = removed else {
518 return false;
519 };
520 let app = actor.ctx.app();
521 app.unregister_memory_context(root_id.as_path(), &actor.ctx);
522 app.actor_root_unregistered();
523 std::thread::spawn(move || {
524 actor.ctx.teardown_deleted_root();
525 drop(actor);
526 });
527 self.wake_scheduler();
528 true
529 }
530
531 pub fn cancel_queued_maintenance(&self, root_id: &ProjectRootId) -> usize {
537 let cancelled = {
538 let mut state = self.inner.state.lock();
539 state
540 .actors
541 .get_mut(root_id)
542 .map(|actor| actor.maintenance.cancel_queued_jobs())
543 .unwrap_or(0)
544 };
545 if cancelled > 0 {
546 self.wake_scheduler();
547 }
548 cancelled
549 }
550
551 pub fn actor_registered(&self, root_id: &ProjectRootId) -> bool {
553 let state = self.inner.state.lock();
554 state.actors.contains_key(root_id)
555 }
556
557 pub fn actor_context(&self, root_id: &ProjectRootId) -> Option<Arc<AppContext>> {
560 let state = self.inner.state.lock();
561 state
562 .actors
563 .get(root_id)
564 .map(|actor| Arc::clone(&actor.ctx))
565 }
566
567 pub fn actor_contexts(&self) -> Vec<Arc<AppContext>> {
572 let state = self.inner.state.lock();
573 state
574 .actors
575 .values()
576 .map(|actor_state| Arc::clone(&actor_state.ctx))
577 .collect()
578 }
579
580 pub fn actor_entries(&self) -> Vec<(ProjectRootId, Arc<AppContext>)> {
582 let state = self.inner.state.lock();
583 state
584 .actors
585 .iter()
586 .map(|(root_id, actor_state)| (root_id.clone(), Arc::clone(&actor_state.ctx)))
587 .collect()
588 }
589
590 pub fn try_actor_entries(&self) -> Option<Vec<(ProjectRootId, Arc<AppContext>)>> {
594 let state = self.inner.state.try_lock()?;
595 Some(
596 state
597 .actors
598 .iter()
599 .map(|(root_id, actor_state)| (root_id.clone(), Arc::clone(&actor_state.ctx)))
600 .collect(),
601 )
602 }
603
604 pub fn submit(
605 &self,
606 root_id: ProjectRootId,
607 lane: Lane,
608 request_id: String,
609 job: ExecutorJob,
610 ) -> CompletionHandle {
611 let (completion_tx, completion_rx) = crossbeam_channel::bounded(1);
612 self.submit_with_completion(
613 root_id,
614 JobClass::Interactive,
615 lane,
616 request_id,
617 job,
618 CompletionSender::Sync(completion_tx),
619 );
620 CompletionHandle { rx: completion_rx }
621 }
622
623 pub fn submit_async(
624 &self,
625 root_id: ProjectRootId,
626 lane: Lane,
627 request_id: String,
628 job: ExecutorJob,
629 ) -> oneshot::Receiver<Response> {
630 let (completion_tx, completion_rx) = oneshot::channel();
631 self.submit_with_completion(
632 root_id,
633 JobClass::Interactive,
634 lane,
635 request_id,
636 job,
637 CompletionSender::Async(completion_tx),
638 );
639 completion_rx
640 }
641
642 pub fn submit_cancellable_async(
648 &self,
649 root_id: ProjectRootId,
650 lane: Lane,
651 request_id: String,
652 job: ExecutorJob,
653 ) -> (oneshot::Receiver<Response>, JobCancellation) {
654 let cancellation = JobCancellation::new();
655 let (completion_tx, completion_rx) = oneshot::channel();
656 self.submit_with_completion_cancellable(
657 root_id,
658 JobClass::Interactive,
659 lane,
660 request_id,
661 job,
662 CompletionSender::Async(completion_tx),
663 Some(cancellation.clone()),
664 );
665 (completion_rx, cancellation)
666 }
667
668 pub fn cancel_job(&self, root_id: &ProjectRootId, token: &JobCancellation) -> JobCancelOutcome {
674 let observed = token.signal_cancel();
681 let (outcome, settled) = {
682 let mut state = self.inner.state.lock();
683 let Some(actor) = state.actors.get_mut(root_id) else {
684 return match observed {
685 JOB_CANCEL_STATE_COMMITTED => JobCancelOutcome::RunningCommitted,
686 JOB_CANCEL_STATE_RUNNING | JOB_CANCEL_STATE_PENDING => {
687 JobCancelOutcome::RunningSignalled
688 }
689 _ => JobCancelOutcome::NotFound,
690 };
691 };
692 match actor.remove_queued_cancellable(token) {
693 Some(queued) => (JobCancelOutcome::QueuedRemoved, Some(queued)),
694 None => match observed {
695 JOB_CANCEL_STATE_COMMITTED => (JobCancelOutcome::RunningCommitted, None),
697 JOB_CANCEL_STATE_RUNNING | JOB_CANCEL_STATE_PENDING => {
701 (JobCancelOutcome::RunningSignalled, None)
702 }
703 _ => (JobCancelOutcome::NotFound, None),
705 },
706 }
707 };
708 if let Some(queued) = settled {
709 queued.completion.send(Response::error(
710 queued.request_id,
711 "request_cancelled",
712 "request cancelled before execution",
713 ));
714 self.wake_scheduler();
715 }
716 outcome
717 }
718
719 pub fn submit_maintenance_async(
720 &self,
721 root_id: ProjectRootId,
722 lane: Lane,
723 request_id: String,
724 job: ExecutorJob,
725 ) -> oneshot::Receiver<Response> {
726 let (completion_tx, completion_rx) = oneshot::channel();
727 self.submit_with_completion(
728 root_id,
729 JobClass::Maintenance,
730 lane,
731 request_id,
732 job,
733 CompletionSender::Async(completion_tx),
734 );
735 completion_rx
736 }
737
738 fn submit_with_completion(
739 &self,
740 root_id: ProjectRootId,
741 job_class: JobClass,
742 lane: Lane,
743 request_id: String,
744 job: ExecutorJob,
745 completion: CompletionSender,
746 ) {
747 self.submit_with_completion_cancellable(
748 root_id, job_class, lane, request_id, job, completion, None,
749 );
750 }
751
752 #[allow(clippy::too_many_arguments)]
753 fn submit_with_completion_cancellable(
754 &self,
755 root_id: ProjectRootId,
756 job_class: JobClass,
757 lane: Lane,
758 request_id: String,
759 job: ExecutorJob,
760 completion: CompletionSender,
761 cancellation: Option<JobCancellation>,
762 ) {
763 let command = job_command(job_class, lane);
764 let mut job = Some(job);
765 let mut completion = Some(completion);
766
767 let response = {
768 let mut state = self.inner.state.lock();
769 match state.actors.get_mut(&root_id) {
770 Some(actor) if actor.fatal => Some(actor_fatal_response(request_id.clone())),
771 Some(actor) => {
772 actor.push_job(
773 job_class,
774 lane,
775 QueuedJob {
776 job: job.take().expect("executor job already queued"),
777 completion: completion
778 .take()
779 .expect("executor completion already queued"),
780 request_id: request_id.clone(),
781 command,
782 queued_at: Instant::now(),
783 cancellation: cancellation.clone(),
784 },
785 );
786 None
787 }
788 None => Some(Response::error(
789 request_id.clone(),
790 "actor_not_registered",
791 "executor actor is not registered",
792 )),
793 }
794 };
795
796 if let Some(response) = response {
797 if let Some(completion) = completion {
798 completion.send(response);
799 }
800 return;
801 }
802
803 self.wake_scheduler();
804 }
805
806 pub fn pool_size(&self) -> usize {
807 self.inner.config.pool_size
808 }
809
810 pub fn actor_cap(&self) -> usize {
811 self.inner.config.actor_cap
812 }
813
814 pub fn read_cap(&self) -> usize {
815 self.inner.config.read_cap
816 }
817
818 pub fn heavy_permits(&self) -> usize {
819 self.inner.config.heavy_permits
820 }
821
822 pub fn interactive_reserve(&self) -> usize {
823 self.inner.config.interactive_reserve
824 }
825
826 pub fn maintenance_cap(&self) -> usize {
827 self.inner.config.maintenance_cap
828 }
829
830 pub fn try_dispatch_liveness_snapshot(&self) -> Option<DispatchLivenessSnapshot> {
831 self.inner
832 .state
833 .try_lock()
834 .map(|state| state.dispatch_liveness_snapshot())
835 }
836
837 pub fn try_mutating_lane_snapshots(&self) -> Option<Vec<MutatingLaneSnapshot>> {
838 self.inner
839 .state
840 .try_lock()
841 .map(|state| state.mutating_lane_snapshots())
842 }
843
844 pub fn try_mutating_job_state_label(
845 &self,
846 root_id: &ProjectRootId,
847 request_id: &str,
848 ) -> Option<&'static str> {
849 self.inner
850 .state
851 .try_lock()
852 .map(|state| state.mutating_job_state_label(root_id, request_id))
853 }
854
855 pub fn try_bind_blocker_snapshot(
859 &self,
860 root_id: &ProjectRootId,
861 request_id: &str,
862 ) -> Option<BindBlockerSnapshot> {
863 self.inner
864 .state
865 .try_lock()
866 .map(|state| state.bind_blocker_snapshot(root_id, request_id))
867 }
868
869 pub fn nonrunnable_dispatch_count(&self) -> usize {
870 self.inner.nonrunnable_dispatches.load(Ordering::Acquire)
871 }
872
873 pub fn completion_counts(&self) -> (u64, u64) {
874 (
875 self.inner.completed_interactive.load(Ordering::Relaxed),
876 self.inner.completed_maintenance.load(Ordering::Relaxed),
877 )
878 }
879
880 pub fn actor_is_fatal(&self, root_id: &ProjectRootId) -> bool {
881 self.inner
882 .state
883 .lock()
884 .actors
885 .get(root_id)
886 .map(|actor| actor.fatal)
887 .unwrap_or(false)
888 }
889
890 fn wake_scheduler(&self) {
891 let _ = self.inner.event_tx.send(SchedulerEvent::Wake);
892 }
893}
894
895impl Default for Executor {
896 fn default() -> Self {
897 Self::new()
898 }
899}
900
901struct ExecutorInner {
902 state: Arc<Mutex<SchedulerState>>,
903 event_tx: Sender<SchedulerEvent>,
904 scheduler_handle: Mutex<Option<JoinHandle<()>>>,
905 worker_handles: Mutex<Vec<JoinHandle<()>>>,
906 config: EffectiveConfig,
907 nonrunnable_dispatches: Arc<AtomicUsize>,
908 completed_interactive: Arc<AtomicU64>,
909 completed_maintenance: Arc<AtomicU64>,
910}
911
912impl Drop for ExecutorInner {
913 fn drop(&mut self) {
914 let _ = self.event_tx.send(SchedulerEvent::Shutdown);
915
916 if let Some(handle) = self.scheduler_handle.lock().take() {
917 let _ = handle.join();
918 }
919
920 let mut workers = self.worker_handles.lock();
921 for handle in workers.drain(..) {
922 let _ = handle.join();
923 }
924 }
925}
926
927struct SchedulerState {
928 actors: HashMap<ProjectRootId, ActorState>,
929 actor_order: Vec<ProjectRootId>,
930 cursor: usize,
931 idle_workers: usize,
932 interactive_inflight: usize,
933 maintenance_inflight: usize,
934 config: EffectiveConfig,
935 running_jobs: HashMap<(ProjectRootId, String), RunningJob>,
936}
937
938impl SchedulerState {
939 fn new(config: EffectiveConfig) -> Self {
940 Self {
941 actors: HashMap::new(),
942 actor_order: Vec::new(),
943 cursor: 0,
944 idle_workers: config.pool_size,
945 interactive_inflight: 0,
946 maintenance_inflight: 0,
947 config,
948 running_jobs: HashMap::new(),
949 }
950 }
951
952 fn dispatch_liveness_snapshot(&self) -> DispatchLivenessSnapshot {
953 let now = Instant::now();
954 let mut interactive = QueueSnapshotAccumulator::default();
955 let mut maintenance = QueueSnapshotAccumulator::default();
956 for actor in self.actors.values() {
957 interactive.add(actor.class_queues(JobClass::Interactive), now);
958 maintenance.add(actor.class_queues(JobClass::Maintenance), now);
959 }
960
961 DispatchLivenessSnapshot {
962 interactive: interactive.finish(),
963 maintenance: maintenance.finish(),
964 running: DispatchRunningSnapshot {
965 interactive: self.interactive_inflight,
966 maintenance: self.maintenance_inflight,
967 },
968 interactive_reserve: self.config.interactive_reserve,
969 maintenance_cap: self.config.maintenance_cap,
970 }
971 }
972
973 fn mutating_lane_snapshots(&self) -> Vec<MutatingLaneSnapshot> {
974 let now = Instant::now();
975 let mut snapshots: Vec<_> = self
976 .actors
977 .iter()
978 .filter_map(|(root_id, actor)| {
979 actor
980 .mutating_inflight
981 .as_ref()
982 .map(|job| MutatingLaneSnapshot {
983 root_id: root_id.clone(),
984 request_id: job.request_id.clone(),
985 command: job.command.clone(),
986 started_age_ms: duration_millis_u64(
987 now.saturating_duration_since(job.started_at),
988 ),
989 })
990 })
991 .collect();
992 snapshots.sort_by(|left, right| left.root_id.as_path().cmp(right.root_id.as_path()));
993 snapshots
994 }
995
996 fn mutating_job_state_label(&self, root_id: &ProjectRootId, request_id: &str) -> &'static str {
997 let Some(actor) = self.actors.get(root_id) else {
998 return "actor_missing";
999 };
1000 if actor
1001 .mutating_inflight
1002 .as_ref()
1003 .is_some_and(|job| job.request_id == request_id)
1004 {
1005 return "running";
1006 }
1007 if actor.has_queued_mutating_job(request_id) {
1008 return "queued";
1009 }
1010 if actor.writer_inflight {
1011 return "blocked_by_other_mutating";
1012 }
1013 "not_found"
1014 }
1015
1016 fn bind_blocker_snapshot(
1017 &self,
1018 root_id: &ProjectRootId,
1019 request_id: &str,
1020 ) -> BindBlockerSnapshot {
1021 let configure_state = self.mutating_job_state_label(root_id, request_id);
1022 let mut blockers = Vec::new();
1023
1024 if let Some(actor) = self.actors.get(root_id) {
1025 if configure_state == "queued" {
1026 let configure_count = actor.pending_configure_count();
1027 if configure_count > 0 {
1028 blockers.push(format!("queued_behind_configure({configure_count})"));
1029 }
1030 if actor.read_inflight > 0 || actor.lsp_inflight {
1031 blockers.push("waiting_on_readers".to_string());
1032 }
1033 }
1034 }
1035
1036 if configure_state == "queued" {
1037 let maintenance: Vec<_> = self
1038 .running_jobs
1039 .values()
1040 .filter(|job| job.job_class == JobClass::Maintenance)
1041 .collect();
1042 if !maintenance.is_empty() {
1043 blockers.push(format!(
1044 "queued_behind_maintenance({})",
1045 format_running_jobs(&maintenance)
1046 ));
1047 }
1048 }
1049
1050 if self.idle_workers == 0 {
1051 let running: Vec<_> = self.running_jobs.values().collect();
1052 blockers.push(format!(
1053 "idle_workers==0({})",
1054 format_running_jobs(&running)
1055 ));
1056 }
1057
1058 BindBlockerSnapshot {
1059 configure_state,
1060 configure_phase_timings: self
1061 .actors
1062 .get(root_id)
1063 .map(|actor| actor.ctx.configure_ack_phase_snapshot()),
1064 blockers,
1065 }
1066 }
1067}
1068
1069fn is_configure_request(request_id: &str) -> bool {
1070 request_id.starts_with("subc-bind-")
1071}
1072
1073fn format_running_jobs(jobs: &[&RunningJob]) -> String {
1074 let now = Instant::now();
1075 let mut labels: Vec<_> = jobs
1076 .iter()
1077 .map(|job| {
1078 format!(
1079 "job={} command={} lane={:?} root={} age_ms={}",
1080 job.request_id,
1081 job.command,
1082 job.lane,
1083 job.root_id.as_path().display(),
1084 duration_millis_u64(now.saturating_duration_since(job.started_at))
1085 )
1086 })
1087 .collect();
1088 labels.sort();
1089 labels.truncate(4);
1090 labels.join("; ")
1091}
1092
1093#[derive(Default)]
1094struct QueueSnapshotAccumulator {
1095 queued: usize,
1096 oldest_age_ms: Option<u64>,
1097}
1098
1099impl QueueSnapshotAccumulator {
1100 fn add(&mut self, queues: &ClassQueues, now: Instant) {
1101 self.queued += queues.queued_count();
1102 if let Some(queued_at) = queues.oldest_queued_at() {
1103 let age_ms = duration_millis_u64(now.saturating_duration_since(queued_at));
1104 self.oldest_age_ms = Some(
1105 self.oldest_age_ms
1106 .map_or(age_ms, |oldest| oldest.max(age_ms)),
1107 );
1108 }
1109 }
1110
1111 fn finish(self) -> DispatchClassQueueSnapshot {
1112 DispatchClassQueueSnapshot {
1113 queued: self.queued,
1114 oldest_age_ms: self.oldest_age_ms,
1115 }
1116 }
1117}
1118
1119fn duration_millis_u64(duration: Duration) -> u64 {
1120 duration.as_millis().min(u128::from(u64::MAX)) as u64
1121}
1122
1123struct ActorState {
1124 ctx: Arc<AppContext>,
1125 epoch: Arc<RwLock<()>>,
1126 read_inflight: usize,
1127 lsp_inflight: bool,
1128 actor_total_inflight: usize,
1129 writer_inflight: bool,
1130 maintenance_commit_inflight: bool,
1131 mutating_inflight: Option<RunningMutatingJob>,
1132 deficit: isize,
1133 interactive: ClassQueues,
1134 maintenance: ClassQueues,
1135 fatal: bool,
1136}
1137
1138impl ActorState {
1139 fn new(ctx: Arc<AppContext>) -> Self {
1140 Self {
1141 ctx,
1142 epoch: Arc::new(RwLock::new(())),
1143 read_inflight: 0,
1144 lsp_inflight: false,
1145 actor_total_inflight: 0,
1146 writer_inflight: false,
1147 maintenance_commit_inflight: false,
1148 mutating_inflight: None,
1149 deficit: 0,
1150 interactive: ClassQueues::new(),
1151 maintenance: ClassQueues::new(),
1152 fatal: false,
1153 }
1154 }
1155
1156 fn push_job(&mut self, job_class: JobClass, lane: Lane, job: QueuedJob) {
1157 self.class_queues_mut(job_class).push_job(lane, job);
1158 }
1159
1160 fn has_queued_jobs(&self) -> bool {
1161 self.interactive.has_queued_jobs() || self.maintenance.has_queued_jobs()
1162 }
1163
1164 fn is_idle(&self) -> bool {
1165 self.actor_total_inflight == 0 && !self.has_queued_jobs()
1166 }
1167
1168 fn has_queued_jobs_for(&self, job_class: JobClass) -> bool {
1169 self.class_queues(job_class).has_queued_jobs()
1170 }
1171
1172 fn front_lane(&self, job_class: JobClass) -> Option<Lane> {
1173 self.class_queues(job_class).front_lane()
1174 }
1175
1176 fn pop_front_job(&mut self, job_class: JobClass, lane: Lane) -> Option<QueuedJob> {
1177 self.class_queues_mut(job_class).pop_front_job(lane)
1178 }
1179
1180 fn higher_priority_writer_barrier_blocks(&self, job_class: JobClass) -> bool {
1181 matches!(job_class, JobClass::Maintenance)
1185 && !self.interactive.queue(Lane::Mutating).is_empty()
1186 }
1187
1188 fn class_queues(&self, job_class: JobClass) -> &ClassQueues {
1189 match job_class {
1190 JobClass::Interactive => &self.interactive,
1191 JobClass::Maintenance => &self.maintenance,
1192 }
1193 }
1194
1195 fn class_queues_mut(&mut self, job_class: JobClass) -> &mut ClassQueues {
1196 match job_class {
1197 JobClass::Interactive => &mut self.interactive,
1198 JobClass::Maintenance => &mut self.maintenance,
1199 }
1200 }
1201
1202 fn fail_queued_jobs(&mut self) {
1203 self.interactive.fail_queued_jobs();
1204 self.maintenance.fail_queued_jobs();
1205 }
1206
1207 fn has_queued_mutating_job(&self, request_id: &str) -> bool {
1208 self.interactive.has_queued_mutating_job(request_id)
1209 || self.maintenance.has_queued_mutating_job(request_id)
1210 }
1211
1212 fn remove_queued_cancellable(&mut self, token: &JobCancellation) -> Option<QueuedJob> {
1213 self.interactive
1214 .remove_cancellable(token)
1215 .or_else(|| self.maintenance.remove_cancellable(token))
1216 }
1217
1218 fn pending_configure_count(&self) -> usize {
1219 usize::from(
1220 self.mutating_inflight
1221 .as_ref()
1222 .is_some_and(|job| is_configure_request(&job.request_id)),
1223 ) + self.interactive.queued_configure_count()
1224 + self.maintenance.queued_configure_count()
1225 }
1226}
1227
1228struct ClassQueues {
1229 order: VecDeque<Lane>,
1230 pure_reads: VecDeque<QueuedJob>,
1231 lsp_status: VecDeque<QueuedJob>,
1232 heavy_init: VecDeque<QueuedJob>,
1233 mutating: VecDeque<QueuedJob>,
1234 maintenance_commit: VecDeque<QueuedJob>,
1235}
1236
1237impl ClassQueues {
1238 fn new() -> Self {
1239 Self {
1240 order: VecDeque::new(),
1241 pure_reads: VecDeque::new(),
1242 lsp_status: VecDeque::new(),
1243 heavy_init: VecDeque::new(),
1244 mutating: VecDeque::new(),
1245 maintenance_commit: VecDeque::new(),
1246 }
1247 }
1248
1249 fn push_job(&mut self, lane: Lane, job: QueuedJob) {
1250 self.order.push_back(lane);
1251 self.queue_mut(lane).push_back(job);
1252 }
1253
1254 fn has_queued_jobs(&self) -> bool {
1255 !self.order.is_empty()
1256 }
1257
1258 fn front_lane(&self) -> Option<Lane> {
1259 self.order.front().copied()
1260 }
1261
1262 fn next_interactive_lane(&self, now: Instant) -> Option<Lane> {
1268 let starved_writer = self.mutating.iter().any(|job| {
1269 now.saturating_duration_since(job.queued_at) >= INTERACTIVE_WRITER_PROMOTION_AGE
1270 });
1271 if starved_writer {
1272 return Some(Lane::Mutating);
1275 }
1276 if !self.pure_reads.is_empty() {
1277 return Some(Lane::PureRead);
1278 }
1279 self.order
1280 .iter()
1281 .copied()
1282 .find(|lane| *lane != Lane::PureRead)
1283 }
1284
1285 fn pop_front_job(&mut self, lane: Lane) -> Option<QueuedJob> {
1286 let position = self.order.iter().position(|queued| *queued == lane)?;
1290 self.order.remove(position);
1291 self.queue_mut(lane).pop_front()
1292 }
1293
1294 fn queued_count(&self) -> usize {
1295 self.order.len()
1296 }
1297
1298 fn oldest_queued_at(&self) -> Option<Instant> {
1299 self.front_lane()
1300 .and_then(|lane| self.queue(lane).front().map(|job| job.queued_at))
1301 }
1302
1303 fn fail_queued_jobs(&mut self) {
1304 self.order.clear();
1305 fail_queued_job_queue(&mut self.pure_reads);
1306 fail_queued_job_queue(&mut self.lsp_status);
1307 fail_queued_job_queue(&mut self.heavy_init);
1308 fail_queued_job_queue(&mut self.mutating);
1309 fail_queued_job_queue(&mut self.maintenance_commit);
1310 }
1311
1312 fn cancel_queued_jobs(&mut self) -> usize {
1313 self.order.clear();
1314 cancel_queued_job_queue(&mut self.pure_reads)
1315 + cancel_queued_job_queue(&mut self.lsp_status)
1316 + cancel_queued_job_queue(&mut self.heavy_init)
1317 + cancel_queued_job_queue(&mut self.mutating)
1318 + cancel_queued_job_queue(&mut self.maintenance_commit)
1319 }
1320
1321 fn has_queued_mutating_job(&self, request_id: &str) -> bool {
1322 self.mutating.iter().any(|job| job.request_id == request_id)
1323 }
1324
1325 fn remove_cancellable(&mut self, token: &JobCancellation) -> Option<QueuedJob> {
1334 for lane in [
1335 Lane::PureRead,
1336 Lane::SerialLspStatus,
1337 Lane::HeavyInit,
1338 Lane::Mutating,
1339 Lane::MaintenanceCommit,
1340 ] {
1341 let queue = self.queue_mut(lane);
1342 let position = queue.iter().position(|queued| {
1343 queued
1344 .cancellation
1345 .as_ref()
1346 .is_some_and(|candidate| candidate.same_token(token))
1347 });
1348 if let Some(position) = position {
1349 let removed = queue.remove(position);
1350 let mut occurrence = 0usize;
1351 if let Some(order_position) = self.order.iter().position(|entry| {
1352 if *entry != lane {
1353 return false;
1354 }
1355 let matched = occurrence == position;
1356 occurrence += 1;
1357 matched
1358 }) {
1359 self.order.remove(order_position);
1360 }
1361 return removed;
1362 }
1363 }
1364 None
1365 }
1366
1367 fn queued_configure_count(&self) -> usize {
1368 self.mutating
1369 .iter()
1370 .filter(|job| is_configure_request(&job.request_id))
1371 .count()
1372 }
1373
1374 fn queue(&self, lane: Lane) -> &VecDeque<QueuedJob> {
1375 match lane {
1376 Lane::PureRead => &self.pure_reads,
1377 Lane::SerialLspStatus => &self.lsp_status,
1378 Lane::HeavyInit => &self.heavy_init,
1379 Lane::Mutating => &self.mutating,
1380 Lane::MaintenanceCommit => &self.maintenance_commit,
1381 }
1382 }
1383
1384 fn queue_mut(&mut self, lane: Lane) -> &mut VecDeque<QueuedJob> {
1385 match lane {
1386 Lane::PureRead => &mut self.pure_reads,
1387 Lane::SerialLspStatus => &mut self.lsp_status,
1388 Lane::HeavyInit => &mut self.heavy_init,
1389 Lane::Mutating => &mut self.mutating,
1390 Lane::MaintenanceCommit => &mut self.maintenance_commit,
1391 }
1392 }
1393}
1394
1395struct QueuedJob {
1396 job: ExecutorJob,
1397 completion: CompletionSender,
1398 request_id: String,
1399 command: String,
1400 queued_at: Instant,
1401 cancellation: Option<JobCancellation>,
1402}
1403
1404fn fail_queued_job_queue(queue: &mut VecDeque<QueuedJob>) {
1405 for queued in queue.drain(..) {
1406 queued
1407 .completion
1408 .send(actor_fatal_response(queued.request_id));
1409 }
1410}
1411
1412fn cancel_queued_job_queue(queue: &mut VecDeque<QueuedJob>) -> usize {
1413 let cancelled = queue.len();
1414 for queued in queue.drain(..) {
1415 queued.completion.send(Response::error(
1416 queued.request_id,
1417 "maintenance_cancelled",
1418 "maintenance cancelled because the actor has no bound routes",
1419 ));
1420 }
1421 cancelled
1422}
1423
1424fn job_command(job_class: JobClass, lane: Lane) -> String {
1425 format!("executor::{job_class:?}::{lane:?}")
1426}
1427
1428fn actor_fatal_response(request_id: impl Into<String>) -> Response {
1429 Response::error(
1430 request_id,
1431 "actor_fatal",
1432 "executor actor is fatal after a mutating job panic",
1433 )
1434}
1435
1436fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> String {
1437 if let Some(message) = payload.downcast_ref::<&'static str>() {
1438 (*message).to_string()
1439 } else if let Some(message) = payload.downcast_ref::<String>() {
1440 message.clone()
1441 } else {
1442 "unknown panic payload".to_string()
1443 }
1444}
1445
1446fn panic_response(
1447 request_id: impl Into<String>,
1448 command: &str,
1449 payload: &(dyn std::any::Any + Send),
1450) -> Response {
1451 let panic_message = panic_payload_message(payload);
1452 Response::error(
1453 request_id,
1454 "actor_fatal",
1455 format!("command '{command}' panicked: {panic_message}"),
1456 )
1457}
1458
1459enum CompletionSender {
1460 Sync(Sender<Response>),
1461 Async(oneshot::Sender<Response>),
1462}
1463
1464impl CompletionSender {
1465 fn send(self, response: Response) {
1466 match self {
1467 Self::Sync(tx) => {
1468 let _ = tx.send(response);
1469 }
1470 Self::Async(tx) => {
1471 let _ = tx.send(response);
1472 }
1473 }
1474 }
1475}
1476
1477struct RunJob {
1478 root_id: ProjectRootId,
1479 job_class: JobClass,
1480 lane: Lane,
1481 ctx: Arc<AppContext>,
1482 epoch: Arc<RwLock<()>>,
1483 job: ExecutorJob,
1484 completion: Option<CompletionSender>,
1485 request_id: String,
1486 command: String,
1487 heavy_permit: Option<HeavyPermit>,
1488 cancellation: Option<JobCancellation>,
1489}
1490
1491struct CompletionEvent {
1492 root_id: ProjectRootId,
1493 request_id: String,
1494 job_class: JobClass,
1495 lane: Lane,
1496 heavy_permit: Option<HeavyPermit>,
1497 panicked: bool,
1498}
1499
1500enum SchedulerEvent {
1501 Wake,
1502 Completed(CompletionEvent),
1503 Shutdown,
1504}
1505
1506fn scheduler_loop(
1507 state: Arc<Mutex<SchedulerState>>,
1508 heavy: Arc<HeavySemaphore>,
1509 run_tx: Sender<RunJob>,
1510 event_rx: Receiver<SchedulerEvent>,
1511 nonrunnable_dispatches: Arc<AtomicUsize>,
1512 completed_interactive: Arc<AtomicU64>,
1513 completed_maintenance: Arc<AtomicU64>,
1514) {
1515 while let Ok(event) = event_rx.recv() {
1516 let mut shutdown = false;
1517 {
1518 let mut state = state.lock();
1519 note_completion_event(&event, &completed_interactive, &completed_maintenance);
1520 shutdown |= process_scheduler_event(event, &mut state);
1521 while !shutdown {
1522 match event_rx.try_recv() {
1523 Ok(event) => {
1524 note_completion_event(
1525 &event,
1526 &completed_interactive,
1527 &completed_maintenance,
1528 );
1529 shutdown |= process_scheduler_event(event, &mut state)
1530 }
1531 Err(_) => break,
1532 }
1533 }
1534
1535 if !shutdown {
1536 dispatch_runnable(&mut state, &heavy, &run_tx, &nonrunnable_dispatches);
1537 }
1538 }
1539
1540 if shutdown {
1541 break;
1542 }
1543 }
1544}
1545
1546fn note_completion_event(
1547 event: &SchedulerEvent,
1548 completed_interactive: &AtomicU64,
1549 completed_maintenance: &AtomicU64,
1550) {
1551 let SchedulerEvent::Completed(event) = event else {
1552 return;
1553 };
1554 match event.job_class {
1555 JobClass::Interactive => {
1556 completed_interactive.fetch_add(1, Ordering::Relaxed);
1557 }
1558 JobClass::Maintenance => {
1559 completed_maintenance.fetch_add(1, Ordering::Relaxed);
1560 }
1561 }
1562}
1563
1564fn process_scheduler_event(event: SchedulerEvent, state: &mut SchedulerState) -> bool {
1565 match event {
1566 SchedulerEvent::Wake => false,
1567 SchedulerEvent::Completed(event) => {
1568 complete_job(state, event);
1569 false
1570 }
1571 SchedulerEvent::Shutdown => true,
1572 }
1573}
1574
1575fn complete_job(state: &mut SchedulerState, event: CompletionEvent) {
1576 let CompletionEvent {
1577 root_id,
1578 request_id,
1579 job_class,
1580 lane,
1581 heavy_permit,
1582 panicked,
1583 } = event;
1584 state.running_jobs.remove(&(root_id.clone(), request_id));
1585
1586 match job_class {
1587 JobClass::Interactive => {
1588 state.interactive_inflight = state.interactive_inflight.saturating_sub(1);
1589 }
1590 JobClass::Maintenance => {
1591 state.maintenance_inflight = state.maintenance_inflight.saturating_sub(1);
1592 }
1593 }
1594
1595 if let Some(actor) = state.actors.get_mut(&root_id) {
1596 actor.actor_total_inflight = actor.actor_total_inflight.saturating_sub(1);
1597 match lane {
1598 Lane::PureRead => {
1599 actor.read_inflight = actor.read_inflight.saturating_sub(1);
1600 }
1601 Lane::SerialLspStatus => {
1602 actor.lsp_inflight = false;
1603 }
1604 Lane::HeavyInit => {}
1605 Lane::Mutating => {
1606 actor.writer_inflight = false;
1607 actor.mutating_inflight = None;
1608 }
1609 Lane::MaintenanceCommit => {
1610 actor.maintenance_commit_inflight = false;
1611 }
1612 }
1613
1614 if panicked && lane == Lane::Mutating {
1615 actor.fatal = true;
1616 actor.fail_queued_jobs();
1617 }
1618 }
1619
1620 drop(heavy_permit);
1621 state.idle_workers += 1;
1622}
1623
1624fn dispatch_runnable(
1625 state: &mut SchedulerState,
1626 heavy: &Arc<HeavySemaphore>,
1627 run_tx: &Sender<RunJob>,
1628 nonrunnable_dispatches: &AtomicUsize,
1629) {
1630 while state.idle_workers > 0 && !state.actor_order.is_empty() {
1631 let mut made_progress = false;
1632 let mut dispatch_failed = false;
1633
1634 made_progress |= dispatch_runnable_class(
1635 state,
1636 JobClass::Interactive,
1637 heavy,
1638 run_tx,
1639 nonrunnable_dispatches,
1640 &mut dispatch_failed,
1641 );
1642 if dispatch_failed || state.idle_workers == 0 {
1643 return;
1644 }
1645
1646 if can_dispatch_class(state, JobClass::Maintenance) {
1647 made_progress |= dispatch_runnable_class(
1648 state,
1649 JobClass::Maintenance,
1650 heavy,
1651 run_tx,
1652 nonrunnable_dispatches,
1653 &mut dispatch_failed,
1654 );
1655 if dispatch_failed {
1656 return;
1657 }
1658 }
1659
1660 if !made_progress {
1661 break;
1662 }
1663 }
1664}
1665
1666fn dispatch_runnable_class(
1667 state: &mut SchedulerState,
1668 job_class: JobClass,
1669 heavy: &Arc<HeavySemaphore>,
1670 run_tx: &Sender<RunJob>,
1671 nonrunnable_dispatches: &AtomicUsize,
1672 dispatch_failed: &mut bool,
1673) -> bool {
1674 if !can_dispatch_class(state, job_class) || state.actor_order.is_empty() {
1675 return false;
1676 }
1677
1678 let actor_count = state.actor_order.len();
1679 let mut made_progress = false;
1680
1681 for _ in 0..actor_count {
1682 if !can_dispatch_class(state, job_class) || state.actor_order.is_empty() {
1683 break;
1684 }
1685
1686 if state.cursor >= state.actor_order.len() {
1687 state.cursor = 0;
1688 }
1689 let root_id = state.actor_order[state.cursor].clone();
1690 state.cursor = (state.cursor + 1) % state.actor_order.len();
1691
1692 let run_job = {
1693 let Some(actor) = state.actors.get_mut(&root_id) else {
1694 continue;
1695 };
1696
1697 if actor.fatal {
1698 actor.fail_queued_jobs();
1699 actor.deficit = 0;
1700 continue;
1701 }
1702
1703 if !actor.has_queued_jobs() {
1704 actor.deficit = 0;
1705 continue;
1706 }
1707
1708 if !actor.has_queued_jobs_for(job_class) {
1709 continue;
1710 }
1711
1712 actor.deficit =
1713 (actor.deficit + state.config.drr_quantum).min(state.config.deficit_cap);
1714 if actor.deficit < JOB_COST {
1715 continue;
1716 }
1717
1718 try_admit_actor(&root_id, actor, job_class, &state.config, heavy)
1719 };
1720
1721 if let Some(run_job) = run_job {
1722 state.running_jobs.insert(
1723 (run_job.root_id.clone(), run_job.request_id.clone()),
1724 RunningJob {
1725 root_id: run_job.root_id.clone(),
1726 request_id: run_job.request_id.clone(),
1727 command: run_job.command.clone(),
1728 job_class: run_job.job_class,
1729 lane: run_job.lane,
1730 started_at: Instant::now(),
1731 },
1732 );
1733 state.idle_workers -= 1;
1734 match job_class {
1735 JobClass::Interactive => state.interactive_inflight += 1,
1736 JobClass::Maintenance => state.maintenance_inflight += 1,
1737 }
1738 made_progress = true;
1739 if run_tx.send(run_job).is_err() {
1740 nonrunnable_dispatches.fetch_add(1, Ordering::AcqRel);
1741 *dispatch_failed = true;
1742 return made_progress;
1743 }
1744 }
1745 }
1746
1747 made_progress
1748}
1749
1750fn can_dispatch_class(state: &SchedulerState, job_class: JobClass) -> bool {
1751 if state.idle_workers == 0 {
1752 return false;
1753 }
1754 match job_class {
1755 JobClass::Interactive => true,
1756 JobClass::Maintenance => {
1757 state.maintenance_inflight < state.config.maintenance_cap
1758 && state.idle_workers > state.config.interactive_reserve
1759 }
1760 }
1761}
1762
1763fn try_admit_actor(
1764 root_id: &ProjectRootId,
1765 actor: &mut ActorState,
1766 job_class: JobClass,
1767 config: &EffectiveConfig,
1768 heavy: &Arc<HeavySemaphore>,
1769) -> Option<RunJob> {
1770 let lane = match job_class {
1771 JobClass::Interactive => actor
1772 .class_queues(JobClass::Interactive)
1773 .next_interactive_lane(Instant::now())?,
1774 JobClass::Maintenance => actor.front_lane(job_class)?,
1775 };
1776 let mut heavy_permit = None;
1777
1778 if actor.writer_inflight || actor.higher_priority_writer_barrier_blocks(job_class) {
1779 return None;
1780 }
1781
1782 let has_epoch_reader =
1783 actor.read_inflight > 0 || actor.lsp_inflight || actor.maintenance_commit_inflight;
1784 let actor_has_capacity = actor.actor_total_inflight < config.actor_cap;
1785 let runnable = match lane {
1786 Lane::PureRead => actor.read_inflight < config.read_cap && actor_has_capacity,
1787 Lane::SerialLspStatus => !actor.lsp_inflight && actor_has_capacity,
1788 Lane::HeavyInit => {
1789 if !actor_has_capacity {
1790 false
1791 } else if let Some(permit) = heavy.try_acquire() {
1792 heavy_permit = Some(permit);
1793 true
1794 } else {
1795 false
1796 }
1797 }
1798 Lane::Mutating => !has_epoch_reader && actor_has_capacity,
1799 Lane::MaintenanceCommit => !actor.maintenance_commit_inflight && actor_has_capacity,
1802 };
1803
1804 if !runnable {
1805 return None;
1806 }
1807
1808 let queued = actor.pop_front_job(job_class, lane)?;
1809 actor.deficit -= JOB_COST;
1810 if let Some(cancellation) = queued.cancellation.as_ref() {
1811 cancellation.mark_running();
1812 }
1813 if lane == Lane::Mutating {
1814 actor.mutating_inflight = Some(RunningMutatingJob {
1815 request_id: queued.request_id.clone(),
1816 command: queued.command.clone(),
1817 started_at: Instant::now(),
1818 });
1819 }
1820 match lane {
1821 Lane::PureRead => {
1822 actor.read_inflight += 1;
1823 actor.actor_total_inflight += 1;
1824 }
1825 Lane::SerialLspStatus => {
1826 actor.lsp_inflight = true;
1827 actor.actor_total_inflight += 1;
1828 }
1829 Lane::HeavyInit => {
1830 actor.actor_total_inflight += 1;
1831 }
1832 Lane::Mutating => {
1833 actor.writer_inflight = true;
1834 actor.actor_total_inflight += 1;
1835 }
1836 Lane::MaintenanceCommit => {
1837 actor.maintenance_commit_inflight = true;
1838 actor.actor_total_inflight += 1;
1839 }
1840 }
1841
1842 Some(RunJob {
1843 root_id: root_id.clone(),
1844 job_class,
1845 lane,
1846 ctx: Arc::clone(&actor.ctx),
1847 epoch: Arc::clone(&actor.epoch),
1848 job: queued.job,
1849 completion: Some(queued.completion),
1850 request_id: queued.request_id,
1851 command: queued.command,
1852 heavy_permit,
1853 cancellation: queued.cancellation,
1854 })
1855}
1856
1857fn worker_loop(run_rx: Receiver<RunJob>, event_tx: Sender<SchedulerEvent>) {
1858 while let Ok(mut run_job) = run_rx.recv() {
1859 let response =
1860 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run_lane_job(&mut run_job)));
1861 let panicked = response.is_err();
1862 let response = match response {
1863 Ok(response) => response,
1864 Err(payload) => panic_response(
1865 run_job.request_id.clone(),
1866 &run_job.command,
1867 payload.as_ref(),
1868 ),
1869 };
1870
1871 if let Some(completion) = run_job.completion.take() {
1872 completion.send(response);
1873 }
1874 let completion = CompletionEvent {
1875 root_id: run_job.root_id,
1876 request_id: run_job.request_id,
1877 job_class: run_job.job_class,
1878 lane: run_job.lane,
1879 heavy_permit: run_job.heavy_permit.take(),
1880 panicked,
1881 };
1882 let _ = event_tx.send(SchedulerEvent::Completed(completion));
1883 }
1884}
1885
1886fn run_lane_job(run_job: &mut RunJob) -> Response {
1887 let _cancellation_ctx = CurrentJobCancellationGuard::install(run_job.cancellation.clone());
1888 let missing_request_id = run_job.request_id.clone();
1889 let job = std::mem::replace(
1890 &mut run_job.job,
1891 Box::new(move |_| {
1892 Response::error(
1893 missing_request_id,
1894 "job_missing",
1895 "executor job already taken",
1896 )
1897 }),
1898 );
1899
1900 match run_job.lane {
1901 Lane::PureRead | Lane::SerialLspStatus => {
1902 let _epoch = run_job.epoch.read();
1903 job(&run_job.ctx)
1904 }
1905 Lane::HeavyInit => {
1906 let response = job(&run_job.ctx);
1907 {
1908 let _install = run_job.epoch.write();
1909 }
1910 response
1911 }
1912 Lane::Mutating => {
1913 let _epoch = run_job.epoch.write();
1914 job(&run_job.ctx)
1915 }
1916 Lane::MaintenanceCommit => {
1917 let _epoch = run_job.epoch.read();
1921 job(&run_job.ctx)
1922 }
1923 }
1924}
1925
1926#[derive(Debug)]
1927struct HeavySemaphore {
1928 available: AtomicUsize,
1929 max: usize,
1930}
1931
1932impl HeavySemaphore {
1933 fn new(permits: usize) -> Self {
1934 Self {
1935 available: AtomicUsize::new(permits),
1936 max: permits,
1937 }
1938 }
1939
1940 fn try_acquire(self: &Arc<Self>) -> Option<HeavyPermit> {
1941 loop {
1942 let available = self.available.load(Ordering::Acquire);
1943 if available == 0 {
1944 return None;
1945 }
1946 if self
1947 .available
1948 .compare_exchange(
1949 available,
1950 available - 1,
1951 Ordering::AcqRel,
1952 Ordering::Acquire,
1953 )
1954 .is_ok()
1955 {
1956 return Some(HeavyPermit {
1957 semaphore: Arc::clone(self),
1958 });
1959 }
1960 }
1961 }
1962}
1963
1964struct HeavyPermit {
1965 semaphore: Arc<HeavySemaphore>,
1966}
1967
1968impl Drop for HeavyPermit {
1969 fn drop(&mut self) {
1970 let previous = self.semaphore.available.fetch_add(1, Ordering::Release);
1971 debug_assert!(previous < self.semaphore.max);
1972 }
1973}