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