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