1mod single_flight;
2
3#[cfg(test)]
4mod tests;
5
6use std::{
7 collections::{HashMap, HashSet, 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;
25const SCHEDULER_EVENT_BATCH_CAP: usize = 64;
29pub(crate) const MAINTENANCE_QUEUE_CAP: usize = 512;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub enum Lane {
34 PureRead,
37 SerialLspStatus,
40 HeavyInit,
44 Mutating,
49 MaintenanceCommit,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
60pub enum JobClass {
61 Interactive,
62 Maintenance,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67pub(crate) enum MaintenanceCoalesceKey {
68 WatcherDrain,
69 LspDrain,
70}
71
72pub type ExecutorJob = Box<dyn FnOnce(&AppContext) -> Response + Send + 'static>;
73
74const INTERACTIVE_WRITER_PROMOTION_AGE: Duration = Duration::from_secs(6);
81
82#[derive(Debug, Clone)]
83pub struct ExecutorConfig {
84 pub pool_size: usize,
85 pub read_cap: usize,
86 pub actor_cap: usize,
87 pub heavy_permits: usize,
88 pub drr_quantum: isize,
89}
90
91impl Default for ExecutorConfig {
92 fn default() -> Self {
93 let available = thread::available_parallelism()
94 .map(usize::from)
95 .unwrap_or(2);
96 let pool_size = available.saturating_sub(1).clamp(2, 8);
97 let actor_cap = pool_size.saturating_sub(1).clamp(1, 4);
98 let read_cap = actor_cap.clamp(1, 4);
99 let heavy_permits = pool_size.saturating_sub(1).clamp(2, 3);
100
101 Self {
102 pool_size,
103 read_cap,
104 actor_cap,
105 heavy_permits,
106 drr_quantum: 1,
107 }
108 }
109}
110
111#[derive(Debug, Clone)]
112struct EffectiveConfig {
113 pool_size: usize,
114 read_cap: usize,
115 actor_cap: usize,
116 heavy_permits: usize,
117 drr_quantum: isize,
118 deficit_cap: isize,
119 interactive_reserve: usize,
120 maintenance_cap: usize,
121}
122
123impl ExecutorConfig {
124 fn effective(&self) -> EffectiveConfig {
125 let pool_size = self.pool_size.clamp(2, 8);
126 let max_actor_cap = pool_size.saturating_sub(1).max(1);
127 let actor_cap = self.actor_cap.max(1).min(max_actor_cap);
128 let read_cap = self.read_cap.max(1).min(actor_cap).min(4);
129 let heavy_permits = self
133 .heavy_permits
134 .max(1)
135 .min(pool_size.saturating_sub(1).max(1))
136 .min(3);
137 let drr_quantum = self.drr_quantum.max(1);
138 let deficit_cap = (actor_cap.max(1) as isize) * 4;
139 let interactive_reserve = if pool_size >= 4 { 2 } else { 1 };
140 let maintenance_cap = pool_size.saturating_sub(interactive_reserve).max(1);
141
142 EffectiveConfig {
143 pool_size,
144 read_cap,
145 actor_cap,
146 heavy_permits,
147 drr_quantum,
148 deficit_cap,
149 interactive_reserve,
150 maintenance_cap,
151 }
152 }
153}
154
155pub struct CompletionHandle {
158 rx: Receiver<Response>,
159}
160
161impl CompletionHandle {
162 pub fn recv(self) -> Result<Response, RecvError> {
163 self.rx.recv()
164 }
165
166 pub fn recv_timeout(&self, timeout: Duration) -> Result<Response, RecvTimeoutError> {
167 self.rx.recv_timeout(timeout)
168 }
169
170 pub fn into_receiver(self) -> Receiver<Response> {
171 self.rx
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct DispatchClassQueueSnapshot {
177 pub queued: usize,
178 pub oldest_age_ms: Option<u64>,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct DispatchRunningSnapshot {
183 pub interactive: usize,
184 pub maintenance: usize,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
188pub struct DispatchLivenessSnapshot {
189 pub interactive: DispatchClassQueueSnapshot,
190 pub maintenance: DispatchClassQueueSnapshot,
191 pub running: DispatchRunningSnapshot,
192 pub interactive_reserve: usize,
193 pub maintenance_cap: usize,
194}
195
196struct DispatchLivenessAtomics {
200 origin: Instant,
201 interactive_queued: AtomicUsize,
202 interactive_oldest_enqueued_ms_plus_one: AtomicU64,
203 maintenance_queued: AtomicUsize,
204 maintenance_oldest_enqueued_ms_plus_one: AtomicU64,
205 interactive_running: AtomicUsize,
206 maintenance_running: AtomicUsize,
207}
208
209impl DispatchLivenessAtomics {
210 fn new() -> Self {
211 Self {
212 origin: Instant::now(),
213 interactive_queued: AtomicUsize::new(0),
214 interactive_oldest_enqueued_ms_plus_one: AtomicU64::new(0),
215 maintenance_queued: AtomicUsize::new(0),
216 maintenance_oldest_enqueued_ms_plus_one: AtomicU64::new(0),
217 interactive_running: AtomicUsize::new(0),
218 maintenance_running: AtomicUsize::new(0),
219 }
220 }
221
222 fn now_ms(&self) -> u64 {
223 duration_millis_u64(self.origin.elapsed())
224 }
225
226 fn record(&self, snapshot: &DispatchLivenessSnapshot) {
227 let now_ms = self.now_ms();
228 let encode_oldest = |queued: usize, oldest_age_ms: Option<u64>| {
229 if queued == 0 {
230 0
231 } else {
232 now_ms
233 .saturating_sub(oldest_age_ms.unwrap_or(0))
234 .saturating_add(1)
235 }
236 };
237 self.interactive_oldest_enqueued_ms_plus_one.store(
238 encode_oldest(
239 snapshot.interactive.queued,
240 snapshot.interactive.oldest_age_ms,
241 ),
242 Ordering::Relaxed,
243 );
244 self.maintenance_oldest_enqueued_ms_plus_one.store(
245 encode_oldest(
246 snapshot.maintenance.queued,
247 snapshot.maintenance.oldest_age_ms,
248 ),
249 Ordering::Relaxed,
250 );
251 self.interactive_running
252 .store(snapshot.running.interactive, Ordering::Release);
253 self.maintenance_running
254 .store(snapshot.running.maintenance, Ordering::Release);
255 self.interactive_queued
256 .store(snapshot.interactive.queued, Ordering::Release);
257 self.maintenance_queued
258 .store(snapshot.maintenance.queued, Ordering::Release);
259 }
260
261 fn snapshot(&self, config: &EffectiveConfig) -> DispatchLivenessSnapshot {
262 let now_ms = self.now_ms();
263 let decode_oldest = |queued: usize, encoded: u64| {
264 (queued > 0 && encoded > 0).then(|| now_ms.saturating_sub(encoded - 1))
265 };
266 let interactive_queued = self.interactive_queued.load(Ordering::Acquire);
267 let maintenance_queued = self.maintenance_queued.load(Ordering::Acquire);
268 DispatchLivenessSnapshot {
269 interactive: DispatchClassQueueSnapshot {
270 queued: interactive_queued,
271 oldest_age_ms: decode_oldest(
272 interactive_queued,
273 self.interactive_oldest_enqueued_ms_plus_one
274 .load(Ordering::Relaxed),
275 ),
276 },
277 maintenance: DispatchClassQueueSnapshot {
278 queued: maintenance_queued,
279 oldest_age_ms: decode_oldest(
280 maintenance_queued,
281 self.maintenance_oldest_enqueued_ms_plus_one
282 .load(Ordering::Relaxed),
283 ),
284 },
285 running: DispatchRunningSnapshot {
286 interactive: self.interactive_running.load(Ordering::Acquire),
287 maintenance: self.maintenance_running.load(Ordering::Acquire),
288 },
289 interactive_reserve: config.interactive_reserve,
290 maintenance_cap: config.maintenance_cap,
291 }
292 }
293}
294
295#[derive(Debug, Clone, PartialEq, Eq)]
296pub struct MutatingLaneSnapshot {
297 pub root_id: ProjectRootId,
298 pub request_id: String,
299 pub command: String,
300 pub started_age_ms: u64,
301}
302
303#[derive(Debug, Clone, PartialEq, Eq)]
305pub struct BindBlockerSnapshot {
306 pub configure_state: &'static str,
307 pub configure_phase_timings: Option<String>,
308 pub blockers: Vec<String>,
309}
310
311#[derive(Debug, Clone)]
322pub struct JobCancellation {
323 inner: Arc<JobCancellationInner>,
324}
325
326#[derive(Debug)]
327struct JobCancellationInner {
328 state: AtomicU8,
329}
330
331const JOB_CANCEL_STATE_PENDING: u8 = 0;
332const JOB_CANCEL_STATE_RUNNING: u8 = 1;
333const JOB_CANCEL_STATE_COMMITTED: u8 = 2;
334const JOB_CANCEL_STATE_CANCELLED: u8 = 3;
335
336impl JobCancellation {
337 pub(crate) fn new() -> Self {
338 Self {
339 inner: Arc::new(JobCancellationInner {
340 state: AtomicU8::new(JOB_CANCEL_STATE_PENDING),
341 }),
342 }
343 }
344
345 fn mark_running(&self) {
346 let _ = self.inner.state.compare_exchange(
347 JOB_CANCEL_STATE_PENDING,
348 JOB_CANCEL_STATE_RUNNING,
349 Ordering::SeqCst,
350 Ordering::SeqCst,
351 );
352 }
353
354 #[must_use]
358 pub fn try_seal_committed(&self) -> bool {
359 loop {
360 let current = self.state();
361 match current {
362 JOB_CANCEL_STATE_CANCELLED => return false,
363 JOB_CANCEL_STATE_COMMITTED => return true,
364 _ => {
365 if self
366 .inner
367 .state
368 .compare_exchange(
369 current,
370 JOB_CANCEL_STATE_COMMITTED,
371 Ordering::SeqCst,
372 Ordering::SeqCst,
373 )
374 .is_ok()
375 {
376 return true;
377 }
378 }
379 }
380 }
381 }
382
383 fn signal_cancel(&self) -> u8 {
386 loop {
387 let current = self.state();
388 match current {
389 JOB_CANCEL_STATE_COMMITTED | JOB_CANCEL_STATE_CANCELLED => return current,
390 _ => {
391 if self
392 .inner
393 .state
394 .compare_exchange(
395 current,
396 JOB_CANCEL_STATE_CANCELLED,
397 Ordering::SeqCst,
398 Ordering::SeqCst,
399 )
400 .is_ok()
401 {
402 return current;
403 }
404 }
405 }
406 }
407 }
408
409 fn state(&self) -> u8 {
410 self.inner.state.load(Ordering::SeqCst)
411 }
412
413 pub fn cancel_requested_before_commit(&self) -> bool {
415 self.state() == JOB_CANCEL_STATE_CANCELLED
416 }
417
418 fn same_token(&self, other: &JobCancellation) -> bool {
419 Arc::ptr_eq(&self.inner, &other.inner)
420 }
421}
422
423#[derive(Debug, Clone, Copy, PartialEq, Eq)]
425pub enum JobCancelOutcome {
426 QueuedRemoved,
429 RunningSignalled,
432 RunningCommitted,
435 NotFound,
437}
438
439thread_local! {
440 static CURRENT_JOB_CANCELLATION: std::cell::RefCell<Option<JobCancellation>> =
441 const { std::cell::RefCell::new(None) };
442}
443
444pub fn current_job_cancellation() -> Option<JobCancellation> {
447 CURRENT_JOB_CANCELLATION.with(|slot| slot.borrow().clone())
448}
449
450struct CurrentJobCancellationGuard {
451 previous: Option<JobCancellation>,
452}
453
454impl CurrentJobCancellationGuard {
455 fn install(token: Option<JobCancellation>) -> Self {
456 let previous = CURRENT_JOB_CANCELLATION.with(|slot| slot.replace(token));
457 Self { previous }
458 }
459}
460
461impl Drop for CurrentJobCancellationGuard {
462 fn drop(&mut self) {
463 CURRENT_JOB_CANCELLATION.with(|slot| {
464 *slot.borrow_mut() = self.previous.take();
465 });
466 }
467}
468
469#[derive(Debug, Clone)]
470struct RunningJob {
471 root_id: ProjectRootId,
472 request_id: String,
473 command: String,
474 job_class: JobClass,
475 lane: Lane,
476 started_at: Instant,
477}
478
479#[derive(Debug, Clone)]
480struct RunningMutatingJob {
481 request_id: String,
482 command: String,
483 started_at: Instant,
484}
485
486pub struct Executor {
488 inner: Arc<ExecutorInner>,
489}
490
491impl Executor {
492 pub fn new() -> Self {
493 Self::with_config(ExecutorConfig::default())
494 }
495
496 pub fn with_config(config: ExecutorConfig) -> Self {
497 let effective = config.effective();
498 let state = Arc::new(Mutex::new(SchedulerState::new(effective.clone())));
499 let heavy = Arc::new(HeavySemaphore::new(effective.heavy_permits));
500 let nonrunnable_dispatches = Arc::new(AtomicUsize::new(0));
501 let completed_interactive = Arc::new(AtomicU64::new(0));
502 let completed_maintenance = Arc::new(AtomicU64::new(0));
503 let dispatch_liveness = Arc::new(DispatchLivenessAtomics::new());
504 let (run_tx, run_rx) = crossbeam_channel::unbounded();
505 let (event_tx, event_rx) = crossbeam_channel::unbounded();
506
507 let scheduler_state = Arc::clone(&state);
508 let scheduler_heavy = Arc::clone(&heavy);
509 let scheduler_violations = Arc::clone(&nonrunnable_dispatches);
510 let scheduler_completed_interactive = Arc::clone(&completed_interactive);
511 let scheduler_completed_maintenance = Arc::clone(&completed_maintenance);
512 let scheduler_dispatch_liveness = Arc::clone(&dispatch_liveness);
513 let scheduler_handle = thread::Builder::new()
514 .name("aft-executor-scheduler".to_string())
515 .spawn(move || {
516 scheduler_loop(
517 scheduler_state,
518 scheduler_heavy,
519 run_tx,
520 event_rx,
521 scheduler_violations,
522 scheduler_completed_interactive,
523 scheduler_completed_maintenance,
524 scheduler_dispatch_liveness,
525 );
526 })
527 .expect("spawn AFT executor scheduler");
528
529 let mut worker_handles = Vec::with_capacity(effective.pool_size);
530 for worker_id in 0..effective.pool_size {
531 let worker_rx = run_rx.clone();
532 let worker_events = event_tx.clone();
533 let handle = thread::Builder::new()
534 .name(format!("aft-executor-worker-{worker_id}"))
535 .spawn(move || worker_loop(worker_rx, worker_events))
536 .expect("spawn AFT executor worker");
537 worker_handles.push(handle);
538 }
539
540 Self {
541 inner: Arc::new(ExecutorInner {
542 state,
543 event_tx,
544 scheduler_handle: Mutex::new(Some(scheduler_handle)),
545 worker_handles: Mutex::new(worker_handles),
546 config: effective,
547 nonrunnable_dispatches,
548 completed_interactive,
549 completed_maintenance,
550 dispatch_liveness,
551 }),
552 }
553 }
554
555 pub fn register_actor(&self, root_id: ProjectRootId, ctx: Arc<AppContext>) -> bool {
562 let memory_root = root_id.as_path().to_path_buf();
563 let inserted = {
564 let mut state = self.inner.state.lock();
565 if state.actors.contains_key(&root_id) {
566 false
567 } else {
568 state.actor_order.push(root_id.clone());
569 state
570 .actors
571 .insert(root_id, ActorState::new(Arc::clone(&ctx)));
572 true
573 }
574 };
575 if inserted {
576 let app = ctx.app();
577 crate::root_cache::register_live_scope(&ctx.storage_dir(), &memory_root);
578 app.register_memory_context(memory_root, &ctx);
579 app.actor_root_registered();
580 }
581 self.wake_scheduler();
582 inserted
583 }
584
585 pub fn remove_actor(&self, root_id: &ProjectRootId) {
593 let removed = {
594 let mut state = self.inner.state.lock();
595 state.actor_order.retain(|actor_root| actor_root != root_id);
596 state.actors.remove(root_id)
597 };
598 if let Some(actor) = removed.as_ref() {
599 let app = actor.ctx.app();
600 crate::root_cache::unregister_live_scope(&actor.ctx.storage_dir(), root_id.as_path());
601 app.unregister_memory_context(root_id.as_path(), &actor.ctx);
602 app.actor_root_unregistered();
603 }
604 drop(removed);
605 self.wake_scheduler();
606 }
607
608 pub fn actor_is_idle(&self, root_id: &ProjectRootId) -> bool {
610 let state = self.inner.state.lock();
611 state.actors.get(root_id).is_some_and(ActorState::is_idle)
612 }
613
614 pub fn try_actor_is_idle(&self, root_id: &ProjectRootId) -> Option<bool> {
618 let state = self.inner.state.try_lock()?;
619 Some(state.actors.get(root_id).is_some_and(ActorState::is_idle))
620 }
621
622 pub fn retire_idle_actor_in_background(&self, root_id: &ProjectRootId) -> bool {
626 let removed = {
627 let mut state = self.inner.state.lock();
628 if !state.actors.get(root_id).is_some_and(ActorState::is_idle) {
629 return false;
630 }
631 state.actor_order.retain(|actor_root| actor_root != root_id);
632 state.actors.remove(root_id)
633 };
634 let Some(actor) = removed else {
635 return false;
636 };
637 let app = actor.ctx.app();
638 crate::root_cache::unregister_live_scope(&actor.ctx.storage_dir(), root_id.as_path());
639 app.unregister_memory_context(root_id.as_path(), &actor.ctx);
640 app.actor_root_unregistered();
641 std::thread::spawn(move || {
642 actor.ctx.teardown_deleted_root();
643 drop(actor);
644 });
645 self.wake_scheduler();
646 true
647 }
648
649 pub fn cancel_queued_maintenance(&self, root_id: &ProjectRootId) -> usize {
655 let cancelled = {
656 let mut state = self.inner.state.lock();
657 state
658 .actors
659 .get_mut(root_id)
660 .map(|actor| actor.maintenance.cancel_queued_jobs())
661 .unwrap_or(0)
662 };
663 if cancelled > 0 {
664 self.wake_scheduler();
665 }
666 cancelled
667 }
668
669 pub fn actor_registered(&self, root_id: &ProjectRootId) -> bool {
671 let state = self.inner.state.lock();
672 state.actors.contains_key(root_id)
673 }
674
675 pub fn actor_context(&self, root_id: &ProjectRootId) -> Option<Arc<AppContext>> {
678 let state = self.inner.state.lock();
679 state
680 .actors
681 .get(root_id)
682 .map(|actor| Arc::clone(&actor.ctx))
683 }
684
685 pub fn actor_contexts(&self) -> Vec<Arc<AppContext>> {
690 let state = self.inner.state.lock();
691 state
692 .actors
693 .values()
694 .map(|actor_state| Arc::clone(&actor_state.ctx))
695 .collect()
696 }
697
698 pub fn actor_entries(&self) -> Vec<(ProjectRootId, Arc<AppContext>)> {
700 let state = self.inner.state.lock();
701 state
702 .actors
703 .iter()
704 .map(|(root_id, actor_state)| (root_id.clone(), Arc::clone(&actor_state.ctx)))
705 .collect()
706 }
707
708 pub fn try_actor_entries(&self) -> Option<Vec<(ProjectRootId, Arc<AppContext>)>> {
712 let state = self.inner.state.try_lock()?;
713 Some(
714 state
715 .actors
716 .iter()
717 .map(|(root_id, actor_state)| (root_id.clone(), Arc::clone(&actor_state.ctx)))
718 .collect(),
719 )
720 }
721
722 pub fn try_actor_count(&self) -> Option<usize> {
724 self.inner.state.try_lock().map(|state| state.actors.len())
725 }
726
727 pub fn submit(
728 &self,
729 root_id: ProjectRootId,
730 lane: Lane,
731 request_id: String,
732 job: ExecutorJob,
733 ) -> CompletionHandle {
734 let (completion_tx, completion_rx) = crossbeam_channel::bounded(1);
735 self.submit_with_completion(
736 root_id,
737 JobClass::Interactive,
738 lane,
739 request_id,
740 job,
741 CompletionSender::Sync(completion_tx),
742 );
743 CompletionHandle { rx: completion_rx }
744 }
745
746 pub fn submit_async(
747 &self,
748 root_id: ProjectRootId,
749 lane: Lane,
750 request_id: String,
751 job: ExecutorJob,
752 ) -> oneshot::Receiver<Response> {
753 let (completion_tx, completion_rx) = oneshot::channel();
754 self.submit_with_completion(
755 root_id,
756 JobClass::Interactive,
757 lane,
758 request_id,
759 job,
760 CompletionSender::Async(completion_tx),
761 );
762 completion_rx
763 }
764
765 pub fn submit_cancellable_async(
771 &self,
772 root_id: ProjectRootId,
773 lane: Lane,
774 request_id: String,
775 job: ExecutorJob,
776 ) -> (oneshot::Receiver<Response>, JobCancellation) {
777 let cancellation = JobCancellation::new();
778 let (completion_tx, completion_rx) = oneshot::channel();
779 self.submit_with_completion_cancellable(
780 root_id,
781 JobClass::Interactive,
782 lane,
783 request_id,
784 job,
785 CompletionSender::Async(completion_tx),
786 Some(cancellation.clone()),
787 None,
788 );
789 (completion_rx, cancellation)
790 }
791
792 pub fn cancel_job(&self, root_id: &ProjectRootId, token: &JobCancellation) -> JobCancelOutcome {
798 let observed = token.signal_cancel();
805 let (outcome, settled) = {
806 let mut state = self.inner.state.lock();
807 let Some(actor) = state.actors.get_mut(root_id) else {
808 return match observed {
809 JOB_CANCEL_STATE_COMMITTED => JobCancelOutcome::RunningCommitted,
810 JOB_CANCEL_STATE_RUNNING | JOB_CANCEL_STATE_PENDING => {
811 JobCancelOutcome::RunningSignalled
812 }
813 _ => JobCancelOutcome::NotFound,
814 };
815 };
816 match actor.remove_queued_cancellable(token) {
817 Some(queued) => (JobCancelOutcome::QueuedRemoved, Some(queued)),
818 None => match observed {
819 JOB_CANCEL_STATE_COMMITTED => (JobCancelOutcome::RunningCommitted, None),
821 JOB_CANCEL_STATE_RUNNING | JOB_CANCEL_STATE_PENDING => {
825 (JobCancelOutcome::RunningSignalled, None)
826 }
827 _ => (JobCancelOutcome::NotFound, None),
829 },
830 }
831 };
832 if let Some(queued) = settled {
833 queued.completion.send(Response::error(
834 queued.request_id,
835 "request_cancelled",
836 "request cancelled before execution",
837 ));
838 self.wake_scheduler();
839 }
840 outcome
841 }
842
843 pub fn submit_maintenance_async(
844 &self,
845 root_id: ProjectRootId,
846 lane: Lane,
847 request_id: String,
848 job: ExecutorJob,
849 ) -> oneshot::Receiver<Response> {
850 self.submit_maintenance_async_with_key(root_id, lane, request_id, job, None)
851 }
852
853 pub(crate) fn submit_coalescable_maintenance_async(
854 &self,
855 root_id: ProjectRootId,
856 lane: Lane,
857 request_id: String,
858 coalesce_key: MaintenanceCoalesceKey,
859 job: ExecutorJob,
860 ) -> oneshot::Receiver<Response> {
861 self.submit_maintenance_async_with_key(root_id, lane, request_id, job, Some(coalesce_key))
862 }
863
864 fn submit_maintenance_async_with_key(
865 &self,
866 root_id: ProjectRootId,
867 lane: Lane,
868 request_id: String,
869 job: ExecutorJob,
870 coalesce_key: Option<MaintenanceCoalesceKey>,
871 ) -> oneshot::Receiver<Response> {
872 let (completion_tx, completion_rx) = oneshot::channel();
873 self.submit_with_completion_cancellable(
874 root_id,
875 JobClass::Maintenance,
876 lane,
877 request_id,
878 job,
879 CompletionSender::Async(completion_tx),
880 None,
881 coalesce_key,
882 );
883 completion_rx
884 }
885
886 fn submit_with_completion(
887 &self,
888 root_id: ProjectRootId,
889 job_class: JobClass,
890 lane: Lane,
891 request_id: String,
892 job: ExecutorJob,
893 completion: CompletionSender,
894 ) {
895 self.submit_with_completion_cancellable(
896 root_id, job_class, lane, request_id, job, completion, None, None,
897 );
898 }
899
900 #[allow(clippy::too_many_arguments)]
901 fn submit_with_completion_cancellable(
902 &self,
903 root_id: ProjectRootId,
904 job_class: JobClass,
905 lane: Lane,
906 request_id: String,
907 job: ExecutorJob,
908 completion: CompletionSender,
909 cancellation: Option<JobCancellation>,
910 maintenance_coalesce_key: Option<MaintenanceCoalesceKey>,
911 ) {
912 let command = job_command(job_class, lane);
913 let mut job = Some(job);
914 let mut completion = Some(completion);
915 let mut duplicate_victims = Vec::new();
916
917 let response = {
918 let mut state = self.inner.state.lock();
919 match state.actors.get_mut(&root_id) {
920 Some(actor) if actor.fatal => Some(actor_fatal_response(request_id.clone())),
921 Some(actor) => {
922 let mut admission_error = None;
923 if job_class == JobClass::Maintenance {
924 if maintenance_coalesce_key
925 .is_some_and(|key| actor.maintenance.has_maintenance_coalesce_key(key))
926 {
927 admission_error = Some(maintenance_cancelled_response(
928 request_id.clone(),
929 "maintenance drain coalesced behind an identical queued drain",
930 ));
931 } else {
932 if actor.maintenance.queued_count() >= MAINTENANCE_QUEUE_CAP {
933 duplicate_victims =
934 actor.maintenance.remove_duplicate_maintenance_jobs();
935 }
936 if actor.maintenance.queued_count() >= MAINTENANCE_QUEUE_CAP {
937 admission_error =
938 Some(maintenance_backpressure_response(request_id.clone()));
939 }
940 }
941 }
942
943 if admission_error.is_none() {
944 actor.push_job(
945 job_class,
946 lane,
947 QueuedJob {
948 job: job.take().expect("executor job already queued"),
949 completion: completion
950 .take()
951 .expect("executor completion already queued"),
952 request_id: request_id.clone(),
953 command,
954 queued_at: Instant::now(),
955 cancellation: cancellation.clone(),
956 maintenance_coalesce_key,
957 },
958 );
959 }
960 admission_error
961 }
962 None => Some(Response::error(
963 request_id.clone(),
964 "actor_not_registered",
965 "executor actor is not registered",
966 )),
967 }
968 };
969
970 for victim in duplicate_victims {
971 victim.completion.send(maintenance_cancelled_response(
972 victim.request_id,
973 "duplicate maintenance drain removed to preserve queue capacity",
974 ));
975 }
976
977 if let Some(response) = response {
978 if let Some(completion) = completion {
979 completion.send(response);
980 }
981 return;
982 }
983
984 self.wake_scheduler();
985 }
986
987 pub fn pool_size(&self) -> usize {
988 self.inner.config.pool_size
989 }
990
991 pub fn actor_cap(&self) -> usize {
992 self.inner.config.actor_cap
993 }
994
995 pub fn read_cap(&self) -> usize {
996 self.inner.config.read_cap
997 }
998
999 pub fn heavy_permits(&self) -> usize {
1000 self.inner.config.heavy_permits
1001 }
1002
1003 pub fn interactive_reserve(&self) -> usize {
1004 self.inner.config.interactive_reserve
1005 }
1006
1007 pub fn maintenance_cap(&self) -> usize {
1008 self.inner.config.maintenance_cap
1009 }
1010
1011 pub fn try_dispatch_liveness_snapshot(&self) -> Option<DispatchLivenessSnapshot> {
1012 Some(self.inner.dispatch_liveness.snapshot(&self.inner.config))
1013 }
1014
1015 pub fn try_mutating_lane_snapshots(&self) -> Option<Vec<MutatingLaneSnapshot>> {
1016 self.inner
1017 .state
1018 .try_lock()
1019 .map(|state| state.mutating_lane_snapshots())
1020 }
1021
1022 pub fn try_mutating_job_state_label(
1023 &self,
1024 root_id: &ProjectRootId,
1025 request_id: &str,
1026 ) -> Option<&'static str> {
1027 self.inner
1028 .state
1029 .try_lock()
1030 .map(|state| state.mutating_job_state_label(root_id, request_id))
1031 }
1032
1033 pub fn try_bind_blocker_snapshot(
1037 &self,
1038 root_id: &ProjectRootId,
1039 request_id: &str,
1040 ) -> Option<BindBlockerSnapshot> {
1041 self.inner
1042 .state
1043 .try_lock()
1044 .map(|state| state.bind_blocker_snapshot(root_id, request_id))
1045 }
1046
1047 pub fn nonrunnable_dispatch_count(&self) -> usize {
1048 self.inner.nonrunnable_dispatches.load(Ordering::Acquire)
1049 }
1050
1051 pub fn completion_counts(&self) -> (u64, u64) {
1052 (
1053 self.inner.completed_interactive.load(Ordering::Relaxed),
1054 self.inner.completed_maintenance.load(Ordering::Relaxed),
1055 )
1056 }
1057
1058 pub fn actor_is_fatal(&self, root_id: &ProjectRootId) -> bool {
1059 self.inner
1060 .state
1061 .lock()
1062 .actors
1063 .get(root_id)
1064 .map(|actor| actor.fatal)
1065 .unwrap_or(false)
1066 }
1067
1068 fn wake_scheduler(&self) {
1069 let _ = self.inner.event_tx.send(SchedulerEvent::Wake);
1070 }
1071}
1072
1073impl Default for Executor {
1074 fn default() -> Self {
1075 Self::new()
1076 }
1077}
1078
1079struct ExecutorInner {
1080 state: Arc<Mutex<SchedulerState>>,
1081 event_tx: Sender<SchedulerEvent>,
1082 scheduler_handle: Mutex<Option<JoinHandle<()>>>,
1083 worker_handles: Mutex<Vec<JoinHandle<()>>>,
1084 config: EffectiveConfig,
1085 nonrunnable_dispatches: Arc<AtomicUsize>,
1086 completed_interactive: Arc<AtomicU64>,
1087 completed_maintenance: Arc<AtomicU64>,
1088 dispatch_liveness: Arc<DispatchLivenessAtomics>,
1089}
1090
1091impl Drop for ExecutorInner {
1092 fn drop(&mut self) {
1093 let _ = self.event_tx.send(SchedulerEvent::Shutdown);
1094
1095 if let Some(handle) = self.scheduler_handle.lock().take() {
1096 let _ = handle.join();
1097 }
1098
1099 let mut workers = self.worker_handles.lock();
1100 for handle in workers.drain(..) {
1101 let _ = handle.join();
1102 }
1103 }
1104}
1105
1106struct SchedulerState {
1107 actors: HashMap<ProjectRootId, ActorState>,
1108 actor_order: Vec<ProjectRootId>,
1109 cursor: usize,
1110 idle_workers: usize,
1111 interactive_inflight: usize,
1112 maintenance_inflight: usize,
1113 config: EffectiveConfig,
1114 running_jobs: HashMap<(ProjectRootId, String), RunningJob>,
1115}
1116
1117impl SchedulerState {
1118 fn new(config: EffectiveConfig) -> Self {
1119 Self {
1120 actors: HashMap::new(),
1121 actor_order: Vec::new(),
1122 cursor: 0,
1123 idle_workers: config.pool_size,
1124 interactive_inflight: 0,
1125 maintenance_inflight: 0,
1126 config,
1127 running_jobs: HashMap::new(),
1128 }
1129 }
1130
1131 fn dispatch_liveness_snapshot(&self) -> DispatchLivenessSnapshot {
1132 let now = Instant::now();
1133 let mut interactive = QueueSnapshotAccumulator::default();
1134 let mut maintenance = QueueSnapshotAccumulator::default();
1135 for actor in self.actors.values() {
1136 interactive.add(actor.class_queues(JobClass::Interactive), now);
1137 maintenance.add(actor.class_queues(JobClass::Maintenance), now);
1138 }
1139
1140 DispatchLivenessSnapshot {
1141 interactive: interactive.finish(),
1142 maintenance: maintenance.finish(),
1143 running: DispatchRunningSnapshot {
1144 interactive: self.interactive_inflight,
1145 maintenance: self.maintenance_inflight,
1146 },
1147 interactive_reserve: self.config.interactive_reserve,
1148 maintenance_cap: self.config.maintenance_cap,
1149 }
1150 }
1151
1152 fn mutating_lane_snapshots(&self) -> Vec<MutatingLaneSnapshot> {
1153 let now = Instant::now();
1154 let mut snapshots: Vec<_> = self
1155 .actors
1156 .iter()
1157 .filter_map(|(root_id, actor)| {
1158 actor
1159 .mutating_inflight
1160 .as_ref()
1161 .map(|job| MutatingLaneSnapshot {
1162 root_id: root_id.clone(),
1163 request_id: job.request_id.clone(),
1164 command: job.command.clone(),
1165 started_age_ms: duration_millis_u64(
1166 now.saturating_duration_since(job.started_at),
1167 ),
1168 })
1169 })
1170 .collect();
1171 snapshots.sort_by(|left, right| left.root_id.as_path().cmp(right.root_id.as_path()));
1172 snapshots
1173 }
1174
1175 fn mutating_job_state_label(&self, root_id: &ProjectRootId, request_id: &str) -> &'static str {
1176 let Some(actor) = self.actors.get(root_id) else {
1177 return "actor_missing";
1178 };
1179 if actor
1180 .mutating_inflight
1181 .as_ref()
1182 .is_some_and(|job| job.request_id == request_id)
1183 {
1184 return "running";
1185 }
1186 if actor.has_queued_mutating_job(request_id) {
1187 return "queued";
1188 }
1189 if actor.writer_inflight {
1190 return "blocked_by_other_mutating";
1191 }
1192 "not_found"
1193 }
1194
1195 fn bind_blocker_snapshot(
1196 &self,
1197 root_id: &ProjectRootId,
1198 request_id: &str,
1199 ) -> BindBlockerSnapshot {
1200 let configure_state = self.mutating_job_state_label(root_id, request_id);
1201 let mut blockers = Vec::new();
1202
1203 if let Some(actor) = self.actors.get(root_id) {
1204 if configure_state == "queued" {
1205 let configure_count = actor.pending_configure_count();
1206 if configure_count > 0 {
1207 blockers.push(format!("queued_behind_configure({configure_count})"));
1208 }
1209 if actor.read_inflight > 0 || actor.lsp_inflight {
1210 blockers.push("waiting_on_readers".to_string());
1211 }
1212 }
1213 }
1214
1215 if configure_state == "queued" {
1216 let maintenance: Vec<_> = self
1217 .running_jobs
1218 .values()
1219 .filter(|job| job.job_class == JobClass::Maintenance)
1220 .collect();
1221 if !maintenance.is_empty() {
1222 blockers.push(format!(
1223 "queued_behind_maintenance({})",
1224 format_running_jobs(&maintenance)
1225 ));
1226 }
1227 }
1228
1229 if self.idle_workers == 0 {
1230 let running: Vec<_> = self.running_jobs.values().collect();
1231 blockers.push(format!(
1232 "idle_workers==0({})",
1233 format_running_jobs(&running)
1234 ));
1235 }
1236
1237 BindBlockerSnapshot {
1238 configure_state,
1239 configure_phase_timings: self
1240 .actors
1241 .get(root_id)
1242 .map(|actor| actor.ctx.configure_ack_phase_snapshot()),
1243 blockers,
1244 }
1245 }
1246}
1247
1248fn is_configure_request(request_id: &str) -> bool {
1249 request_id.starts_with("subc-bind-")
1250}
1251
1252fn format_running_jobs(jobs: &[&RunningJob]) -> String {
1253 let now = Instant::now();
1254 let mut labels: Vec<_> = jobs
1255 .iter()
1256 .map(|job| {
1257 format!(
1258 "job={} command={} lane={:?} root={} age_ms={}",
1259 job.request_id,
1260 job.command,
1261 job.lane,
1262 job.root_id.as_path().display(),
1263 duration_millis_u64(now.saturating_duration_since(job.started_at))
1264 )
1265 })
1266 .collect();
1267 labels.sort();
1268 labels.truncate(4);
1269 labels.join("; ")
1270}
1271
1272#[derive(Default)]
1273struct QueueSnapshotAccumulator {
1274 queued: usize,
1275 oldest_age_ms: Option<u64>,
1276}
1277
1278impl QueueSnapshotAccumulator {
1279 fn add(&mut self, queues: &ClassQueues, now: Instant) {
1280 self.queued += queues.queued_count();
1281 if let Some(queued_at) = queues.oldest_queued_at() {
1282 let age_ms = duration_millis_u64(now.saturating_duration_since(queued_at));
1283 self.oldest_age_ms = Some(
1284 self.oldest_age_ms
1285 .map_or(age_ms, |oldest| oldest.max(age_ms)),
1286 );
1287 }
1288 }
1289
1290 fn finish(self) -> DispatchClassQueueSnapshot {
1291 DispatchClassQueueSnapshot {
1292 queued: self.queued,
1293 oldest_age_ms: self.oldest_age_ms,
1294 }
1295 }
1296}
1297
1298fn duration_millis_u64(duration: Duration) -> u64 {
1299 duration.as_millis().min(u128::from(u64::MAX)) as u64
1300}
1301
1302struct ActorState {
1303 ctx: Arc<AppContext>,
1304 epoch: Arc<RwLock<()>>,
1305 read_inflight: usize,
1306 lsp_inflight: bool,
1307 actor_total_inflight: usize,
1308 writer_inflight: bool,
1309 maintenance_commit_inflight: bool,
1310 mutating_inflight: Option<RunningMutatingJob>,
1311 deficit: isize,
1312 interactive: ClassQueues,
1313 maintenance: ClassQueues,
1314 fatal: bool,
1315}
1316
1317impl ActorState {
1318 fn new(ctx: Arc<AppContext>) -> Self {
1319 Self {
1320 ctx,
1321 epoch: Arc::new(RwLock::new(())),
1322 read_inflight: 0,
1323 lsp_inflight: false,
1324 actor_total_inflight: 0,
1325 writer_inflight: false,
1326 maintenance_commit_inflight: false,
1327 mutating_inflight: None,
1328 deficit: 0,
1329 interactive: ClassQueues::new(),
1330 maintenance: ClassQueues::new(),
1331 fatal: false,
1332 }
1333 }
1334
1335 fn push_job(&mut self, job_class: JobClass, lane: Lane, job: QueuedJob) {
1336 self.class_queues_mut(job_class).push_job(lane, job);
1337 }
1338
1339 fn has_queued_jobs(&self) -> bool {
1340 self.interactive.has_queued_jobs() || self.maintenance.has_queued_jobs()
1341 }
1342
1343 fn is_idle(&self) -> bool {
1344 self.actor_total_inflight == 0 && !self.has_queued_jobs()
1345 }
1346
1347 fn has_queued_jobs_for(&self, job_class: JobClass) -> bool {
1348 self.class_queues(job_class).has_queued_jobs()
1349 }
1350
1351 fn front_lane(&self, job_class: JobClass) -> Option<Lane> {
1352 self.class_queues(job_class).front_lane()
1353 }
1354
1355 fn pop_front_job(&mut self, job_class: JobClass, lane: Lane) -> Option<QueuedJob> {
1356 self.class_queues_mut(job_class).pop_front_job(lane)
1357 }
1358
1359 fn higher_priority_writer_barrier_blocks(&self, job_class: JobClass) -> bool {
1360 matches!(job_class, JobClass::Maintenance)
1364 && !self.interactive.queue(Lane::Mutating).is_empty()
1365 }
1366
1367 fn class_queues(&self, job_class: JobClass) -> &ClassQueues {
1368 match job_class {
1369 JobClass::Interactive => &self.interactive,
1370 JobClass::Maintenance => &self.maintenance,
1371 }
1372 }
1373
1374 fn class_queues_mut(&mut self, job_class: JobClass) -> &mut ClassQueues {
1375 match job_class {
1376 JobClass::Interactive => &mut self.interactive,
1377 JobClass::Maintenance => &mut self.maintenance,
1378 }
1379 }
1380
1381 fn fail_queued_jobs(&mut self) {
1382 self.interactive.fail_queued_jobs();
1383 self.maintenance.fail_queued_jobs();
1384 }
1385
1386 fn has_queued_mutating_job(&self, request_id: &str) -> bool {
1387 self.interactive.has_queued_mutating_job(request_id)
1388 || self.maintenance.has_queued_mutating_job(request_id)
1389 }
1390
1391 fn remove_queued_cancellable(&mut self, token: &JobCancellation) -> Option<QueuedJob> {
1392 self.interactive
1393 .remove_cancellable(token)
1394 .or_else(|| self.maintenance.remove_cancellable(token))
1395 }
1396
1397 fn pending_configure_count(&self) -> usize {
1398 usize::from(
1399 self.mutating_inflight
1400 .as_ref()
1401 .is_some_and(|job| is_configure_request(&job.request_id)),
1402 ) + self.interactive.queued_configure_count()
1403 + self.maintenance.queued_configure_count()
1404 }
1405}
1406
1407struct ClassQueues {
1408 order: VecDeque<Lane>,
1409 pure_reads: VecDeque<QueuedJob>,
1410 lsp_status: VecDeque<QueuedJob>,
1411 heavy_init: VecDeque<QueuedJob>,
1412 mutating: VecDeque<QueuedJob>,
1413 maintenance_commit: VecDeque<QueuedJob>,
1414}
1415
1416impl ClassQueues {
1417 fn new() -> Self {
1418 Self {
1419 order: VecDeque::new(),
1420 pure_reads: VecDeque::new(),
1421 lsp_status: VecDeque::new(),
1422 heavy_init: VecDeque::new(),
1423 mutating: VecDeque::new(),
1424 maintenance_commit: VecDeque::new(),
1425 }
1426 }
1427
1428 fn push_job(&mut self, lane: Lane, job: QueuedJob) {
1429 self.order.push_back(lane);
1430 self.queue_mut(lane).push_back(job);
1431 }
1432
1433 fn has_queued_jobs(&self) -> bool {
1434 !self.order.is_empty()
1435 }
1436
1437 fn front_lane(&self) -> Option<Lane> {
1438 self.order.front().copied()
1439 }
1440
1441 fn next_interactive_lane(&self, now: Instant) -> Option<Lane> {
1447 let starved_writer = self.mutating.iter().any(|job| {
1448 now.saturating_duration_since(job.queued_at) >= INTERACTIVE_WRITER_PROMOTION_AGE
1449 });
1450 if starved_writer {
1451 return Some(Lane::Mutating);
1454 }
1455 if !self.pure_reads.is_empty() {
1456 return Some(Lane::PureRead);
1457 }
1458 self.order
1459 .iter()
1460 .copied()
1461 .find(|lane| *lane != Lane::PureRead)
1462 }
1463
1464 fn pop_front_job(&mut self, lane: Lane) -> Option<QueuedJob> {
1465 let position = self.order.iter().position(|queued| *queued == lane)?;
1469 self.order.remove(position);
1470 self.queue_mut(lane).pop_front()
1471 }
1472
1473 fn queued_count(&self) -> usize {
1474 self.order.len()
1475 }
1476
1477 fn has_maintenance_coalesce_key(&self, key: MaintenanceCoalesceKey) -> bool {
1478 [
1479 Lane::PureRead,
1480 Lane::SerialLspStatus,
1481 Lane::HeavyInit,
1482 Lane::Mutating,
1483 Lane::MaintenanceCommit,
1484 ]
1485 .into_iter()
1486 .any(|lane| {
1487 self.queue(lane)
1488 .iter()
1489 .any(|job| job.maintenance_coalesce_key == Some(key))
1490 })
1491 }
1492
1493 fn remove_duplicate_maintenance_jobs(&mut self) -> Vec<QueuedJob> {
1495 let mut seen = HashSet::new();
1496 let mut lane_positions = [0usize; 5];
1497 let mut duplicates = Vec::new();
1498
1499 for (order_position, lane) in self.order.iter().copied().enumerate() {
1500 let lane_index = lane_index(lane);
1501 let queue_position = lane_positions[lane_index];
1502 lane_positions[lane_index] += 1;
1503 let Some(key) = self
1504 .queue(lane)
1505 .get(queue_position)
1506 .and_then(|job| job.maintenance_coalesce_key)
1507 else {
1508 continue;
1509 };
1510 if !seen.insert(key) {
1511 duplicates.push((order_position, lane, queue_position));
1512 }
1513 }
1514
1515 let mut removed = Vec::with_capacity(duplicates.len());
1516 for (order_position, lane, queue_position) in duplicates.into_iter().rev() {
1517 self.order.remove(order_position);
1518 if let Some(job) = self.queue_mut(lane).remove(queue_position) {
1519 removed.push(job);
1520 }
1521 }
1522 removed
1523 }
1524
1525 fn oldest_queued_at(&self) -> Option<Instant> {
1526 self.front_lane()
1527 .and_then(|lane| self.queue(lane).front().map(|job| job.queued_at))
1528 }
1529
1530 fn fail_queued_jobs(&mut self) {
1531 self.order.clear();
1532 fail_queued_job_queue(&mut self.pure_reads);
1533 fail_queued_job_queue(&mut self.lsp_status);
1534 fail_queued_job_queue(&mut self.heavy_init);
1535 fail_queued_job_queue(&mut self.mutating);
1536 fail_queued_job_queue(&mut self.maintenance_commit);
1537 }
1538
1539 fn cancel_queued_jobs(&mut self) -> usize {
1540 self.order.clear();
1541 cancel_queued_job_queue(&mut self.pure_reads)
1542 + cancel_queued_job_queue(&mut self.lsp_status)
1543 + cancel_queued_job_queue(&mut self.heavy_init)
1544 + cancel_queued_job_queue(&mut self.mutating)
1545 + cancel_queued_job_queue(&mut self.maintenance_commit)
1546 }
1547
1548 fn has_queued_mutating_job(&self, request_id: &str) -> bool {
1549 self.mutating.iter().any(|job| job.request_id == request_id)
1550 }
1551
1552 fn remove_cancellable(&mut self, token: &JobCancellation) -> Option<QueuedJob> {
1561 for lane in [
1562 Lane::PureRead,
1563 Lane::SerialLspStatus,
1564 Lane::HeavyInit,
1565 Lane::Mutating,
1566 Lane::MaintenanceCommit,
1567 ] {
1568 let queue = self.queue_mut(lane);
1569 let position = queue.iter().position(|queued| {
1570 queued
1571 .cancellation
1572 .as_ref()
1573 .is_some_and(|candidate| candidate.same_token(token))
1574 });
1575 if let Some(position) = position {
1576 let removed = queue.remove(position);
1577 let mut occurrence = 0usize;
1578 if let Some(order_position) = self.order.iter().position(|entry| {
1579 if *entry != lane {
1580 return false;
1581 }
1582 let matched = occurrence == position;
1583 occurrence += 1;
1584 matched
1585 }) {
1586 self.order.remove(order_position);
1587 }
1588 return removed;
1589 }
1590 }
1591 None
1592 }
1593
1594 fn queued_configure_count(&self) -> usize {
1595 self.mutating
1596 .iter()
1597 .filter(|job| is_configure_request(&job.request_id))
1598 .count()
1599 }
1600
1601 fn queue(&self, lane: Lane) -> &VecDeque<QueuedJob> {
1602 match lane {
1603 Lane::PureRead => &self.pure_reads,
1604 Lane::SerialLspStatus => &self.lsp_status,
1605 Lane::HeavyInit => &self.heavy_init,
1606 Lane::Mutating => &self.mutating,
1607 Lane::MaintenanceCommit => &self.maintenance_commit,
1608 }
1609 }
1610
1611 fn queue_mut(&mut self, lane: Lane) -> &mut VecDeque<QueuedJob> {
1612 match lane {
1613 Lane::PureRead => &mut self.pure_reads,
1614 Lane::SerialLspStatus => &mut self.lsp_status,
1615 Lane::HeavyInit => &mut self.heavy_init,
1616 Lane::Mutating => &mut self.mutating,
1617 Lane::MaintenanceCommit => &mut self.maintenance_commit,
1618 }
1619 }
1620}
1621
1622struct QueuedJob {
1623 job: ExecutorJob,
1624 completion: CompletionSender,
1625 request_id: String,
1626 command: String,
1627 queued_at: Instant,
1628 cancellation: Option<JobCancellation>,
1629 maintenance_coalesce_key: Option<MaintenanceCoalesceKey>,
1630}
1631
1632fn lane_index(lane: Lane) -> usize {
1633 match lane {
1634 Lane::PureRead => 0,
1635 Lane::SerialLspStatus => 1,
1636 Lane::HeavyInit => 2,
1637 Lane::Mutating => 3,
1638 Lane::MaintenanceCommit => 4,
1639 }
1640}
1641
1642fn fail_queued_job_queue(queue: &mut VecDeque<QueuedJob>) {
1643 for queued in queue.drain(..) {
1644 queued
1645 .completion
1646 .send(actor_fatal_response(queued.request_id));
1647 }
1648}
1649
1650fn cancel_queued_job_queue(queue: &mut VecDeque<QueuedJob>) -> usize {
1651 let cancelled = queue.len();
1652 for queued in queue.drain(..) {
1653 queued.completion.send(Response::error(
1654 queued.request_id,
1655 "maintenance_cancelled",
1656 "maintenance cancelled because the actor has no bound routes",
1657 ));
1658 }
1659 cancelled
1660}
1661
1662fn job_command(job_class: JobClass, lane: Lane) -> String {
1663 format!("executor::{job_class:?}::{lane:?}")
1664}
1665
1666fn maintenance_cancelled_response(
1667 request_id: impl Into<String>,
1668 message: impl Into<String>,
1669) -> Response {
1670 Response::error(request_id, "maintenance_cancelled", message)
1671}
1672
1673fn maintenance_backpressure_response(request_id: impl Into<String>) -> Response {
1674 Response::error_with_data(
1675 request_id,
1676 "maintenance_backpressure",
1677 format!("maintenance queue reached its per-actor capacity of {MAINTENANCE_QUEUE_CAP} jobs"),
1678 serde_json::json!({
1679 "retryable": true,
1680 "queue_cap": MAINTENANCE_QUEUE_CAP,
1681 }),
1682 )
1683}
1684
1685fn actor_fatal_response(request_id: impl Into<String>) -> Response {
1686 Response::error(
1687 request_id,
1688 "actor_fatal",
1689 "executor actor is fatal after a mutating job panic",
1690 )
1691}
1692
1693fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> String {
1694 if let Some(message) = payload.downcast_ref::<&'static str>() {
1695 (*message).to_string()
1696 } else if let Some(message) = payload.downcast_ref::<String>() {
1697 message.clone()
1698 } else {
1699 "unknown panic payload".to_string()
1700 }
1701}
1702
1703fn panic_response(
1704 request_id: impl Into<String>,
1705 command: &str,
1706 payload: &(dyn std::any::Any + Send),
1707) -> Response {
1708 let panic_message = panic_payload_message(payload);
1709 Response::error(
1710 request_id,
1711 "actor_fatal",
1712 format!("command '{command}' panicked: {panic_message}"),
1713 )
1714}
1715
1716enum CompletionSender {
1717 Sync(Sender<Response>),
1718 Async(oneshot::Sender<Response>),
1719}
1720
1721impl CompletionSender {
1722 fn send(self, response: Response) {
1723 match self {
1724 Self::Sync(tx) => {
1725 let _ = tx.send(response);
1726 }
1727 Self::Async(tx) => {
1728 let _ = tx.send(response);
1729 }
1730 }
1731 }
1732}
1733
1734struct RunJob {
1735 root_id: ProjectRootId,
1736 job_class: JobClass,
1737 lane: Lane,
1738 ctx: Arc<AppContext>,
1739 epoch: Arc<RwLock<()>>,
1740 job: ExecutorJob,
1741 completion: Option<CompletionSender>,
1742 request_id: String,
1743 command: String,
1744 heavy_permit: Option<HeavyPermit>,
1745 cancellation: Option<JobCancellation>,
1746}
1747
1748struct CompletionEvent {
1749 root_id: ProjectRootId,
1750 request_id: String,
1751 job_class: JobClass,
1752 lane: Lane,
1753 heavy_permit: Option<HeavyPermit>,
1754 panicked: bool,
1755}
1756
1757enum SchedulerEvent {
1758 Wake,
1759 Completed(CompletionEvent),
1760 Shutdown,
1761}
1762
1763fn scheduler_loop(
1764 state: Arc<Mutex<SchedulerState>>,
1765 heavy: Arc<HeavySemaphore>,
1766 run_tx: Sender<RunJob>,
1767 event_rx: Receiver<SchedulerEvent>,
1768 nonrunnable_dispatches: Arc<AtomicUsize>,
1769 completed_interactive: Arc<AtomicU64>,
1770 completed_maintenance: Arc<AtomicU64>,
1771 dispatch_liveness: Arc<DispatchLivenessAtomics>,
1772) {
1773 while let Ok(event) = event_rx.recv() {
1774 let shutdown;
1775 {
1776 let mut state = state.lock();
1777 shutdown = process_scheduler_event_batch(
1778 event,
1779 &event_rx,
1780 &mut state,
1781 &completed_interactive,
1782 &completed_maintenance,
1783 );
1784
1785 if !shutdown {
1786 dispatch_runnable(&mut state, &heavy, &run_tx, &nonrunnable_dispatches);
1787 }
1788 dispatch_liveness.record(&state.dispatch_liveness_snapshot());
1789 }
1790
1791 if shutdown {
1792 break;
1793 }
1794 }
1795}
1796
1797fn process_scheduler_event_batch(
1798 first: SchedulerEvent,
1799 event_rx: &Receiver<SchedulerEvent>,
1800 state: &mut SchedulerState,
1801 completed_interactive: &AtomicU64,
1802 completed_maintenance: &AtomicU64,
1803) -> bool {
1804 let mut event = Some(first);
1805 for _ in 0..SCHEDULER_EVENT_BATCH_CAP {
1806 let Some(current) = event.take().or_else(|| event_rx.try_recv().ok()) else {
1807 break;
1808 };
1809 note_completion_event(¤t, completed_interactive, completed_maintenance);
1810 if process_scheduler_event(current, state) {
1811 return true;
1812 }
1813 }
1814 false
1815}
1816
1817fn note_completion_event(
1818 event: &SchedulerEvent,
1819 completed_interactive: &AtomicU64,
1820 completed_maintenance: &AtomicU64,
1821) {
1822 let SchedulerEvent::Completed(event) = event else {
1823 return;
1824 };
1825 match event.job_class {
1826 JobClass::Interactive => {
1827 completed_interactive.fetch_add(1, Ordering::Relaxed);
1828 }
1829 JobClass::Maintenance => {
1830 completed_maintenance.fetch_add(1, Ordering::Relaxed);
1831 }
1832 }
1833}
1834
1835fn process_scheduler_event(event: SchedulerEvent, state: &mut SchedulerState) -> bool {
1836 match event {
1837 SchedulerEvent::Wake => false,
1838 SchedulerEvent::Completed(event) => {
1839 complete_job(state, event);
1840 false
1841 }
1842 SchedulerEvent::Shutdown => true,
1843 }
1844}
1845
1846fn complete_job(state: &mut SchedulerState, event: CompletionEvent) {
1847 let CompletionEvent {
1848 root_id,
1849 request_id,
1850 job_class,
1851 lane,
1852 heavy_permit,
1853 panicked,
1854 } = event;
1855 state.running_jobs.remove(&(root_id.clone(), request_id));
1856
1857 match job_class {
1858 JobClass::Interactive => {
1859 state.interactive_inflight = state.interactive_inflight.saturating_sub(1);
1860 }
1861 JobClass::Maintenance => {
1862 state.maintenance_inflight = state.maintenance_inflight.saturating_sub(1);
1863 }
1864 }
1865
1866 if let Some(actor) = state.actors.get_mut(&root_id) {
1867 actor.actor_total_inflight = actor.actor_total_inflight.saturating_sub(1);
1868 match lane {
1869 Lane::PureRead => {
1870 actor.read_inflight = actor.read_inflight.saturating_sub(1);
1871 }
1872 Lane::SerialLspStatus => {
1873 actor.lsp_inflight = false;
1874 }
1875 Lane::HeavyInit => {}
1876 Lane::Mutating => {
1877 actor.writer_inflight = false;
1878 actor.mutating_inflight = None;
1879 }
1880 Lane::MaintenanceCommit => {
1881 actor.maintenance_commit_inflight = false;
1882 }
1883 }
1884
1885 if panicked && lane == Lane::Mutating {
1886 actor.fatal = true;
1887 actor.fail_queued_jobs();
1888 }
1889 }
1890
1891 drop(heavy_permit);
1892 state.idle_workers += 1;
1893}
1894
1895fn dispatch_runnable(
1896 state: &mut SchedulerState,
1897 heavy: &Arc<HeavySemaphore>,
1898 run_tx: &Sender<RunJob>,
1899 nonrunnable_dispatches: &AtomicUsize,
1900) {
1901 while state.idle_workers > 0 && !state.actor_order.is_empty() {
1902 let mut made_progress = false;
1903 let mut dispatch_failed = false;
1904
1905 made_progress |= dispatch_runnable_class(
1906 state,
1907 JobClass::Interactive,
1908 heavy,
1909 run_tx,
1910 nonrunnable_dispatches,
1911 &mut dispatch_failed,
1912 );
1913 if dispatch_failed || state.idle_workers == 0 {
1914 return;
1915 }
1916
1917 if can_dispatch_class(state, JobClass::Maintenance) {
1918 made_progress |= dispatch_runnable_class(
1919 state,
1920 JobClass::Maintenance,
1921 heavy,
1922 run_tx,
1923 nonrunnable_dispatches,
1924 &mut dispatch_failed,
1925 );
1926 if dispatch_failed {
1927 return;
1928 }
1929 }
1930
1931 if !made_progress {
1932 break;
1933 }
1934 }
1935}
1936
1937fn dispatch_runnable_class(
1938 state: &mut SchedulerState,
1939 job_class: JobClass,
1940 heavy: &Arc<HeavySemaphore>,
1941 run_tx: &Sender<RunJob>,
1942 nonrunnable_dispatches: &AtomicUsize,
1943 dispatch_failed: &mut bool,
1944) -> bool {
1945 if !can_dispatch_class(state, job_class) || state.actor_order.is_empty() {
1946 return false;
1947 }
1948
1949 let actor_count = state.actor_order.len();
1950 let mut made_progress = false;
1951
1952 for _ in 0..actor_count {
1953 if !can_dispatch_class(state, job_class) || state.actor_order.is_empty() {
1954 break;
1955 }
1956
1957 if state.cursor >= state.actor_order.len() {
1958 state.cursor = 0;
1959 }
1960 let root_id = state.actor_order[state.cursor].clone();
1961 state.cursor = (state.cursor + 1) % state.actor_order.len();
1962
1963 let run_job = {
1964 let Some(actor) = state.actors.get_mut(&root_id) else {
1965 continue;
1966 };
1967
1968 if actor.fatal {
1969 actor.fail_queued_jobs();
1970 actor.deficit = 0;
1971 continue;
1972 }
1973
1974 if !actor.has_queued_jobs() {
1975 actor.deficit = 0;
1976 continue;
1977 }
1978
1979 if !actor.has_queued_jobs_for(job_class) {
1980 continue;
1981 }
1982
1983 actor.deficit =
1984 (actor.deficit + state.config.drr_quantum).min(state.config.deficit_cap);
1985 if actor.deficit < JOB_COST {
1986 continue;
1987 }
1988
1989 try_admit_actor(&root_id, actor, job_class, &state.config, heavy)
1990 };
1991
1992 if let Some(run_job) = run_job {
1993 state.running_jobs.insert(
1994 (run_job.root_id.clone(), run_job.request_id.clone()),
1995 RunningJob {
1996 root_id: run_job.root_id.clone(),
1997 request_id: run_job.request_id.clone(),
1998 command: run_job.command.clone(),
1999 job_class: run_job.job_class,
2000 lane: run_job.lane,
2001 started_at: Instant::now(),
2002 },
2003 );
2004 state.idle_workers -= 1;
2005 match job_class {
2006 JobClass::Interactive => state.interactive_inflight += 1,
2007 JobClass::Maintenance => state.maintenance_inflight += 1,
2008 }
2009 made_progress = true;
2010 if run_tx.send(run_job).is_err() {
2011 nonrunnable_dispatches.fetch_add(1, Ordering::AcqRel);
2012 *dispatch_failed = true;
2013 return made_progress;
2014 }
2015 }
2016 }
2017
2018 made_progress
2019}
2020
2021fn can_dispatch_class(state: &SchedulerState, job_class: JobClass) -> bool {
2022 if state.idle_workers == 0 {
2023 return false;
2024 }
2025 match job_class {
2026 JobClass::Interactive => true,
2027 JobClass::Maintenance => {
2028 state.maintenance_inflight < state.config.maintenance_cap
2029 && state.idle_workers > state.config.interactive_reserve
2030 }
2031 }
2032}
2033
2034fn try_admit_actor(
2035 root_id: &ProjectRootId,
2036 actor: &mut ActorState,
2037 job_class: JobClass,
2038 config: &EffectiveConfig,
2039 heavy: &Arc<HeavySemaphore>,
2040) -> Option<RunJob> {
2041 let lane = match job_class {
2042 JobClass::Interactive => actor
2043 .class_queues(JobClass::Interactive)
2044 .next_interactive_lane(Instant::now())?,
2045 JobClass::Maintenance => actor.front_lane(job_class)?,
2046 };
2047 let mut heavy_permit = None;
2048
2049 if actor.writer_inflight || actor.higher_priority_writer_barrier_blocks(job_class) {
2050 return None;
2051 }
2052
2053 let has_epoch_reader =
2054 actor.read_inflight > 0 || actor.lsp_inflight || actor.maintenance_commit_inflight;
2055 let actor_has_capacity = actor.actor_total_inflight < config.actor_cap;
2056 let runnable = match lane {
2057 Lane::PureRead => actor.read_inflight < config.read_cap && actor_has_capacity,
2058 Lane::SerialLspStatus => !actor.lsp_inflight && actor_has_capacity,
2059 Lane::HeavyInit => {
2060 if !actor_has_capacity {
2061 false
2062 } else if let Some(permit) = heavy.try_acquire() {
2063 heavy_permit = Some(permit);
2064 true
2065 } else {
2066 false
2067 }
2068 }
2069 Lane::Mutating => !has_epoch_reader && actor_has_capacity,
2070 Lane::MaintenanceCommit => !actor.maintenance_commit_inflight && actor_has_capacity,
2073 };
2074
2075 if !runnable {
2076 return None;
2077 }
2078
2079 let queued = actor.pop_front_job(job_class, lane)?;
2080 actor.deficit -= JOB_COST;
2081 if let Some(cancellation) = queued.cancellation.as_ref() {
2082 cancellation.mark_running();
2083 }
2084 if lane == Lane::Mutating {
2085 actor.mutating_inflight = Some(RunningMutatingJob {
2086 request_id: queued.request_id.clone(),
2087 command: queued.command.clone(),
2088 started_at: Instant::now(),
2089 });
2090 }
2091 match lane {
2092 Lane::PureRead => {
2093 actor.read_inflight += 1;
2094 actor.actor_total_inflight += 1;
2095 }
2096 Lane::SerialLspStatus => {
2097 actor.lsp_inflight = true;
2098 actor.actor_total_inflight += 1;
2099 }
2100 Lane::HeavyInit => {
2101 actor.actor_total_inflight += 1;
2102 }
2103 Lane::Mutating => {
2104 actor.writer_inflight = true;
2105 actor.actor_total_inflight += 1;
2106 }
2107 Lane::MaintenanceCommit => {
2108 actor.maintenance_commit_inflight = true;
2109 actor.actor_total_inflight += 1;
2110 }
2111 }
2112
2113 Some(RunJob {
2114 root_id: root_id.clone(),
2115 job_class,
2116 lane,
2117 ctx: Arc::clone(&actor.ctx),
2118 epoch: Arc::clone(&actor.epoch),
2119 job: queued.job,
2120 completion: Some(queued.completion),
2121 request_id: queued.request_id,
2122 command: queued.command,
2123 heavy_permit,
2124 cancellation: queued.cancellation,
2125 })
2126}
2127
2128fn worker_loop(run_rx: Receiver<RunJob>, event_tx: Sender<SchedulerEvent>) {
2129 while let Ok(mut run_job) = run_rx.recv() {
2130 let response =
2131 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run_lane_job(&mut run_job)));
2132 let panicked = response.is_err();
2133 let response = match response {
2134 Ok(response) => response,
2135 Err(payload) => panic_response(
2136 run_job.request_id.clone(),
2137 &run_job.command,
2138 payload.as_ref(),
2139 ),
2140 };
2141
2142 if let Some(completion) = run_job.completion.take() {
2143 completion.send(response);
2144 }
2145 let completion = CompletionEvent {
2146 root_id: run_job.root_id,
2147 request_id: run_job.request_id,
2148 job_class: run_job.job_class,
2149 lane: run_job.lane,
2150 heavy_permit: run_job.heavy_permit.take(),
2151 panicked,
2152 };
2153 let _ = event_tx.send(SchedulerEvent::Completed(completion));
2154 }
2155}
2156
2157fn run_lane_job(run_job: &mut RunJob) -> Response {
2158 let _cancellation_ctx = CurrentJobCancellationGuard::install(run_job.cancellation.clone());
2159 let missing_request_id = run_job.request_id.clone();
2160 let job = std::mem::replace(
2161 &mut run_job.job,
2162 Box::new(move |_| {
2163 Response::error(
2164 missing_request_id,
2165 "job_missing",
2166 "executor job already taken",
2167 )
2168 }),
2169 );
2170
2171 match run_job.lane {
2172 Lane::PureRead | Lane::SerialLspStatus => {
2173 let _epoch = run_job.epoch.read();
2174 job(&run_job.ctx)
2175 }
2176 Lane::HeavyInit => {
2177 let response = job(&run_job.ctx);
2178 {
2179 let _install = run_job.epoch.write();
2180 }
2181 response
2182 }
2183 Lane::Mutating => {
2184 let _epoch = run_job.epoch.write();
2185 job(&run_job.ctx)
2186 }
2187 Lane::MaintenanceCommit => {
2188 let _epoch = run_job.epoch.read();
2192 job(&run_job.ctx)
2193 }
2194 }
2195}
2196
2197#[derive(Debug)]
2198struct HeavySemaphore {
2199 available: AtomicUsize,
2200 max: usize,
2201}
2202
2203impl HeavySemaphore {
2204 fn new(permits: usize) -> Self {
2205 Self {
2206 available: AtomicUsize::new(permits),
2207 max: permits,
2208 }
2209 }
2210
2211 fn try_acquire(self: &Arc<Self>) -> Option<HeavyPermit> {
2212 loop {
2213 let available = self.available.load(Ordering::Acquire);
2214 if available == 0 {
2215 return None;
2216 }
2217 if self
2218 .available
2219 .compare_exchange(
2220 available,
2221 available - 1,
2222 Ordering::AcqRel,
2223 Ordering::Acquire,
2224 )
2225 .is_ok()
2226 {
2227 return Some(HeavyPermit {
2228 semaphore: Arc::clone(self),
2229 });
2230 }
2231 }
2232 }
2233}
2234
2235struct HeavyPermit {
2236 semaphore: Arc<HeavySemaphore>,
2237}
2238
2239impl Drop for HeavyPermit {
2240 fn drop(&mut self) {
2241 let previous = self.semaphore.available.fetch_add(1, Ordering::Release);
2242 debug_assert!(previous < self.semaphore.max);
2243 }
2244}