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