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