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 crate::root_cache::register_live_scope(&ctx.storage_dir(), &memory_root);
463 app.register_memory_context(memory_root, &ctx);
464 app.actor_root_registered();
465 }
466 self.wake_scheduler();
467 inserted
468 }
469
470 pub fn remove_actor(&self, root_id: &ProjectRootId) {
478 let removed = {
479 let mut state = self.inner.state.lock();
480 state.actor_order.retain(|actor_root| actor_root != root_id);
481 state.actors.remove(root_id)
482 };
483 if let Some(actor) = removed.as_ref() {
484 let app = actor.ctx.app();
485 crate::root_cache::unregister_live_scope(&actor.ctx.storage_dir(), root_id.as_path());
486 app.unregister_memory_context(root_id.as_path(), &actor.ctx);
487 app.actor_root_unregistered();
488 }
489 drop(removed);
490 self.wake_scheduler();
491 }
492
493 pub fn actor_is_idle(&self, root_id: &ProjectRootId) -> bool {
495 let state = self.inner.state.lock();
496 state.actors.get(root_id).is_some_and(ActorState::is_idle)
497 }
498
499 pub fn try_actor_is_idle(&self, root_id: &ProjectRootId) -> Option<bool> {
503 let state = self.inner.state.try_lock()?;
504 Some(state.actors.get(root_id).is_some_and(ActorState::is_idle))
505 }
506
507 pub fn retire_idle_actor_in_background(&self, root_id: &ProjectRootId) -> bool {
511 let removed = {
512 let mut state = self.inner.state.lock();
513 if !state.actors.get(root_id).is_some_and(ActorState::is_idle) {
514 return false;
515 }
516 state.actor_order.retain(|actor_root| actor_root != root_id);
517 state.actors.remove(root_id)
518 };
519 let Some(actor) = removed else {
520 return false;
521 };
522 let app = actor.ctx.app();
523 crate::root_cache::unregister_live_scope(&actor.ctx.storage_dir(), root_id.as_path());
524 app.unregister_memory_context(root_id.as_path(), &actor.ctx);
525 app.actor_root_unregistered();
526 std::thread::spawn(move || {
527 actor.ctx.teardown_deleted_root();
528 drop(actor);
529 });
530 self.wake_scheduler();
531 true
532 }
533
534 pub fn cancel_queued_maintenance(&self, root_id: &ProjectRootId) -> usize {
540 let cancelled = {
541 let mut state = self.inner.state.lock();
542 state
543 .actors
544 .get_mut(root_id)
545 .map(|actor| actor.maintenance.cancel_queued_jobs())
546 .unwrap_or(0)
547 };
548 if cancelled > 0 {
549 self.wake_scheduler();
550 }
551 cancelled
552 }
553
554 pub fn actor_registered(&self, root_id: &ProjectRootId) -> bool {
556 let state = self.inner.state.lock();
557 state.actors.contains_key(root_id)
558 }
559
560 pub fn actor_context(&self, root_id: &ProjectRootId) -> Option<Arc<AppContext>> {
563 let state = self.inner.state.lock();
564 state
565 .actors
566 .get(root_id)
567 .map(|actor| Arc::clone(&actor.ctx))
568 }
569
570 pub fn actor_contexts(&self) -> Vec<Arc<AppContext>> {
575 let state = self.inner.state.lock();
576 state
577 .actors
578 .values()
579 .map(|actor_state| Arc::clone(&actor_state.ctx))
580 .collect()
581 }
582
583 pub fn actor_entries(&self) -> Vec<(ProjectRootId, Arc<AppContext>)> {
585 let state = self.inner.state.lock();
586 state
587 .actors
588 .iter()
589 .map(|(root_id, actor_state)| (root_id.clone(), Arc::clone(&actor_state.ctx)))
590 .collect()
591 }
592
593 pub fn try_actor_entries(&self) -> Option<Vec<(ProjectRootId, Arc<AppContext>)>> {
597 let state = self.inner.state.try_lock()?;
598 Some(
599 state
600 .actors
601 .iter()
602 .map(|(root_id, actor_state)| (root_id.clone(), Arc::clone(&actor_state.ctx)))
603 .collect(),
604 )
605 }
606
607 pub fn submit(
608 &self,
609 root_id: ProjectRootId,
610 lane: Lane,
611 request_id: String,
612 job: ExecutorJob,
613 ) -> CompletionHandle {
614 let (completion_tx, completion_rx) = crossbeam_channel::bounded(1);
615 self.submit_with_completion(
616 root_id,
617 JobClass::Interactive,
618 lane,
619 request_id,
620 job,
621 CompletionSender::Sync(completion_tx),
622 );
623 CompletionHandle { rx: completion_rx }
624 }
625
626 pub fn submit_async(
627 &self,
628 root_id: ProjectRootId,
629 lane: Lane,
630 request_id: String,
631 job: ExecutorJob,
632 ) -> oneshot::Receiver<Response> {
633 let (completion_tx, completion_rx) = oneshot::channel();
634 self.submit_with_completion(
635 root_id,
636 JobClass::Interactive,
637 lane,
638 request_id,
639 job,
640 CompletionSender::Async(completion_tx),
641 );
642 completion_rx
643 }
644
645 pub fn submit_cancellable_async(
651 &self,
652 root_id: ProjectRootId,
653 lane: Lane,
654 request_id: String,
655 job: ExecutorJob,
656 ) -> (oneshot::Receiver<Response>, JobCancellation) {
657 let cancellation = JobCancellation::new();
658 let (completion_tx, completion_rx) = oneshot::channel();
659 self.submit_with_completion_cancellable(
660 root_id,
661 JobClass::Interactive,
662 lane,
663 request_id,
664 job,
665 CompletionSender::Async(completion_tx),
666 Some(cancellation.clone()),
667 );
668 (completion_rx, cancellation)
669 }
670
671 pub fn cancel_job(&self, root_id: &ProjectRootId, token: &JobCancellation) -> JobCancelOutcome {
677 let observed = token.signal_cancel();
684 let (outcome, settled) = {
685 let mut state = self.inner.state.lock();
686 let Some(actor) = state.actors.get_mut(root_id) else {
687 return match observed {
688 JOB_CANCEL_STATE_COMMITTED => JobCancelOutcome::RunningCommitted,
689 JOB_CANCEL_STATE_RUNNING | JOB_CANCEL_STATE_PENDING => {
690 JobCancelOutcome::RunningSignalled
691 }
692 _ => JobCancelOutcome::NotFound,
693 };
694 };
695 match actor.remove_queued_cancellable(token) {
696 Some(queued) => (JobCancelOutcome::QueuedRemoved, Some(queued)),
697 None => match observed {
698 JOB_CANCEL_STATE_COMMITTED => (JobCancelOutcome::RunningCommitted, None),
700 JOB_CANCEL_STATE_RUNNING | JOB_CANCEL_STATE_PENDING => {
704 (JobCancelOutcome::RunningSignalled, None)
705 }
706 _ => (JobCancelOutcome::NotFound, None),
708 },
709 }
710 };
711 if let Some(queued) = settled {
712 queued.completion.send(Response::error(
713 queued.request_id,
714 "request_cancelled",
715 "request cancelled before execution",
716 ));
717 self.wake_scheduler();
718 }
719 outcome
720 }
721
722 pub fn submit_maintenance_async(
723 &self,
724 root_id: ProjectRootId,
725 lane: Lane,
726 request_id: String,
727 job: ExecutorJob,
728 ) -> oneshot::Receiver<Response> {
729 let (completion_tx, completion_rx) = oneshot::channel();
730 self.submit_with_completion(
731 root_id,
732 JobClass::Maintenance,
733 lane,
734 request_id,
735 job,
736 CompletionSender::Async(completion_tx),
737 );
738 completion_rx
739 }
740
741 fn submit_with_completion(
742 &self,
743 root_id: ProjectRootId,
744 job_class: JobClass,
745 lane: Lane,
746 request_id: String,
747 job: ExecutorJob,
748 completion: CompletionSender,
749 ) {
750 self.submit_with_completion_cancellable(
751 root_id, job_class, lane, request_id, job, completion, None,
752 );
753 }
754
755 #[allow(clippy::too_many_arguments)]
756 fn submit_with_completion_cancellable(
757 &self,
758 root_id: ProjectRootId,
759 job_class: JobClass,
760 lane: Lane,
761 request_id: String,
762 job: ExecutorJob,
763 completion: CompletionSender,
764 cancellation: Option<JobCancellation>,
765 ) {
766 let command = job_command(job_class, lane);
767 let mut job = Some(job);
768 let mut completion = Some(completion);
769
770 let response = {
771 let mut state = self.inner.state.lock();
772 match state.actors.get_mut(&root_id) {
773 Some(actor) if actor.fatal => Some(actor_fatal_response(request_id.clone())),
774 Some(actor) => {
775 actor.push_job(
776 job_class,
777 lane,
778 QueuedJob {
779 job: job.take().expect("executor job already queued"),
780 completion: completion
781 .take()
782 .expect("executor completion already queued"),
783 request_id: request_id.clone(),
784 command,
785 queued_at: Instant::now(),
786 cancellation: cancellation.clone(),
787 },
788 );
789 None
790 }
791 None => Some(Response::error(
792 request_id.clone(),
793 "actor_not_registered",
794 "executor actor is not registered",
795 )),
796 }
797 };
798
799 if let Some(response) = response {
800 if let Some(completion) = completion {
801 completion.send(response);
802 }
803 return;
804 }
805
806 self.wake_scheduler();
807 }
808
809 pub fn pool_size(&self) -> usize {
810 self.inner.config.pool_size
811 }
812
813 pub fn actor_cap(&self) -> usize {
814 self.inner.config.actor_cap
815 }
816
817 pub fn read_cap(&self) -> usize {
818 self.inner.config.read_cap
819 }
820
821 pub fn heavy_permits(&self) -> usize {
822 self.inner.config.heavy_permits
823 }
824
825 pub fn interactive_reserve(&self) -> usize {
826 self.inner.config.interactive_reserve
827 }
828
829 pub fn maintenance_cap(&self) -> usize {
830 self.inner.config.maintenance_cap
831 }
832
833 pub fn try_dispatch_liveness_snapshot(&self) -> Option<DispatchLivenessSnapshot> {
834 self.inner
835 .state
836 .try_lock()
837 .map(|state| state.dispatch_liveness_snapshot())
838 }
839
840 pub fn try_mutating_lane_snapshots(&self) -> Option<Vec<MutatingLaneSnapshot>> {
841 self.inner
842 .state
843 .try_lock()
844 .map(|state| state.mutating_lane_snapshots())
845 }
846
847 pub fn try_mutating_job_state_label(
848 &self,
849 root_id: &ProjectRootId,
850 request_id: &str,
851 ) -> Option<&'static str> {
852 self.inner
853 .state
854 .try_lock()
855 .map(|state| state.mutating_job_state_label(root_id, request_id))
856 }
857
858 pub fn try_bind_blocker_snapshot(
862 &self,
863 root_id: &ProjectRootId,
864 request_id: &str,
865 ) -> Option<BindBlockerSnapshot> {
866 self.inner
867 .state
868 .try_lock()
869 .map(|state| state.bind_blocker_snapshot(root_id, request_id))
870 }
871
872 pub fn nonrunnable_dispatch_count(&self) -> usize {
873 self.inner.nonrunnable_dispatches.load(Ordering::Acquire)
874 }
875
876 pub fn completion_counts(&self) -> (u64, u64) {
877 (
878 self.inner.completed_interactive.load(Ordering::Relaxed),
879 self.inner.completed_maintenance.load(Ordering::Relaxed),
880 )
881 }
882
883 pub fn actor_is_fatal(&self, root_id: &ProjectRootId) -> bool {
884 self.inner
885 .state
886 .lock()
887 .actors
888 .get(root_id)
889 .map(|actor| actor.fatal)
890 .unwrap_or(false)
891 }
892
893 fn wake_scheduler(&self) {
894 let _ = self.inner.event_tx.send(SchedulerEvent::Wake);
895 }
896}
897
898impl Default for Executor {
899 fn default() -> Self {
900 Self::new()
901 }
902}
903
904struct ExecutorInner {
905 state: Arc<Mutex<SchedulerState>>,
906 event_tx: Sender<SchedulerEvent>,
907 scheduler_handle: Mutex<Option<JoinHandle<()>>>,
908 worker_handles: Mutex<Vec<JoinHandle<()>>>,
909 config: EffectiveConfig,
910 nonrunnable_dispatches: Arc<AtomicUsize>,
911 completed_interactive: Arc<AtomicU64>,
912 completed_maintenance: Arc<AtomicU64>,
913}
914
915impl Drop for ExecutorInner {
916 fn drop(&mut self) {
917 let _ = self.event_tx.send(SchedulerEvent::Shutdown);
918
919 if let Some(handle) = self.scheduler_handle.lock().take() {
920 let _ = handle.join();
921 }
922
923 let mut workers = self.worker_handles.lock();
924 for handle in workers.drain(..) {
925 let _ = handle.join();
926 }
927 }
928}
929
930struct SchedulerState {
931 actors: HashMap<ProjectRootId, ActorState>,
932 actor_order: Vec<ProjectRootId>,
933 cursor: usize,
934 idle_workers: usize,
935 interactive_inflight: usize,
936 maintenance_inflight: usize,
937 config: EffectiveConfig,
938 running_jobs: HashMap<(ProjectRootId, String), RunningJob>,
939}
940
941impl SchedulerState {
942 fn new(config: EffectiveConfig) -> Self {
943 Self {
944 actors: HashMap::new(),
945 actor_order: Vec::new(),
946 cursor: 0,
947 idle_workers: config.pool_size,
948 interactive_inflight: 0,
949 maintenance_inflight: 0,
950 config,
951 running_jobs: HashMap::new(),
952 }
953 }
954
955 fn dispatch_liveness_snapshot(&self) -> DispatchLivenessSnapshot {
956 let now = Instant::now();
957 let mut interactive = QueueSnapshotAccumulator::default();
958 let mut maintenance = QueueSnapshotAccumulator::default();
959 for actor in self.actors.values() {
960 interactive.add(actor.class_queues(JobClass::Interactive), now);
961 maintenance.add(actor.class_queues(JobClass::Maintenance), now);
962 }
963
964 DispatchLivenessSnapshot {
965 interactive: interactive.finish(),
966 maintenance: maintenance.finish(),
967 running: DispatchRunningSnapshot {
968 interactive: self.interactive_inflight,
969 maintenance: self.maintenance_inflight,
970 },
971 interactive_reserve: self.config.interactive_reserve,
972 maintenance_cap: self.config.maintenance_cap,
973 }
974 }
975
976 fn mutating_lane_snapshots(&self) -> Vec<MutatingLaneSnapshot> {
977 let now = Instant::now();
978 let mut snapshots: Vec<_> = self
979 .actors
980 .iter()
981 .filter_map(|(root_id, actor)| {
982 actor
983 .mutating_inflight
984 .as_ref()
985 .map(|job| MutatingLaneSnapshot {
986 root_id: root_id.clone(),
987 request_id: job.request_id.clone(),
988 command: job.command.clone(),
989 started_age_ms: duration_millis_u64(
990 now.saturating_duration_since(job.started_at),
991 ),
992 })
993 })
994 .collect();
995 snapshots.sort_by(|left, right| left.root_id.as_path().cmp(right.root_id.as_path()));
996 snapshots
997 }
998
999 fn mutating_job_state_label(&self, root_id: &ProjectRootId, request_id: &str) -> &'static str {
1000 let Some(actor) = self.actors.get(root_id) else {
1001 return "actor_missing";
1002 };
1003 if actor
1004 .mutating_inflight
1005 .as_ref()
1006 .is_some_and(|job| job.request_id == request_id)
1007 {
1008 return "running";
1009 }
1010 if actor.has_queued_mutating_job(request_id) {
1011 return "queued";
1012 }
1013 if actor.writer_inflight {
1014 return "blocked_by_other_mutating";
1015 }
1016 "not_found"
1017 }
1018
1019 fn bind_blocker_snapshot(
1020 &self,
1021 root_id: &ProjectRootId,
1022 request_id: &str,
1023 ) -> BindBlockerSnapshot {
1024 let configure_state = self.mutating_job_state_label(root_id, request_id);
1025 let mut blockers = Vec::new();
1026
1027 if let Some(actor) = self.actors.get(root_id) {
1028 if configure_state == "queued" {
1029 let configure_count = actor.pending_configure_count();
1030 if configure_count > 0 {
1031 blockers.push(format!("queued_behind_configure({configure_count})"));
1032 }
1033 if actor.read_inflight > 0 || actor.lsp_inflight {
1034 blockers.push("waiting_on_readers".to_string());
1035 }
1036 }
1037 }
1038
1039 if configure_state == "queued" {
1040 let maintenance: Vec<_> = self
1041 .running_jobs
1042 .values()
1043 .filter(|job| job.job_class == JobClass::Maintenance)
1044 .collect();
1045 if !maintenance.is_empty() {
1046 blockers.push(format!(
1047 "queued_behind_maintenance({})",
1048 format_running_jobs(&maintenance)
1049 ));
1050 }
1051 }
1052
1053 if self.idle_workers == 0 {
1054 let running: Vec<_> = self.running_jobs.values().collect();
1055 blockers.push(format!(
1056 "idle_workers==0({})",
1057 format_running_jobs(&running)
1058 ));
1059 }
1060
1061 BindBlockerSnapshot {
1062 configure_state,
1063 configure_phase_timings: self
1064 .actors
1065 .get(root_id)
1066 .map(|actor| actor.ctx.configure_ack_phase_snapshot()),
1067 blockers,
1068 }
1069 }
1070}
1071
1072fn is_configure_request(request_id: &str) -> bool {
1073 request_id.starts_with("subc-bind-")
1074}
1075
1076fn format_running_jobs(jobs: &[&RunningJob]) -> String {
1077 let now = Instant::now();
1078 let mut labels: Vec<_> = jobs
1079 .iter()
1080 .map(|job| {
1081 format!(
1082 "job={} command={} lane={:?} root={} age_ms={}",
1083 job.request_id,
1084 job.command,
1085 job.lane,
1086 job.root_id.as_path().display(),
1087 duration_millis_u64(now.saturating_duration_since(job.started_at))
1088 )
1089 })
1090 .collect();
1091 labels.sort();
1092 labels.truncate(4);
1093 labels.join("; ")
1094}
1095
1096#[derive(Default)]
1097struct QueueSnapshotAccumulator {
1098 queued: usize,
1099 oldest_age_ms: Option<u64>,
1100}
1101
1102impl QueueSnapshotAccumulator {
1103 fn add(&mut self, queues: &ClassQueues, now: Instant) {
1104 self.queued += queues.queued_count();
1105 if let Some(queued_at) = queues.oldest_queued_at() {
1106 let age_ms = duration_millis_u64(now.saturating_duration_since(queued_at));
1107 self.oldest_age_ms = Some(
1108 self.oldest_age_ms
1109 .map_or(age_ms, |oldest| oldest.max(age_ms)),
1110 );
1111 }
1112 }
1113
1114 fn finish(self) -> DispatchClassQueueSnapshot {
1115 DispatchClassQueueSnapshot {
1116 queued: self.queued,
1117 oldest_age_ms: self.oldest_age_ms,
1118 }
1119 }
1120}
1121
1122fn duration_millis_u64(duration: Duration) -> u64 {
1123 duration.as_millis().min(u128::from(u64::MAX)) as u64
1124}
1125
1126struct ActorState {
1127 ctx: Arc<AppContext>,
1128 epoch: Arc<RwLock<()>>,
1129 read_inflight: usize,
1130 lsp_inflight: bool,
1131 actor_total_inflight: usize,
1132 writer_inflight: bool,
1133 maintenance_commit_inflight: bool,
1134 mutating_inflight: Option<RunningMutatingJob>,
1135 deficit: isize,
1136 interactive: ClassQueues,
1137 maintenance: ClassQueues,
1138 fatal: bool,
1139}
1140
1141impl ActorState {
1142 fn new(ctx: Arc<AppContext>) -> Self {
1143 Self {
1144 ctx,
1145 epoch: Arc::new(RwLock::new(())),
1146 read_inflight: 0,
1147 lsp_inflight: false,
1148 actor_total_inflight: 0,
1149 writer_inflight: false,
1150 maintenance_commit_inflight: false,
1151 mutating_inflight: None,
1152 deficit: 0,
1153 interactive: ClassQueues::new(),
1154 maintenance: ClassQueues::new(),
1155 fatal: false,
1156 }
1157 }
1158
1159 fn push_job(&mut self, job_class: JobClass, lane: Lane, job: QueuedJob) {
1160 self.class_queues_mut(job_class).push_job(lane, job);
1161 }
1162
1163 fn has_queued_jobs(&self) -> bool {
1164 self.interactive.has_queued_jobs() || self.maintenance.has_queued_jobs()
1165 }
1166
1167 fn is_idle(&self) -> bool {
1168 self.actor_total_inflight == 0 && !self.has_queued_jobs()
1169 }
1170
1171 fn has_queued_jobs_for(&self, job_class: JobClass) -> bool {
1172 self.class_queues(job_class).has_queued_jobs()
1173 }
1174
1175 fn front_lane(&self, job_class: JobClass) -> Option<Lane> {
1176 self.class_queues(job_class).front_lane()
1177 }
1178
1179 fn pop_front_job(&mut self, job_class: JobClass, lane: Lane) -> Option<QueuedJob> {
1180 self.class_queues_mut(job_class).pop_front_job(lane)
1181 }
1182
1183 fn higher_priority_writer_barrier_blocks(&self, job_class: JobClass) -> bool {
1184 matches!(job_class, JobClass::Maintenance)
1188 && !self.interactive.queue(Lane::Mutating).is_empty()
1189 }
1190
1191 fn class_queues(&self, job_class: JobClass) -> &ClassQueues {
1192 match job_class {
1193 JobClass::Interactive => &self.interactive,
1194 JobClass::Maintenance => &self.maintenance,
1195 }
1196 }
1197
1198 fn class_queues_mut(&mut self, job_class: JobClass) -> &mut ClassQueues {
1199 match job_class {
1200 JobClass::Interactive => &mut self.interactive,
1201 JobClass::Maintenance => &mut self.maintenance,
1202 }
1203 }
1204
1205 fn fail_queued_jobs(&mut self) {
1206 self.interactive.fail_queued_jobs();
1207 self.maintenance.fail_queued_jobs();
1208 }
1209
1210 fn has_queued_mutating_job(&self, request_id: &str) -> bool {
1211 self.interactive.has_queued_mutating_job(request_id)
1212 || self.maintenance.has_queued_mutating_job(request_id)
1213 }
1214
1215 fn remove_queued_cancellable(&mut self, token: &JobCancellation) -> Option<QueuedJob> {
1216 self.interactive
1217 .remove_cancellable(token)
1218 .or_else(|| self.maintenance.remove_cancellable(token))
1219 }
1220
1221 fn pending_configure_count(&self) -> usize {
1222 usize::from(
1223 self.mutating_inflight
1224 .as_ref()
1225 .is_some_and(|job| is_configure_request(&job.request_id)),
1226 ) + self.interactive.queued_configure_count()
1227 + self.maintenance.queued_configure_count()
1228 }
1229}
1230
1231struct ClassQueues {
1232 order: VecDeque<Lane>,
1233 pure_reads: VecDeque<QueuedJob>,
1234 lsp_status: VecDeque<QueuedJob>,
1235 heavy_init: VecDeque<QueuedJob>,
1236 mutating: VecDeque<QueuedJob>,
1237 maintenance_commit: VecDeque<QueuedJob>,
1238}
1239
1240impl ClassQueues {
1241 fn new() -> Self {
1242 Self {
1243 order: VecDeque::new(),
1244 pure_reads: VecDeque::new(),
1245 lsp_status: VecDeque::new(),
1246 heavy_init: VecDeque::new(),
1247 mutating: VecDeque::new(),
1248 maintenance_commit: VecDeque::new(),
1249 }
1250 }
1251
1252 fn push_job(&mut self, lane: Lane, job: QueuedJob) {
1253 self.order.push_back(lane);
1254 self.queue_mut(lane).push_back(job);
1255 }
1256
1257 fn has_queued_jobs(&self) -> bool {
1258 !self.order.is_empty()
1259 }
1260
1261 fn front_lane(&self) -> Option<Lane> {
1262 self.order.front().copied()
1263 }
1264
1265 fn next_interactive_lane(&self, now: Instant) -> Option<Lane> {
1271 let starved_writer = self.mutating.iter().any(|job| {
1272 now.saturating_duration_since(job.queued_at) >= INTERACTIVE_WRITER_PROMOTION_AGE
1273 });
1274 if starved_writer {
1275 return Some(Lane::Mutating);
1278 }
1279 if !self.pure_reads.is_empty() {
1280 return Some(Lane::PureRead);
1281 }
1282 self.order
1283 .iter()
1284 .copied()
1285 .find(|lane| *lane != Lane::PureRead)
1286 }
1287
1288 fn pop_front_job(&mut self, lane: Lane) -> Option<QueuedJob> {
1289 let position = self.order.iter().position(|queued| *queued == lane)?;
1293 self.order.remove(position);
1294 self.queue_mut(lane).pop_front()
1295 }
1296
1297 fn queued_count(&self) -> usize {
1298 self.order.len()
1299 }
1300
1301 fn oldest_queued_at(&self) -> Option<Instant> {
1302 self.front_lane()
1303 .and_then(|lane| self.queue(lane).front().map(|job| job.queued_at))
1304 }
1305
1306 fn fail_queued_jobs(&mut self) {
1307 self.order.clear();
1308 fail_queued_job_queue(&mut self.pure_reads);
1309 fail_queued_job_queue(&mut self.lsp_status);
1310 fail_queued_job_queue(&mut self.heavy_init);
1311 fail_queued_job_queue(&mut self.mutating);
1312 fail_queued_job_queue(&mut self.maintenance_commit);
1313 }
1314
1315 fn cancel_queued_jobs(&mut self) -> usize {
1316 self.order.clear();
1317 cancel_queued_job_queue(&mut self.pure_reads)
1318 + cancel_queued_job_queue(&mut self.lsp_status)
1319 + cancel_queued_job_queue(&mut self.heavy_init)
1320 + cancel_queued_job_queue(&mut self.mutating)
1321 + cancel_queued_job_queue(&mut self.maintenance_commit)
1322 }
1323
1324 fn has_queued_mutating_job(&self, request_id: &str) -> bool {
1325 self.mutating.iter().any(|job| job.request_id == request_id)
1326 }
1327
1328 fn remove_cancellable(&mut self, token: &JobCancellation) -> Option<QueuedJob> {
1337 for lane in [
1338 Lane::PureRead,
1339 Lane::SerialLspStatus,
1340 Lane::HeavyInit,
1341 Lane::Mutating,
1342 Lane::MaintenanceCommit,
1343 ] {
1344 let queue = self.queue_mut(lane);
1345 let position = queue.iter().position(|queued| {
1346 queued
1347 .cancellation
1348 .as_ref()
1349 .is_some_and(|candidate| candidate.same_token(token))
1350 });
1351 if let Some(position) = position {
1352 let removed = queue.remove(position);
1353 let mut occurrence = 0usize;
1354 if let Some(order_position) = self.order.iter().position(|entry| {
1355 if *entry != lane {
1356 return false;
1357 }
1358 let matched = occurrence == position;
1359 occurrence += 1;
1360 matched
1361 }) {
1362 self.order.remove(order_position);
1363 }
1364 return removed;
1365 }
1366 }
1367 None
1368 }
1369
1370 fn queued_configure_count(&self) -> usize {
1371 self.mutating
1372 .iter()
1373 .filter(|job| is_configure_request(&job.request_id))
1374 .count()
1375 }
1376
1377 fn queue(&self, lane: Lane) -> &VecDeque<QueuedJob> {
1378 match lane {
1379 Lane::PureRead => &self.pure_reads,
1380 Lane::SerialLspStatus => &self.lsp_status,
1381 Lane::HeavyInit => &self.heavy_init,
1382 Lane::Mutating => &self.mutating,
1383 Lane::MaintenanceCommit => &self.maintenance_commit,
1384 }
1385 }
1386
1387 fn queue_mut(&mut self, lane: Lane) -> &mut VecDeque<QueuedJob> {
1388 match lane {
1389 Lane::PureRead => &mut self.pure_reads,
1390 Lane::SerialLspStatus => &mut self.lsp_status,
1391 Lane::HeavyInit => &mut self.heavy_init,
1392 Lane::Mutating => &mut self.mutating,
1393 Lane::MaintenanceCommit => &mut self.maintenance_commit,
1394 }
1395 }
1396}
1397
1398struct QueuedJob {
1399 job: ExecutorJob,
1400 completion: CompletionSender,
1401 request_id: String,
1402 command: String,
1403 queued_at: Instant,
1404 cancellation: Option<JobCancellation>,
1405}
1406
1407fn fail_queued_job_queue(queue: &mut VecDeque<QueuedJob>) {
1408 for queued in queue.drain(..) {
1409 queued
1410 .completion
1411 .send(actor_fatal_response(queued.request_id));
1412 }
1413}
1414
1415fn cancel_queued_job_queue(queue: &mut VecDeque<QueuedJob>) -> usize {
1416 let cancelled = queue.len();
1417 for queued in queue.drain(..) {
1418 queued.completion.send(Response::error(
1419 queued.request_id,
1420 "maintenance_cancelled",
1421 "maintenance cancelled because the actor has no bound routes",
1422 ));
1423 }
1424 cancelled
1425}
1426
1427fn job_command(job_class: JobClass, lane: Lane) -> String {
1428 format!("executor::{job_class:?}::{lane:?}")
1429}
1430
1431fn actor_fatal_response(request_id: impl Into<String>) -> Response {
1432 Response::error(
1433 request_id,
1434 "actor_fatal",
1435 "executor actor is fatal after a mutating job panic",
1436 )
1437}
1438
1439fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> String {
1440 if let Some(message) = payload.downcast_ref::<&'static str>() {
1441 (*message).to_string()
1442 } else if let Some(message) = payload.downcast_ref::<String>() {
1443 message.clone()
1444 } else {
1445 "unknown panic payload".to_string()
1446 }
1447}
1448
1449fn panic_response(
1450 request_id: impl Into<String>,
1451 command: &str,
1452 payload: &(dyn std::any::Any + Send),
1453) -> Response {
1454 let panic_message = panic_payload_message(payload);
1455 Response::error(
1456 request_id,
1457 "actor_fatal",
1458 format!("command '{command}' panicked: {panic_message}"),
1459 )
1460}
1461
1462enum CompletionSender {
1463 Sync(Sender<Response>),
1464 Async(oneshot::Sender<Response>),
1465}
1466
1467impl CompletionSender {
1468 fn send(self, response: Response) {
1469 match self {
1470 Self::Sync(tx) => {
1471 let _ = tx.send(response);
1472 }
1473 Self::Async(tx) => {
1474 let _ = tx.send(response);
1475 }
1476 }
1477 }
1478}
1479
1480struct RunJob {
1481 root_id: ProjectRootId,
1482 job_class: JobClass,
1483 lane: Lane,
1484 ctx: Arc<AppContext>,
1485 epoch: Arc<RwLock<()>>,
1486 job: ExecutorJob,
1487 completion: Option<CompletionSender>,
1488 request_id: String,
1489 command: String,
1490 heavy_permit: Option<HeavyPermit>,
1491 cancellation: Option<JobCancellation>,
1492}
1493
1494struct CompletionEvent {
1495 root_id: ProjectRootId,
1496 request_id: String,
1497 job_class: JobClass,
1498 lane: Lane,
1499 heavy_permit: Option<HeavyPermit>,
1500 panicked: bool,
1501}
1502
1503enum SchedulerEvent {
1504 Wake,
1505 Completed(CompletionEvent),
1506 Shutdown,
1507}
1508
1509fn scheduler_loop(
1510 state: Arc<Mutex<SchedulerState>>,
1511 heavy: Arc<HeavySemaphore>,
1512 run_tx: Sender<RunJob>,
1513 event_rx: Receiver<SchedulerEvent>,
1514 nonrunnable_dispatches: Arc<AtomicUsize>,
1515 completed_interactive: Arc<AtomicU64>,
1516 completed_maintenance: Arc<AtomicU64>,
1517) {
1518 while let Ok(event) = event_rx.recv() {
1519 let mut shutdown = false;
1520 {
1521 let mut state = state.lock();
1522 note_completion_event(&event, &completed_interactive, &completed_maintenance);
1523 shutdown |= process_scheduler_event(event, &mut state);
1524 while !shutdown {
1525 match event_rx.try_recv() {
1526 Ok(event) => {
1527 note_completion_event(
1528 &event,
1529 &completed_interactive,
1530 &completed_maintenance,
1531 );
1532 shutdown |= process_scheduler_event(event, &mut state)
1533 }
1534 Err(_) => break,
1535 }
1536 }
1537
1538 if !shutdown {
1539 dispatch_runnable(&mut state, &heavy, &run_tx, &nonrunnable_dispatches);
1540 }
1541 }
1542
1543 if shutdown {
1544 break;
1545 }
1546 }
1547}
1548
1549fn note_completion_event(
1550 event: &SchedulerEvent,
1551 completed_interactive: &AtomicU64,
1552 completed_maintenance: &AtomicU64,
1553) {
1554 let SchedulerEvent::Completed(event) = event else {
1555 return;
1556 };
1557 match event.job_class {
1558 JobClass::Interactive => {
1559 completed_interactive.fetch_add(1, Ordering::Relaxed);
1560 }
1561 JobClass::Maintenance => {
1562 completed_maintenance.fetch_add(1, Ordering::Relaxed);
1563 }
1564 }
1565}
1566
1567fn process_scheduler_event(event: SchedulerEvent, state: &mut SchedulerState) -> bool {
1568 match event {
1569 SchedulerEvent::Wake => false,
1570 SchedulerEvent::Completed(event) => {
1571 complete_job(state, event);
1572 false
1573 }
1574 SchedulerEvent::Shutdown => true,
1575 }
1576}
1577
1578fn complete_job(state: &mut SchedulerState, event: CompletionEvent) {
1579 let CompletionEvent {
1580 root_id,
1581 request_id,
1582 job_class,
1583 lane,
1584 heavy_permit,
1585 panicked,
1586 } = event;
1587 state.running_jobs.remove(&(root_id.clone(), request_id));
1588
1589 match job_class {
1590 JobClass::Interactive => {
1591 state.interactive_inflight = state.interactive_inflight.saturating_sub(1);
1592 }
1593 JobClass::Maintenance => {
1594 state.maintenance_inflight = state.maintenance_inflight.saturating_sub(1);
1595 }
1596 }
1597
1598 if let Some(actor) = state.actors.get_mut(&root_id) {
1599 actor.actor_total_inflight = actor.actor_total_inflight.saturating_sub(1);
1600 match lane {
1601 Lane::PureRead => {
1602 actor.read_inflight = actor.read_inflight.saturating_sub(1);
1603 }
1604 Lane::SerialLspStatus => {
1605 actor.lsp_inflight = false;
1606 }
1607 Lane::HeavyInit => {}
1608 Lane::Mutating => {
1609 actor.writer_inflight = false;
1610 actor.mutating_inflight = None;
1611 }
1612 Lane::MaintenanceCommit => {
1613 actor.maintenance_commit_inflight = false;
1614 }
1615 }
1616
1617 if panicked && lane == Lane::Mutating {
1618 actor.fatal = true;
1619 actor.fail_queued_jobs();
1620 }
1621 }
1622
1623 drop(heavy_permit);
1624 state.idle_workers += 1;
1625}
1626
1627fn dispatch_runnable(
1628 state: &mut SchedulerState,
1629 heavy: &Arc<HeavySemaphore>,
1630 run_tx: &Sender<RunJob>,
1631 nonrunnable_dispatches: &AtomicUsize,
1632) {
1633 while state.idle_workers > 0 && !state.actor_order.is_empty() {
1634 let mut made_progress = false;
1635 let mut dispatch_failed = false;
1636
1637 made_progress |= dispatch_runnable_class(
1638 state,
1639 JobClass::Interactive,
1640 heavy,
1641 run_tx,
1642 nonrunnable_dispatches,
1643 &mut dispatch_failed,
1644 );
1645 if dispatch_failed || state.idle_workers == 0 {
1646 return;
1647 }
1648
1649 if can_dispatch_class(state, JobClass::Maintenance) {
1650 made_progress |= dispatch_runnable_class(
1651 state,
1652 JobClass::Maintenance,
1653 heavy,
1654 run_tx,
1655 nonrunnable_dispatches,
1656 &mut dispatch_failed,
1657 );
1658 if dispatch_failed {
1659 return;
1660 }
1661 }
1662
1663 if !made_progress {
1664 break;
1665 }
1666 }
1667}
1668
1669fn dispatch_runnable_class(
1670 state: &mut SchedulerState,
1671 job_class: JobClass,
1672 heavy: &Arc<HeavySemaphore>,
1673 run_tx: &Sender<RunJob>,
1674 nonrunnable_dispatches: &AtomicUsize,
1675 dispatch_failed: &mut bool,
1676) -> bool {
1677 if !can_dispatch_class(state, job_class) || state.actor_order.is_empty() {
1678 return false;
1679 }
1680
1681 let actor_count = state.actor_order.len();
1682 let mut made_progress = false;
1683
1684 for _ in 0..actor_count {
1685 if !can_dispatch_class(state, job_class) || state.actor_order.is_empty() {
1686 break;
1687 }
1688
1689 if state.cursor >= state.actor_order.len() {
1690 state.cursor = 0;
1691 }
1692 let root_id = state.actor_order[state.cursor].clone();
1693 state.cursor = (state.cursor + 1) % state.actor_order.len();
1694
1695 let run_job = {
1696 let Some(actor) = state.actors.get_mut(&root_id) else {
1697 continue;
1698 };
1699
1700 if actor.fatal {
1701 actor.fail_queued_jobs();
1702 actor.deficit = 0;
1703 continue;
1704 }
1705
1706 if !actor.has_queued_jobs() {
1707 actor.deficit = 0;
1708 continue;
1709 }
1710
1711 if !actor.has_queued_jobs_for(job_class) {
1712 continue;
1713 }
1714
1715 actor.deficit =
1716 (actor.deficit + state.config.drr_quantum).min(state.config.deficit_cap);
1717 if actor.deficit < JOB_COST {
1718 continue;
1719 }
1720
1721 try_admit_actor(&root_id, actor, job_class, &state.config, heavy)
1722 };
1723
1724 if let Some(run_job) = run_job {
1725 state.running_jobs.insert(
1726 (run_job.root_id.clone(), run_job.request_id.clone()),
1727 RunningJob {
1728 root_id: run_job.root_id.clone(),
1729 request_id: run_job.request_id.clone(),
1730 command: run_job.command.clone(),
1731 job_class: run_job.job_class,
1732 lane: run_job.lane,
1733 started_at: Instant::now(),
1734 },
1735 );
1736 state.idle_workers -= 1;
1737 match job_class {
1738 JobClass::Interactive => state.interactive_inflight += 1,
1739 JobClass::Maintenance => state.maintenance_inflight += 1,
1740 }
1741 made_progress = true;
1742 if run_tx.send(run_job).is_err() {
1743 nonrunnable_dispatches.fetch_add(1, Ordering::AcqRel);
1744 *dispatch_failed = true;
1745 return made_progress;
1746 }
1747 }
1748 }
1749
1750 made_progress
1751}
1752
1753fn can_dispatch_class(state: &SchedulerState, job_class: JobClass) -> bool {
1754 if state.idle_workers == 0 {
1755 return false;
1756 }
1757 match job_class {
1758 JobClass::Interactive => true,
1759 JobClass::Maintenance => {
1760 state.maintenance_inflight < state.config.maintenance_cap
1761 && state.idle_workers > state.config.interactive_reserve
1762 }
1763 }
1764}
1765
1766fn try_admit_actor(
1767 root_id: &ProjectRootId,
1768 actor: &mut ActorState,
1769 job_class: JobClass,
1770 config: &EffectiveConfig,
1771 heavy: &Arc<HeavySemaphore>,
1772) -> Option<RunJob> {
1773 let lane = match job_class {
1774 JobClass::Interactive => actor
1775 .class_queues(JobClass::Interactive)
1776 .next_interactive_lane(Instant::now())?,
1777 JobClass::Maintenance => actor.front_lane(job_class)?,
1778 };
1779 let mut heavy_permit = None;
1780
1781 if actor.writer_inflight || actor.higher_priority_writer_barrier_blocks(job_class) {
1782 return None;
1783 }
1784
1785 let has_epoch_reader =
1786 actor.read_inflight > 0 || actor.lsp_inflight || actor.maintenance_commit_inflight;
1787 let actor_has_capacity = actor.actor_total_inflight < config.actor_cap;
1788 let runnable = match lane {
1789 Lane::PureRead => actor.read_inflight < config.read_cap && actor_has_capacity,
1790 Lane::SerialLspStatus => !actor.lsp_inflight && actor_has_capacity,
1791 Lane::HeavyInit => {
1792 if !actor_has_capacity {
1793 false
1794 } else if let Some(permit) = heavy.try_acquire() {
1795 heavy_permit = Some(permit);
1796 true
1797 } else {
1798 false
1799 }
1800 }
1801 Lane::Mutating => !has_epoch_reader && actor_has_capacity,
1802 Lane::MaintenanceCommit => !actor.maintenance_commit_inflight && actor_has_capacity,
1805 };
1806
1807 if !runnable {
1808 return None;
1809 }
1810
1811 let queued = actor.pop_front_job(job_class, lane)?;
1812 actor.deficit -= JOB_COST;
1813 if let Some(cancellation) = queued.cancellation.as_ref() {
1814 cancellation.mark_running();
1815 }
1816 if lane == Lane::Mutating {
1817 actor.mutating_inflight = Some(RunningMutatingJob {
1818 request_id: queued.request_id.clone(),
1819 command: queued.command.clone(),
1820 started_at: Instant::now(),
1821 });
1822 }
1823 match lane {
1824 Lane::PureRead => {
1825 actor.read_inflight += 1;
1826 actor.actor_total_inflight += 1;
1827 }
1828 Lane::SerialLspStatus => {
1829 actor.lsp_inflight = true;
1830 actor.actor_total_inflight += 1;
1831 }
1832 Lane::HeavyInit => {
1833 actor.actor_total_inflight += 1;
1834 }
1835 Lane::Mutating => {
1836 actor.writer_inflight = true;
1837 actor.actor_total_inflight += 1;
1838 }
1839 Lane::MaintenanceCommit => {
1840 actor.maintenance_commit_inflight = true;
1841 actor.actor_total_inflight += 1;
1842 }
1843 }
1844
1845 Some(RunJob {
1846 root_id: root_id.clone(),
1847 job_class,
1848 lane,
1849 ctx: Arc::clone(&actor.ctx),
1850 epoch: Arc::clone(&actor.epoch),
1851 job: queued.job,
1852 completion: Some(queued.completion),
1853 request_id: queued.request_id,
1854 command: queued.command,
1855 heavy_permit,
1856 cancellation: queued.cancellation,
1857 })
1858}
1859
1860fn worker_loop(run_rx: Receiver<RunJob>, event_tx: Sender<SchedulerEvent>) {
1861 while let Ok(mut run_job) = run_rx.recv() {
1862 let response =
1863 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run_lane_job(&mut run_job)));
1864 let panicked = response.is_err();
1865 let response = match response {
1866 Ok(response) => response,
1867 Err(payload) => panic_response(
1868 run_job.request_id.clone(),
1869 &run_job.command,
1870 payload.as_ref(),
1871 ),
1872 };
1873
1874 if let Some(completion) = run_job.completion.take() {
1875 completion.send(response);
1876 }
1877 let completion = CompletionEvent {
1878 root_id: run_job.root_id,
1879 request_id: run_job.request_id,
1880 job_class: run_job.job_class,
1881 lane: run_job.lane,
1882 heavy_permit: run_job.heavy_permit.take(),
1883 panicked,
1884 };
1885 let _ = event_tx.send(SchedulerEvent::Completed(completion));
1886 }
1887}
1888
1889fn run_lane_job(run_job: &mut RunJob) -> Response {
1890 let _cancellation_ctx = CurrentJobCancellationGuard::install(run_job.cancellation.clone());
1891 let missing_request_id = run_job.request_id.clone();
1892 let job = std::mem::replace(
1893 &mut run_job.job,
1894 Box::new(move |_| {
1895 Response::error(
1896 missing_request_id,
1897 "job_missing",
1898 "executor job already taken",
1899 )
1900 }),
1901 );
1902
1903 match run_job.lane {
1904 Lane::PureRead | Lane::SerialLspStatus => {
1905 let _epoch = run_job.epoch.read();
1906 job(&run_job.ctx)
1907 }
1908 Lane::HeavyInit => {
1909 let response = job(&run_job.ctx);
1910 {
1911 let _install = run_job.epoch.write();
1912 }
1913 response
1914 }
1915 Lane::Mutating => {
1916 let _epoch = run_job.epoch.write();
1917 job(&run_job.ctx)
1918 }
1919 Lane::MaintenanceCommit => {
1920 let _epoch = run_job.epoch.read();
1924 job(&run_job.ctx)
1925 }
1926 }
1927}
1928
1929#[derive(Debug)]
1930struct HeavySemaphore {
1931 available: AtomicUsize,
1932 max: usize,
1933}
1934
1935impl HeavySemaphore {
1936 fn new(permits: usize) -> Self {
1937 Self {
1938 available: AtomicUsize::new(permits),
1939 max: permits,
1940 }
1941 }
1942
1943 fn try_acquire(self: &Arc<Self>) -> Option<HeavyPermit> {
1944 loop {
1945 let available = self.available.load(Ordering::Acquire);
1946 if available == 0 {
1947 return None;
1948 }
1949 if self
1950 .available
1951 .compare_exchange(
1952 available,
1953 available - 1,
1954 Ordering::AcqRel,
1955 Ordering::Acquire,
1956 )
1957 .is_ok()
1958 {
1959 return Some(HeavyPermit {
1960 semaphore: Arc::clone(self),
1961 });
1962 }
1963 }
1964 }
1965}
1966
1967struct HeavyPermit {
1968 semaphore: Arc<HeavySemaphore>,
1969}
1970
1971impl Drop for HeavyPermit {
1972 fn drop(&mut self) {
1973 let previous = self.semaphore.available.fetch_add(1, Ordering::Release);
1974 debug_assert!(previous < self.semaphore.max);
1975 }
1976}