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