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