1mod single_flight;
2pub(crate) mod view_publication;
3
4#[cfg(test)]
5mod tests;
6
7use std::{
8 collections::{HashMap, HashSet, VecDeque},
9 sync::{
10 atomic::{AtomicBool, AtomicU64, AtomicU8, AtomicUsize, Ordering},
11 Arc, Weak,
12 },
13 thread::{self, JoinHandle},
14 time::{Duration, Instant},
15};
16
17use crossbeam_channel::{Receiver, RecvError, RecvTimeoutError, Sender};
18use parking_lot::{Condvar, Mutex, RwLock};
19use tokio::sync::oneshot;
20
21use crate::{context::AppContext, path_identity::ProjectRootId, protocol::Response};
22
23pub use single_flight::SingleFlight;
24
25const JOB_COST: isize = 1;
26const SCHEDULER_EVENT_BATCH_CAP: usize = 64;
30pub(crate) const MAINTENANCE_QUEUE_CAP: usize = 512;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub enum Lane {
35 PureRead,
38 SerialLspStatus,
41 HeavyInit,
46 Mutating,
51 MaintenanceCommit,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub enum JobClass {
63 Interactive,
64 Maintenance,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub(crate) enum MaintenanceCoalesceKey {
70 WatcherDrain,
71 LspDrain,
72 StandingPass,
76}
77
78pub type ExecutorJob = Box<dyn FnOnce(&AppContext) -> Response + Send + 'static>;
79
80const INTERACTIVE_WRITER_PROMOTION_AGE: Duration = Duration::from_secs(6);
87const READER_STUCK_CENSUS_AGE: Duration = Duration::from_secs(60);
90const EXECUTOR_WORKER_STACK_BYTES: usize = 8 * 1024 * 1024;
94
95#[derive(Debug, Clone)]
96pub struct ExecutorConfig {
97 pub pool_size: usize,
98 pub read_cap: usize,
99 pub actor_cap: usize,
100 pub heavy_permits: usize,
101 pub drr_quantum: isize,
102}
103
104impl Default for ExecutorConfig {
105 fn default() -> Self {
106 let available = thread::available_parallelism()
107 .map(usize::from)
108 .unwrap_or(2);
109 let pool_size = available.saturating_sub(1).clamp(2, 8);
110 let actor_cap = pool_size.saturating_sub(1).clamp(1, 4);
111 let read_cap = actor_cap.clamp(1, 4);
112 let heavy_permits = pool_size.saturating_sub(1).clamp(2, 3);
113
114 Self {
115 pool_size,
116 read_cap,
117 actor_cap,
118 heavy_permits,
119 drr_quantum: 1,
120 }
121 }
122}
123
124#[derive(Debug, Clone)]
125struct EffectiveConfig {
126 pool_size: usize,
127 read_cap: usize,
128 actor_cap: usize,
129 heavy_permits: usize,
130 drr_quantum: isize,
131 deficit_cap: isize,
132 interactive_reserve: usize,
133 maintenance_cap: usize,
134}
135
136impl ExecutorConfig {
137 fn effective(&self) -> EffectiveConfig {
138 let pool_size = self.pool_size.clamp(2, 8);
139 let max_actor_cap = pool_size.saturating_sub(1).max(1);
140 let actor_cap = self.actor_cap.max(1).min(max_actor_cap);
141 let read_cap = self.read_cap.max(1).min(actor_cap).min(4);
142 let heavy_permits = self
146 .heavy_permits
147 .max(1)
148 .min(pool_size.saturating_sub(1).max(1))
149 .min(3);
150 let drr_quantum = self.drr_quantum.max(1);
151 let deficit_cap = (actor_cap.max(1) as isize) * 4;
152 let interactive_reserve = if pool_size >= 4 { 2 } else { 1 };
153 let maintenance_cap = pool_size.saturating_sub(interactive_reserve).max(1);
154
155 EffectiveConfig {
156 pool_size,
157 read_cap,
158 actor_cap,
159 heavy_permits,
160 drr_quantum,
161 deficit_cap,
162 interactive_reserve,
163 maintenance_cap,
164 }
165 }
166}
167
168pub struct CompletionHandle {
171 rx: Receiver<Response>,
172}
173
174impl CompletionHandle {
175 pub fn recv(self) -> Result<Response, RecvError> {
176 self.rx.recv()
177 }
178
179 pub fn recv_timeout(&self, timeout: Duration) -> Result<Response, RecvTimeoutError> {
180 self.rx.recv_timeout(timeout)
181 }
182
183 pub fn into_receiver(self) -> Receiver<Response> {
184 self.rx
185 }
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct DispatchClassQueueSnapshot {
190 pub queued: usize,
191 pub oldest_age_ms: Option<u64>,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct DispatchRunningSnapshot {
196 pub interactive: usize,
197 pub maintenance: usize,
198 pub completion_owned_interactive: usize,
199 pub completion_owned_maintenance: usize,
200 pub oldest_worker_owned_maintenance_age_ms: Option<u64>,
203 pub phantom_interactive: usize,
204 pub phantom_maintenance: usize,
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct DispatchLivenessSnapshot {
209 pub interactive: DispatchClassQueueSnapshot,
210 pub maintenance: DispatchClassQueueSnapshot,
211 pub running: DispatchRunningSnapshot,
212 pub phantom_running_maintenance: usize,
213 pub interactive_reserve: usize,
214 pub maintenance_cap: usize,
215}
216
217struct DispatchLivenessAtomics {
221 origin: Instant,
222 interactive_queued: AtomicUsize,
223 interactive_oldest_enqueued_ms_plus_one: AtomicU64,
224 maintenance_queued: AtomicUsize,
225 maintenance_oldest_enqueued_ms_plus_one: AtomicU64,
226 interactive_running: AtomicUsize,
227 maintenance_running: AtomicUsize,
228 completion_owned_interactive: AtomicUsize,
229 completion_owned_maintenance: AtomicUsize,
230 oldest_worker_owned_maintenance_started_ms_plus_one: AtomicU64,
231 phantom_interactive: AtomicUsize,
232 phantom_maintenance: AtomicUsize,
233}
234
235impl DispatchLivenessAtomics {
236 fn new() -> Self {
237 Self {
238 origin: Instant::now(),
239 interactive_queued: AtomicUsize::new(0),
240 interactive_oldest_enqueued_ms_plus_one: AtomicU64::new(0),
241 maintenance_queued: AtomicUsize::new(0),
242 maintenance_oldest_enqueued_ms_plus_one: AtomicU64::new(0),
243 interactive_running: AtomicUsize::new(0),
244 maintenance_running: AtomicUsize::new(0),
245 completion_owned_interactive: AtomicUsize::new(0),
246 completion_owned_maintenance: AtomicUsize::new(0),
247 oldest_worker_owned_maintenance_started_ms_plus_one: AtomicU64::new(0),
248 phantom_interactive: AtomicUsize::new(0),
249 phantom_maintenance: AtomicUsize::new(0),
250 }
251 }
252
253 fn now_ms(&self) -> u64 {
254 duration_millis_u64(self.origin.elapsed())
255 }
256
257 fn record(&self, snapshot: &DispatchLivenessSnapshot) {
258 let now_ms = self.now_ms();
259 let encode_oldest = |queued: usize, oldest_age_ms: Option<u64>| {
260 if queued == 0 {
261 0
262 } else {
263 now_ms
264 .saturating_sub(oldest_age_ms.unwrap_or(0))
265 .saturating_add(1)
266 }
267 };
268 self.interactive_oldest_enqueued_ms_plus_one.store(
269 encode_oldest(
270 snapshot.interactive.queued,
271 snapshot.interactive.oldest_age_ms,
272 ),
273 Ordering::Relaxed,
274 );
275 self.maintenance_oldest_enqueued_ms_plus_one.store(
276 encode_oldest(
277 snapshot.maintenance.queued,
278 snapshot.maintenance.oldest_age_ms,
279 ),
280 Ordering::Relaxed,
281 );
282 self.interactive_running
283 .store(snapshot.running.interactive, Ordering::Release);
284 self.maintenance_running
285 .store(snapshot.running.maintenance, Ordering::Release);
286 self.completion_owned_interactive.store(
287 snapshot.running.completion_owned_interactive,
288 Ordering::Release,
289 );
290 self.completion_owned_maintenance.store(
291 snapshot.running.completion_owned_maintenance,
292 Ordering::Release,
293 );
294 self.oldest_worker_owned_maintenance_started_ms_plus_one
295 .store(
296 encode_oldest(
297 snapshot.running.completion_owned_maintenance,
298 snapshot.running.oldest_worker_owned_maintenance_age_ms,
299 ),
300 Ordering::Relaxed,
301 );
302 self.phantom_interactive
303 .store(snapshot.running.phantom_interactive, Ordering::Release);
304 self.phantom_maintenance
305 .store(snapshot.running.phantom_maintenance, Ordering::Release);
306 self.interactive_queued
307 .store(snapshot.interactive.queued, Ordering::Release);
308 self.maintenance_queued
309 .store(snapshot.maintenance.queued, Ordering::Release);
310 }
311
312 fn snapshot(&self, config: &EffectiveConfig) -> DispatchLivenessSnapshot {
313 let now_ms = self.now_ms();
314 let decode_oldest = |queued: usize, encoded: u64| {
315 (queued > 0 && encoded > 0).then(|| now_ms.saturating_sub(encoded - 1))
316 };
317 let interactive_queued = self.interactive_queued.load(Ordering::Acquire);
318 let maintenance_queued = self.maintenance_queued.load(Ordering::Acquire);
319 DispatchLivenessSnapshot {
320 interactive: DispatchClassQueueSnapshot {
321 queued: interactive_queued,
322 oldest_age_ms: decode_oldest(
323 interactive_queued,
324 self.interactive_oldest_enqueued_ms_plus_one
325 .load(Ordering::Relaxed),
326 ),
327 },
328 maintenance: DispatchClassQueueSnapshot {
329 queued: maintenance_queued,
330 oldest_age_ms: decode_oldest(
331 maintenance_queued,
332 self.maintenance_oldest_enqueued_ms_plus_one
333 .load(Ordering::Relaxed),
334 ),
335 },
336 running: DispatchRunningSnapshot {
337 interactive: self.interactive_running.load(Ordering::Acquire),
338 maintenance: self.maintenance_running.load(Ordering::Acquire),
339 completion_owned_interactive: self
340 .completion_owned_interactive
341 .load(Ordering::Acquire),
342 completion_owned_maintenance: self
343 .completion_owned_maintenance
344 .load(Ordering::Acquire),
345 oldest_worker_owned_maintenance_age_ms: decode_oldest(
346 self.completion_owned_maintenance.load(Ordering::Acquire),
347 self.oldest_worker_owned_maintenance_started_ms_plus_one
348 .load(Ordering::Relaxed),
349 ),
350 phantom_interactive: self.phantom_interactive.load(Ordering::Acquire),
351 phantom_maintenance: self.phantom_maintenance.load(Ordering::Acquire),
352 },
353 phantom_running_maintenance: self.phantom_maintenance.load(Ordering::Acquire),
354 interactive_reserve: config.interactive_reserve,
355 maintenance_cap: config.maintenance_cap,
356 }
357 }
358}
359
360#[derive(Debug, Clone, PartialEq, Eq)]
361pub struct MutatingLaneSnapshot {
362 pub root_id: ProjectRootId,
363 pub request_id: String,
364 pub command: String,
365 pub started_age_ms: u64,
366}
367
368#[derive(Debug, Clone, PartialEq, Eq)]
370pub struct BindBlockerReaderSnapshot {
371 pub request_id: String,
372 pub command: String,
373 pub lane: Lane,
374 pub started_age_ms: u64,
375 pub execution_started: bool,
376 pub started_before_oldest_writer: bool,
377}
378
379#[derive(Debug, Clone, PartialEq, Eq)]
380pub struct BindBlockerSnapshot {
381 pub configure_state: &'static str,
382 pub configure_phase_timings: Option<String>,
383 pub blockers: Vec<String>,
384 pub oldest_queued_writer_age_ms: Option<u64>,
385 pub in_flight_readers: Vec<BindBlockerReaderSnapshot>,
386 pub reader_admissions_while_promoted_writer_waited: u64,
387}
388
389#[derive(Debug, Clone)]
400pub struct JobCancellation {
401 inner: Arc<JobCancellationInner>,
402}
403
404#[derive(Debug)]
405struct JobCancellationInner {
406 state: AtomicU8,
407 wait_lock: Mutex<()>,
408 wake: Condvar,
409}
410
411const JOB_CANCEL_STATE_PENDING: u8 = 0;
412const JOB_CANCEL_STATE_RUNNING: u8 = 1;
413const JOB_CANCEL_STATE_COMMITTED: u8 = 2;
414const JOB_CANCEL_STATE_CANCELLED: u8 = 3;
415
416impl JobCancellation {
417 pub fn new() -> Self {
418 Self {
419 inner: Arc::new(JobCancellationInner {
420 state: AtomicU8::new(JOB_CANCEL_STATE_PENDING),
421 wait_lock: Mutex::new(()),
422 wake: Condvar::new(),
423 }),
424 }
425 }
426
427 fn mark_running(&self) -> bool {
428 match self.inner.state.compare_exchange(
429 JOB_CANCEL_STATE_PENDING,
430 JOB_CANCEL_STATE_RUNNING,
431 Ordering::SeqCst,
432 Ordering::SeqCst,
433 ) {
434 Ok(_) => true,
435 Err(state) => state != JOB_CANCEL_STATE_CANCELLED,
436 }
437 }
438
439 fn try_cancel_before_running(&self) -> bool {
440 let cancelled = self
441 .inner
442 .state
443 .compare_exchange(
444 JOB_CANCEL_STATE_PENDING,
445 JOB_CANCEL_STATE_CANCELLED,
446 Ordering::SeqCst,
447 Ordering::SeqCst,
448 )
449 .is_ok();
450 if cancelled {
451 let _wait_guard = self.inner.wait_lock.lock();
452 self.inner.wake.notify_all();
453 }
454 cancelled
455 }
456
457 #[must_use]
461 pub fn try_seal_committed(&self) -> bool {
462 loop {
463 let current = self.state();
464 match current {
465 JOB_CANCEL_STATE_CANCELLED => return false,
466 JOB_CANCEL_STATE_COMMITTED => return true,
467 _ => {
468 if self
469 .inner
470 .state
471 .compare_exchange(
472 current,
473 JOB_CANCEL_STATE_COMMITTED,
474 Ordering::SeqCst,
475 Ordering::SeqCst,
476 )
477 .is_ok()
478 {
479 return true;
480 }
481 }
482 }
483 }
484 }
485
486 fn signal_cancel(&self) -> u8 {
489 let observed = loop {
490 let current = self.state();
491 match current {
492 JOB_CANCEL_STATE_COMMITTED | JOB_CANCEL_STATE_CANCELLED => break current,
493 _ => {
494 if self
495 .inner
496 .state
497 .compare_exchange(
498 current,
499 JOB_CANCEL_STATE_CANCELLED,
500 Ordering::SeqCst,
501 Ordering::SeqCst,
502 )
503 .is_ok()
504 {
505 break current;
506 }
507 }
508 }
509 };
510 if observed != JOB_CANCEL_STATE_COMMITTED {
511 let _wait_guard = self.inner.wait_lock.lock();
512 self.inner.wake.notify_all();
513 }
514 observed
515 }
516
517 fn state(&self) -> u8 {
518 self.inner.state.load(Ordering::SeqCst)
519 }
520
521 pub fn request_cancel(&self) {
525 self.signal_cancel();
526 }
527
528 pub fn cancel_requested_before_commit(&self) -> bool {
530 self.state() == JOB_CANCEL_STATE_CANCELLED
531 }
532
533 pub fn wait_for_cancellation(&self, timeout: Duration) -> bool {
538 if self.cancel_requested_before_commit() {
539 return true;
540 }
541 let mut guard = self.inner.wait_lock.lock();
542 if self.cancel_requested_before_commit() {
543 return true;
544 }
545 self.inner.wake.wait_for(&mut guard, timeout);
546 self.cancel_requested_before_commit()
547 }
548
549 fn same_token(&self, other: &JobCancellation) -> bool {
550 Arc::ptr_eq(&self.inner, &other.inner)
551 }
552}
553
554#[derive(Debug, Clone, Copy, PartialEq, Eq)]
556pub enum JobCancelOutcome {
557 QueuedRemoved,
560 RunningSignalled,
563 RunningCommitted,
566 NotFound,
568}
569
570#[derive(Debug, Clone, Copy, PartialEq, Eq)]
572pub(crate) enum PreExecutionCancelOutcome {
573 QueuedRemoved,
574 DispatchedCancelled,
575 AlreadyStarted,
576}
577
578thread_local! {
579 static CURRENT_JOB_CANCELLATION: std::cell::RefCell<Option<JobCancellation>> =
580 const { std::cell::RefCell::new(None) };
581}
582
583pub fn current_job_cancellation() -> Option<JobCancellation> {
586 CURRENT_JOB_CANCELLATION.with(|slot| slot.borrow().clone())
587}
588
589pub fn current_job_cancelled() -> bool {
590 current_job_cancellation().is_some_and(|token| token.cancel_requested_before_commit())
591}
592
593pub struct JobCancellationContextGuard {
594 previous: Option<JobCancellation>,
595}
596
597impl JobCancellationContextGuard {
598 fn install(token: Option<JobCancellation>) -> Self {
599 let previous = CURRENT_JOB_CANCELLATION.with(|slot| slot.replace(token));
600 Self { previous }
601 }
602}
603
604impl Drop for JobCancellationContextGuard {
605 fn drop(&mut self) {
606 CURRENT_JOB_CANCELLATION.with(|slot| {
607 *slot.borrow_mut() = self.previous.take();
608 });
609 }
610}
611
612pub fn install_job_cancellation(token: JobCancellation) -> JobCancellationContextGuard {
618 JobCancellationContextGuard::install(Some(token))
619}
620
621#[derive(Debug, Clone)]
622struct RunningJob {
623 root_id: ProjectRootId,
624 request_id: String,
625 command: String,
626 job_class: JobClass,
627 lane: Lane,
628 started_at: Instant,
629 execution_started: Arc<AtomicBool>,
630 completion_ownership: Weak<JobCompletionOwnership>,
631 occupancy_reported: bool,
632}
633
634#[derive(Debug, Clone)]
635pub(crate) struct LongRunningInteractiveJobSnapshot {
636 pub(crate) root_id: ProjectRootId,
637 pub(crate) request_id: String,
638 pub(crate) command: String,
639 pub(crate) lane: Lane,
640 pub(crate) age: Duration,
641 pub(crate) execution_started: bool,
642}
643
644#[derive(Debug, Clone)]
645struct RunningMutatingJob {
646 request_id: String,
647 command: String,
648 started_at: Instant,
649}
650
651pub struct Executor {
653 inner: Arc<ExecutorInner>,
654}
655
656impl Executor {
657 pub fn new() -> Self {
658 Self::with_config(ExecutorConfig::default())
659 }
660
661 pub fn with_config(config: ExecutorConfig) -> Self {
662 let effective = config.effective();
663 let state = Arc::new(Mutex::new(SchedulerState::new(effective.clone())));
664 let heavy = Arc::new(HeavySemaphore::new(effective.heavy_permits));
665 let nonrunnable_dispatches = Arc::new(AtomicUsize::new(0));
666 let completed_interactive = Arc::new(AtomicU64::new(0));
667 let completed_maintenance = Arc::new(AtomicU64::new(0));
668 let dispatch_liveness = Arc::new(DispatchLivenessAtomics::new());
669 let (run_tx, run_rx) = crossbeam_channel::unbounded();
670 let (event_tx, event_rx) = crossbeam_channel::unbounded();
671
672 let scheduler_event_tx = event_tx.clone();
673 let scheduler_state = Arc::clone(&state);
674 let scheduler_heavy = Arc::clone(&heavy);
675 let scheduler_violations = Arc::clone(&nonrunnable_dispatches);
676 let scheduler_completed_interactive = Arc::clone(&completed_interactive);
677 let scheduler_completed_maintenance = Arc::clone(&completed_maintenance);
678 let scheduler_dispatch_liveness = Arc::clone(&dispatch_liveness);
679 let scheduler_handle = thread::Builder::new()
680 .name("aft-executor-scheduler".to_string())
681 .spawn(move || {
682 scheduler_loop(
683 scheduler_state,
684 scheduler_heavy,
685 run_tx,
686 scheduler_event_tx,
687 event_rx,
688 scheduler_violations,
689 scheduler_completed_interactive,
690 scheduler_completed_maintenance,
691 scheduler_dispatch_liveness,
692 );
693 })
694 .expect("spawn AFT executor scheduler");
695
696 let mut worker_handles = Vec::with_capacity(effective.pool_size);
697 for worker_id in 0..effective.pool_size {
698 let worker_rx = run_rx.clone();
699 let handle = thread::Builder::new()
705 .name(format!("aft-executor-worker-{worker_id}"))
706 .stack_size(EXECUTOR_WORKER_STACK_BYTES)
707 .spawn(move || worker_loop(worker_rx))
708 .expect("spawn AFT executor worker");
709 worker_handles.push(handle);
710 }
711
712 Self {
713 inner: Arc::new(ExecutorInner {
714 state,
715 event_tx,
716 scheduler_handle: Mutex::new(Some(scheduler_handle)),
717 worker_handles: Mutex::new(worker_handles),
718 config: effective,
719 nonrunnable_dispatches,
720 completed_interactive,
721 completed_maintenance,
722 dispatch_liveness,
723 }),
724 }
725 }
726
727 pub fn register_actor(&self, root_id: ProjectRootId, ctx: Arc<AppContext>) -> bool {
734 let memory_root = root_id.as_path().to_path_buf();
735 let inserted = {
736 let mut state = self.inner.state.lock();
737 if state.actors.contains_key(&root_id) {
738 false
739 } else {
740 state.actor_order.push(root_id.clone());
741 state
742 .actors
743 .insert(root_id, ActorState::new(Arc::clone(&ctx)));
744 true
745 }
746 };
747 if inserted {
748 let app = ctx.app();
749 crate::root_cache::register_live_scope(&ctx.storage_dir(), &memory_root);
750 app.register_memory_context(memory_root, &ctx);
751 app.actor_root_registered();
752 }
753 self.wake_scheduler();
754 inserted
755 }
756
757 pub fn remove_actor(&self, root_id: &ProjectRootId) {
765 let removed = {
766 let mut state = self.inner.state.lock();
767 state.actor_order.retain(|actor_root| actor_root != root_id);
768 state.actors.remove(root_id)
769 };
770 if let Some(actor) = removed.as_ref() {
771 let app = actor.ctx.app();
772 crate::root_cache::unregister_live_scope(&actor.ctx.storage_dir(), root_id.as_path());
773 app.unregister_memory_context(root_id.as_path(), &actor.ctx);
774 app.actor_root_unregistered();
775 }
776 drop(removed);
777 self.wake_scheduler();
778 }
779
780 pub fn actor_is_idle(&self, root_id: &ProjectRootId) -> bool {
782 let state = self.inner.state.lock();
783 state.actors.get(root_id).is_some_and(ActorState::is_idle)
784 }
785
786 pub fn try_actor_is_idle(&self, root_id: &ProjectRootId) -> Option<bool> {
790 let state = self.inner.state.try_lock()?;
791 Some(state.actors.get(root_id).is_some_and(ActorState::is_idle))
792 }
793
794 pub fn retire_idle_actor_in_background(&self, root_id: &ProjectRootId) -> bool {
798 let removed = {
799 let mut state = self.inner.state.lock();
800 if !state.actors.get(root_id).is_some_and(ActorState::is_idle) {
801 return false;
802 }
803 state.actor_order.retain(|actor_root| actor_root != root_id);
804 state.actors.remove(root_id)
805 };
806 let Some(actor) = removed else {
807 return false;
808 };
809 let app = actor.ctx.app();
810 crate::root_cache::unregister_live_scope(&actor.ctx.storage_dir(), root_id.as_path());
811 app.unregister_memory_context(root_id.as_path(), &actor.ctx);
812 app.actor_root_unregistered();
813 std::thread::spawn(move || {
814 actor.ctx.teardown_deleted_root();
815 drop(actor);
816 });
817 self.wake_scheduler();
818 true
819 }
820
821 pub fn cancel_queued_maintenance(&self, root_id: &ProjectRootId) -> usize {
827 let cancelled = {
828 let mut state = self.inner.state.lock();
829 state
830 .actors
831 .get_mut(root_id)
832 .map(|actor| actor.maintenance.cancel_queued_jobs())
833 .unwrap_or(0)
834 };
835 if cancelled > 0 {
836 self.wake_scheduler();
837 }
838 cancelled
839 }
840
841 pub fn actor_registered(&self, root_id: &ProjectRootId) -> bool {
843 let state = self.inner.state.lock();
844 state.actors.contains_key(root_id)
845 }
846
847 pub fn actor_context(&self, root_id: &ProjectRootId) -> Option<Arc<AppContext>> {
850 let state = self.inner.state.lock();
851 state
852 .actors
853 .get(root_id)
854 .map(|actor| Arc::clone(&actor.ctx))
855 }
856
857 pub fn actor_contexts(&self) -> Vec<Arc<AppContext>> {
862 let state = self.inner.state.lock();
863 state
864 .actors
865 .values()
866 .map(|actor_state| Arc::clone(&actor_state.ctx))
867 .collect()
868 }
869
870 pub fn actor_entries(&self) -> Vec<(ProjectRootId, Arc<AppContext>)> {
872 let state = self.inner.state.lock();
873 state
874 .actors
875 .iter()
876 .map(|(root_id, actor_state)| (root_id.clone(), Arc::clone(&actor_state.ctx)))
877 .collect()
878 }
879
880 pub fn try_actor_entries(&self) -> Option<Vec<(ProjectRootId, Arc<AppContext>)>> {
884 let state = self.inner.state.try_lock()?;
885 Some(
886 state
887 .actors
888 .iter()
889 .map(|(root_id, actor_state)| (root_id.clone(), Arc::clone(&actor_state.ctx)))
890 .collect(),
891 )
892 }
893
894 pub fn actor_count(&self) -> usize {
898 self.inner.state.lock().actors.len()
899 }
900
901 pub fn try_actor_count(&self) -> Option<usize> {
903 self.inner.state.try_lock().map(|state| state.actors.len())
904 }
905
906 pub fn submit(
907 &self,
908 root_id: ProjectRootId,
909 lane: Lane,
910 request_id: String,
911 job: ExecutorJob,
912 ) -> CompletionHandle {
913 let (completion_tx, completion_rx) = crossbeam_channel::bounded(1);
914 self.submit_with_completion(
915 root_id,
916 JobClass::Interactive,
917 lane,
918 request_id,
919 job,
920 CompletionSender::Sync(completion_tx),
921 );
922 CompletionHandle { rx: completion_rx }
923 }
924
925 pub fn submit_async(
926 &self,
927 root_id: ProjectRootId,
928 lane: Lane,
929 request_id: String,
930 job: ExecutorJob,
931 ) -> oneshot::Receiver<Response> {
932 let (completion_tx, completion_rx) = oneshot::channel();
933 self.submit_with_completion(
934 root_id,
935 JobClass::Interactive,
936 lane,
937 request_id,
938 job,
939 CompletionSender::Async(completion_tx),
940 );
941 completion_rx
942 }
943
944 pub fn submit_cancellable_async(
950 &self,
951 root_id: ProjectRootId,
952 lane: Lane,
953 request_id: String,
954 job: ExecutorJob,
955 ) -> (oneshot::Receiver<Response>, JobCancellation) {
956 let cancellation = JobCancellation::new();
957 let (completion_tx, completion_rx) = oneshot::channel();
958 self.submit_with_completion_cancellable(
959 root_id,
960 JobClass::Interactive,
961 lane,
962 request_id,
963 job,
964 CompletionSender::Async(completion_tx),
965 Some(cancellation.clone()),
966 None,
967 );
968 (completion_rx, cancellation)
969 }
970
971 pub fn cancel_job(&self, root_id: &ProjectRootId, token: &JobCancellation) -> JobCancelOutcome {
977 let observed = token.signal_cancel();
984 let (outcome, settled) = {
985 let mut state = self.inner.state.lock();
986 let Some(actor) = state.actors.get_mut(root_id) else {
987 return match observed {
988 JOB_CANCEL_STATE_COMMITTED => JobCancelOutcome::RunningCommitted,
989 JOB_CANCEL_STATE_RUNNING | JOB_CANCEL_STATE_PENDING => {
990 JobCancelOutcome::RunningSignalled
991 }
992 _ => JobCancelOutcome::NotFound,
993 };
994 };
995 match actor.remove_queued_cancellable(token) {
996 Some(queued) => (JobCancelOutcome::QueuedRemoved, Some(queued)),
997 None => match observed {
998 JOB_CANCEL_STATE_COMMITTED => (JobCancelOutcome::RunningCommitted, None),
1000 JOB_CANCEL_STATE_RUNNING | JOB_CANCEL_STATE_PENDING => {
1004 (JobCancelOutcome::RunningSignalled, None)
1005 }
1006 _ => (JobCancelOutcome::NotFound, None),
1008 },
1009 }
1010 };
1011 if let Some(queued) = settled {
1012 queued.completion.send(Response::error(
1013 queued.request_id,
1014 "request_cancelled",
1015 "request cancelled before execution",
1016 ));
1017 self.wake_scheduler();
1018 }
1019 outcome
1020 }
1021
1022 pub(crate) fn cancel_job_before_execution(
1030 &self,
1031 root_id: &ProjectRootId,
1032 token: &JobCancellation,
1033 ) -> PreExecutionCancelOutcome {
1034 let (outcome, settled) = {
1035 let mut state = self.inner.state.lock();
1036 if !token.try_cancel_before_running() {
1037 return PreExecutionCancelOutcome::AlreadyStarted;
1038 }
1039 let settled = state
1040 .actors
1041 .get_mut(root_id)
1042 .and_then(|actor| actor.remove_queued_cancellable(token));
1043 let outcome = if settled.is_some() {
1044 PreExecutionCancelOutcome::QueuedRemoved
1045 } else {
1046 PreExecutionCancelOutcome::DispatchedCancelled
1047 };
1048 (outcome, settled)
1049 };
1050 if let Some(queued) = settled {
1051 queued.completion.send(Response::error(
1052 queued.request_id,
1053 "request_cancelled",
1054 "request cancelled before execution",
1055 ));
1056 self.wake_scheduler();
1057 }
1058 outcome
1059 }
1060
1061 pub fn submit_maintenance_async(
1062 &self,
1063 root_id: ProjectRootId,
1064 lane: Lane,
1065 request_id: String,
1066 job: ExecutorJob,
1067 ) -> oneshot::Receiver<Response> {
1068 self.submit_maintenance_async_with_key(root_id, lane, request_id, job, None)
1069 }
1070
1071 #[cfg(test)]
1072 fn submit_maintenance_cancellable_async(
1073 &self,
1074 root_id: ProjectRootId,
1075 lane: Lane,
1076 request_id: String,
1077 job: ExecutorJob,
1078 ) -> (oneshot::Receiver<Response>, JobCancellation) {
1079 let (completion_tx, completion_rx) = oneshot::channel();
1080 let cancellation = JobCancellation::new();
1081 self.submit_with_completion_cancellable(
1082 root_id,
1083 JobClass::Maintenance,
1084 lane,
1085 request_id,
1086 job,
1087 CompletionSender::Async(completion_tx),
1088 Some(cancellation.clone()),
1089 None,
1090 );
1091 (completion_rx, cancellation)
1092 }
1093
1094 pub(crate) fn submit_coalescable_maintenance_async(
1095 &self,
1096 root_id: ProjectRootId,
1097 lane: Lane,
1098 request_id: String,
1099 coalesce_key: MaintenanceCoalesceKey,
1100 job: ExecutorJob,
1101 ) -> oneshot::Receiver<Response> {
1102 self.submit_maintenance_async_with_key(root_id, lane, request_id, job, Some(coalesce_key))
1103 }
1104
1105 fn submit_maintenance_async_with_key(
1106 &self,
1107 root_id: ProjectRootId,
1108 lane: Lane,
1109 request_id: String,
1110 job: ExecutorJob,
1111 coalesce_key: Option<MaintenanceCoalesceKey>,
1112 ) -> oneshot::Receiver<Response> {
1113 let (completion_tx, completion_rx) = oneshot::channel();
1114 self.submit_with_completion_cancellable(
1115 root_id,
1116 JobClass::Maintenance,
1117 lane,
1118 request_id,
1119 job,
1120 CompletionSender::Async(completion_tx),
1121 None,
1122 coalesce_key,
1123 );
1124 completion_rx
1125 }
1126
1127 fn submit_with_completion(
1128 &self,
1129 root_id: ProjectRootId,
1130 job_class: JobClass,
1131 lane: Lane,
1132 request_id: String,
1133 job: ExecutorJob,
1134 completion: CompletionSender,
1135 ) {
1136 self.submit_with_completion_cancellable(
1137 root_id, job_class, lane, request_id, job, completion, None, None,
1138 );
1139 }
1140
1141 #[allow(clippy::too_many_arguments)]
1142 fn submit_with_completion_cancellable(
1143 &self,
1144 root_id: ProjectRootId,
1145 job_class: JobClass,
1146 lane: Lane,
1147 request_id: String,
1148 job: ExecutorJob,
1149 completion: CompletionSender,
1150 cancellation: Option<JobCancellation>,
1151 maintenance_coalesce_key: Option<MaintenanceCoalesceKey>,
1152 ) {
1153 let command = job_command(job_class, lane);
1154 let mut job = Some(job);
1155 let mut completion = Some(completion);
1156 let mut duplicate_victims = Vec::new();
1157
1158 let response = {
1159 let mut state = self.inner.state.lock();
1160 match state.actors.get_mut(&root_id) {
1161 Some(actor) if actor.fatal => Some(actor_fatal_response(request_id.clone())),
1162 Some(actor) => {
1163 let mut admission_error = None;
1164 if job_class == JobClass::Maintenance {
1165 if maintenance_coalesce_key
1166 .is_some_and(|key| actor.maintenance.has_maintenance_coalesce_key(key))
1167 {
1168 admission_error = Some(maintenance_cancelled_response(
1169 request_id.clone(),
1170 "maintenance drain coalesced behind an identical queued drain",
1171 ));
1172 } else {
1173 if actor.maintenance.queued_count() >= MAINTENANCE_QUEUE_CAP {
1174 duplicate_victims =
1175 actor.maintenance.remove_duplicate_maintenance_jobs();
1176 }
1177 if actor.maintenance.queued_count() >= MAINTENANCE_QUEUE_CAP {
1178 admission_error =
1179 Some(maintenance_backpressure_response(request_id.clone()));
1180 }
1181 }
1182 }
1183
1184 if admission_error.is_none() {
1185 actor.push_job(
1186 job_class,
1187 lane,
1188 QueuedJob {
1189 job: job.take().expect("executor job already queued"),
1190 completion: completion
1191 .take()
1192 .expect("executor completion already queued"),
1193 request_id: request_id.clone(),
1194 command,
1195 queued_at: Instant::now(),
1196 cancellation: cancellation.clone(),
1197 maintenance_coalesce_key,
1198 },
1199 );
1200 }
1201 admission_error
1202 }
1203 None => Some(Response::error(
1204 request_id.clone(),
1205 "actor_not_registered",
1206 "executor actor is not registered",
1207 )),
1208 }
1209 };
1210
1211 for victim in duplicate_victims {
1212 victim.completion.send(maintenance_cancelled_response(
1213 victim.request_id,
1214 "duplicate maintenance drain removed to preserve queue capacity",
1215 ));
1216 }
1217
1218 if let Some(response) = response {
1219 if let Some(completion) = completion {
1220 completion.send(response);
1221 }
1222 return;
1223 }
1224
1225 self.wake_scheduler();
1226 }
1227
1228 pub fn pool_size(&self) -> usize {
1229 self.inner.config.pool_size
1230 }
1231
1232 pub fn actor_cap(&self) -> usize {
1233 self.inner.config.actor_cap
1234 }
1235
1236 pub fn read_cap(&self) -> usize {
1237 self.inner.config.read_cap
1238 }
1239
1240 pub fn heavy_permits(&self) -> usize {
1241 self.inner.config.heavy_permits
1242 }
1243
1244 pub fn interactive_reserve(&self) -> usize {
1245 self.inner.config.interactive_reserve
1246 }
1247
1248 pub fn maintenance_cap(&self) -> usize {
1249 self.inner.config.maintenance_cap
1250 }
1251
1252 pub fn try_dispatch_liveness_snapshot(&self) -> Option<DispatchLivenessSnapshot> {
1253 Some(self.inner.dispatch_liveness.snapshot(&self.inner.config))
1254 }
1255
1256 pub fn try_mutating_lane_snapshots(&self) -> Option<Vec<MutatingLaneSnapshot>> {
1257 self.inner
1258 .state
1259 .try_lock()
1260 .map(|state| state.mutating_lane_snapshots())
1261 }
1262
1263 pub fn try_mutating_job_state_label(
1264 &self,
1265 root_id: &ProjectRootId,
1266 request_id: &str,
1267 ) -> Option<&'static str> {
1268 self.inner
1269 .state
1270 .try_lock()
1271 .map(|state| state.mutating_job_state_label(root_id, request_id))
1272 }
1273
1274 pub fn try_bind_blocker_snapshot(
1278 &self,
1279 root_id: &ProjectRootId,
1280 request_id: &str,
1281 ) -> Option<BindBlockerSnapshot> {
1282 self.inner
1283 .state
1284 .try_lock()
1285 .map(|state| state.bind_blocker_snapshot(root_id, request_id))
1286 }
1287
1288 pub(crate) fn try_take_long_running_interactive_jobs(
1292 &self,
1293 minimum_age: Duration,
1294 ) -> Option<Vec<LongRunningInteractiveJobSnapshot>> {
1295 let now = Instant::now();
1296 let mut state = self.inner.state.try_lock()?;
1297 let mut snapshots = state
1298 .running_jobs
1299 .values_mut()
1300 .filter_map(|job| {
1301 let age = now.saturating_duration_since(job.started_at);
1302 if job.job_class != JobClass::Interactive
1303 || job.occupancy_reported
1304 || age < minimum_age
1305 {
1306 return None;
1307 }
1308 job.occupancy_reported = true;
1309 Some(LongRunningInteractiveJobSnapshot {
1310 root_id: job.root_id.clone(),
1311 request_id: job.request_id.clone(),
1312 command: job.command.clone(),
1313 lane: job.lane,
1314 age,
1315 execution_started: job.execution_started.load(Ordering::Acquire),
1316 })
1317 })
1318 .collect::<Vec<_>>();
1319 snapshots.sort_by(|left, right| {
1320 left.root_id
1321 .as_path()
1322 .cmp(right.root_id.as_path())
1323 .then_with(|| left.request_id.cmp(&right.request_id))
1324 });
1325 Some(snapshots)
1326 }
1327
1328 pub fn nonrunnable_dispatch_count(&self) -> usize {
1329 self.inner.nonrunnable_dispatches.load(Ordering::Acquire)
1330 }
1331
1332 pub fn completion_counts(&self) -> (u64, u64) {
1333 (
1334 self.inner.completed_interactive.load(Ordering::Relaxed),
1335 self.inner.completed_maintenance.load(Ordering::Relaxed),
1336 )
1337 }
1338
1339 pub fn actor_is_fatal(&self, root_id: &ProjectRootId) -> bool {
1340 self.inner
1341 .state
1342 .lock()
1343 .actors
1344 .get(root_id)
1345 .map(|actor| actor.fatal)
1346 .unwrap_or(false)
1347 }
1348
1349 fn wake_scheduler(&self) {
1350 let _ = self.inner.event_tx.send(SchedulerEvent::Wake);
1351 }
1352}
1353
1354impl Default for Executor {
1355 fn default() -> Self {
1356 Self::new()
1357 }
1358}
1359
1360struct ExecutorInner {
1361 state: Arc<Mutex<SchedulerState>>,
1362 event_tx: Sender<SchedulerEvent>,
1363 scheduler_handle: Mutex<Option<JoinHandle<()>>>,
1364 worker_handles: Mutex<Vec<JoinHandle<()>>>,
1365 config: EffectiveConfig,
1366 nonrunnable_dispatches: Arc<AtomicUsize>,
1367 completed_interactive: Arc<AtomicU64>,
1368 completed_maintenance: Arc<AtomicU64>,
1369 dispatch_liveness: Arc<DispatchLivenessAtomics>,
1370}
1371
1372impl Drop for ExecutorInner {
1373 fn drop(&mut self) {
1374 let _ = self.event_tx.send(SchedulerEvent::Shutdown);
1375
1376 if let Some(handle) = self.scheduler_handle.lock().take() {
1377 let _ = handle.join();
1378 }
1379
1380 let mut workers = self.worker_handles.lock();
1381 for handle in workers.drain(..) {
1382 let _ = handle.join();
1383 }
1384 }
1385}
1386
1387struct SchedulerState {
1388 actors: HashMap<ProjectRootId, ActorState>,
1389 actor_order: Vec<ProjectRootId>,
1390 cursor: usize,
1391 idle_workers: usize,
1392 interactive_inflight: usize,
1393 maintenance_inflight: usize,
1394 config: EffectiveConfig,
1395 running_jobs: HashMap<u64, RunningJob>,
1396 next_job_id: u64,
1397}
1398
1399impl SchedulerState {
1400 fn new(config: EffectiveConfig) -> Self {
1401 Self {
1402 actors: HashMap::new(),
1403 actor_order: Vec::new(),
1404 cursor: 0,
1405 idle_workers: config.pool_size,
1406 interactive_inflight: 0,
1407 maintenance_inflight: 0,
1408 config,
1409 running_jobs: HashMap::new(),
1410 next_job_id: 1,
1411 }
1412 }
1413
1414 fn dispatch_liveness_snapshot(&self) -> DispatchLivenessSnapshot {
1415 self.assert_running_invariants();
1416 let now = Instant::now();
1417 let (completion_owned_interactive, completion_owned_maintenance) = self
1418 .running_jobs
1419 .values()
1420 .filter(|job| job.completion_ownership.strong_count() > 0)
1421 .fold((0, 0), |(interactive, maintenance), job| {
1422 match job.job_class {
1423 JobClass::Interactive => (interactive + 1, maintenance),
1424 JobClass::Maintenance => (interactive, maintenance + 1),
1425 }
1426 });
1427 let oldest_worker_owned_maintenance_age_ms = self
1428 .running_jobs
1429 .values()
1430 .filter(|job| {
1431 job.job_class == JobClass::Maintenance
1432 && job.completion_ownership.strong_count() > 0
1433 })
1434 .map(|job| duration_millis_u64(now.saturating_duration_since(job.started_at)))
1435 .max();
1436 let mut interactive = QueueSnapshotAccumulator::default();
1437 let mut maintenance = QueueSnapshotAccumulator::default();
1438 for actor in self.actors.values() {
1439 interactive.add(actor.class_queues(JobClass::Interactive), now);
1440 maintenance.add(actor.class_queues(JobClass::Maintenance), now);
1441 }
1442
1443 DispatchLivenessSnapshot {
1444 interactive: interactive.finish(),
1445 maintenance: maintenance.finish(),
1446 running: DispatchRunningSnapshot {
1447 interactive: self.interactive_inflight,
1448 maintenance: self.maintenance_inflight,
1449 completion_owned_interactive,
1450 completion_owned_maintenance,
1451 oldest_worker_owned_maintenance_age_ms,
1452 phantom_interactive: self
1453 .interactive_inflight
1454 .saturating_sub(completion_owned_interactive),
1455 phantom_maintenance: self
1456 .maintenance_inflight
1457 .saturating_sub(completion_owned_maintenance),
1458 },
1459 phantom_running_maintenance: self
1460 .maintenance_inflight
1461 .saturating_sub(completion_owned_maintenance),
1462 interactive_reserve: self.config.interactive_reserve,
1463 maintenance_cap: self.config.maintenance_cap,
1464 }
1465 }
1466
1467 fn assert_running_invariants(&self) {
1468 let interactive = self
1469 .running_jobs
1470 .values()
1471 .filter(|job| job.job_class == JobClass::Interactive)
1472 .count();
1473 let maintenance = self
1474 .running_jobs
1475 .values()
1476 .filter(|job| job.job_class == JobClass::Maintenance)
1477 .count();
1478 debug_assert_eq!(self.interactive_inflight, interactive);
1479 debug_assert_eq!(self.maintenance_inflight, maintenance);
1480 debug_assert_eq!(
1481 self.idle_workers + self.running_jobs.len(),
1482 self.config.pool_size
1483 );
1484 debug_assert!(self
1485 .running_jobs
1486 .values()
1487 .all(|job| job.completion_ownership.strong_count() > 0));
1488 }
1489
1490 fn next_job_id(&mut self) -> u64 {
1491 let job_id = self.next_job_id;
1492 self.next_job_id = self.next_job_id.wrapping_add(1).max(1);
1493 job_id
1494 }
1495
1496 fn mutating_lane_snapshots(&self) -> Vec<MutatingLaneSnapshot> {
1497 let now = Instant::now();
1498 let mut snapshots: Vec<_> = self
1499 .actors
1500 .iter()
1501 .filter_map(|(root_id, actor)| {
1502 actor
1503 .mutating_inflight
1504 .as_ref()
1505 .map(|job| MutatingLaneSnapshot {
1506 root_id: root_id.clone(),
1507 request_id: job.request_id.clone(),
1508 command: job.command.clone(),
1509 started_age_ms: duration_millis_u64(
1510 now.saturating_duration_since(job.started_at),
1511 ),
1512 })
1513 })
1514 .collect();
1515 snapshots.sort_by(|left, right| left.root_id.as_path().cmp(right.root_id.as_path()));
1516 snapshots
1517 }
1518
1519 fn mutating_job_state_label(&self, root_id: &ProjectRootId, request_id: &str) -> &'static str {
1520 let Some(actor) = self.actors.get(root_id) else {
1521 return "actor_missing";
1522 };
1523 if actor
1524 .mutating_inflight
1525 .as_ref()
1526 .is_some_and(|job| job.request_id == request_id)
1527 {
1528 return "running";
1529 }
1530 if actor.has_queued_mutating_job(request_id) {
1531 return "queued";
1532 }
1533 if actor.writer_inflight {
1534 return "blocked_by_other_mutating";
1535 }
1536 "not_found"
1537 }
1538
1539 fn bind_blocker_snapshot(
1540 &self,
1541 root_id: &ProjectRootId,
1542 request_id: &str,
1543 ) -> BindBlockerSnapshot {
1544 let configure_state = self.mutating_job_state_label(root_id, request_id);
1545 let now = Instant::now();
1546 let actor = self.actors.get(root_id);
1547 let oldest_queued_writer_at = actor.and_then(ActorState::oldest_queued_writer_at);
1548 let oldest_queued_writer_age_ms = oldest_queued_writer_at
1549 .map(|queued_at| duration_millis_u64(now.saturating_duration_since(queued_at)));
1550 let mut in_flight_readers = self
1551 .running_jobs
1552 .values()
1553 .filter(|job| {
1554 job.root_id == *root_id
1555 && matches!(job.lane, Lane::PureRead | Lane::SerialLspStatus)
1556 })
1557 .map(|job| BindBlockerReaderSnapshot {
1558 request_id: job.request_id.clone(),
1559 command: job.command.clone(),
1560 lane: job.lane,
1561 started_age_ms: duration_millis_u64(now.saturating_duration_since(job.started_at)),
1562 execution_started: job.execution_started.load(Ordering::Acquire),
1563 started_before_oldest_writer: oldest_queued_writer_at
1564 .is_some_and(|queued_at| job.started_at <= queued_at),
1565 })
1566 .collect::<Vec<_>>();
1567 in_flight_readers.sort_by(|left, right| left.request_id.cmp(&right.request_id));
1568
1569 let mut blockers = Vec::new();
1570 if let Some(actor) = actor {
1571 if configure_state == "queued" {
1572 let configure_count = actor.pending_configure_count();
1573 if configure_count > 0 {
1574 blockers.push(format!("queued_behind_configure({configure_count})"));
1575 }
1576 if actor.read_inflight > 0 || actor.lsp_inflight {
1577 let zombie_readers = in_flight_readers
1578 .iter()
1579 .filter(|reader| !reader.execution_started)
1580 .map(format_bind_blocker_reader)
1581 .collect::<Vec<_>>();
1582 let stuck_readers = in_flight_readers
1583 .iter()
1584 .filter(|reader| {
1585 reader.execution_started
1586 && reader.started_age_ms
1587 >= duration_millis_u64(READER_STUCK_CENSUS_AGE)
1588 })
1589 .map(format_bind_blocker_reader)
1590 .collect::<Vec<_>>();
1591 if !zombie_readers.is_empty() {
1592 blockers.push(format!("zombie_reader({})", zombie_readers.join("; ")));
1593 } else if !stuck_readers.is_empty() {
1594 blockers.push(format!(
1595 "waiting_on_readers_stuck({})",
1596 stuck_readers.join("; ")
1597 ));
1598 } else {
1599 blockers.push("waiting_on_readers".to_string());
1600 }
1601 }
1602 }
1603 }
1604
1605 if configure_state == "queued" {
1606 let maintenance: Vec<_> = self
1607 .running_jobs
1608 .values()
1609 .filter(|job| job.job_class == JobClass::Maintenance)
1610 .collect();
1611 if !maintenance.is_empty() {
1612 blockers.push(format!(
1613 "queued_behind_maintenance({})",
1614 format_running_jobs(&maintenance)
1615 ));
1616 }
1617 }
1618
1619 if self.idle_workers == 0 {
1620 let running: Vec<_> = self.running_jobs.values().collect();
1621 blockers.push(format!(
1622 "idle_workers==0({})",
1623 format_running_jobs(&running)
1624 ));
1625 }
1626
1627 BindBlockerSnapshot {
1628 configure_state,
1629 configure_phase_timings: actor.map(|actor| actor.ctx.configure_ack_phase_snapshot()),
1630 blockers,
1631 oldest_queued_writer_age_ms,
1632 in_flight_readers,
1633 reader_admissions_while_promoted_writer_waited: actor
1634 .map(|actor| actor.reader_admissions_while_promoted_writer_waited)
1635 .unwrap_or(0),
1636 }
1637 }
1638}
1639
1640fn is_configure_request(request_id: &str) -> bool {
1641 request_id.starts_with("subc-bind-")
1642}
1643
1644fn format_bind_blocker_reader(reader: &BindBlockerReaderSnapshot) -> String {
1645 format!(
1646 "job={} command={} lane={:?} age_ms={} execution_started={} started_before_oldest_writer={}",
1647 reader.request_id,
1648 reader.command,
1649 reader.lane,
1650 reader.started_age_ms,
1651 reader.execution_started,
1652 reader.started_before_oldest_writer
1653 )
1654}
1655
1656fn format_running_jobs(jobs: &[&RunningJob]) -> String {
1657 let now = Instant::now();
1658 let mut labels: Vec<_> = jobs
1659 .iter()
1660 .map(|job| {
1661 format!(
1662 "job={} command={} lane={:?} root={} age_ms={}",
1663 job.request_id,
1664 job.command,
1665 job.lane,
1666 job.root_id.as_path().display(),
1667 duration_millis_u64(now.saturating_duration_since(job.started_at))
1668 )
1669 })
1670 .collect();
1671 labels.sort();
1672 labels.truncate(4);
1673 labels.join("; ")
1674}
1675
1676#[derive(Default)]
1677struct QueueSnapshotAccumulator {
1678 queued: usize,
1679 oldest_age_ms: Option<u64>,
1680}
1681
1682impl QueueSnapshotAccumulator {
1683 fn add(&mut self, queues: &ClassQueues, now: Instant) {
1684 self.queued += queues.queued_count();
1685 if let Some(queued_at) = queues.oldest_queued_at() {
1686 let age_ms = duration_millis_u64(now.saturating_duration_since(queued_at));
1687 self.oldest_age_ms = Some(
1688 self.oldest_age_ms
1689 .map_or(age_ms, |oldest| oldest.max(age_ms)),
1690 );
1691 }
1692 }
1693
1694 fn finish(self) -> DispatchClassQueueSnapshot {
1695 DispatchClassQueueSnapshot {
1696 queued: self.queued,
1697 oldest_age_ms: self.oldest_age_ms,
1698 }
1699 }
1700}
1701
1702fn duration_millis_u64(duration: Duration) -> u64 {
1703 duration.as_millis().min(u128::from(u64::MAX)) as u64
1704}
1705
1706struct ActorState {
1707 ctx: Arc<AppContext>,
1708 epoch: Arc<RwLock<()>>,
1709 read_inflight: usize,
1710 lsp_inflight: bool,
1711 actor_total_inflight: usize,
1712 writer_inflight: bool,
1713 maintenance_commit_inflight: bool,
1714 mutating_inflight: Option<RunningMutatingJob>,
1715 reader_admissions_while_promoted_writer_waited: u64,
1716 deficit: isize,
1717 interactive: ClassQueues,
1718 maintenance: ClassQueues,
1719 fatal: bool,
1720}
1721
1722impl ActorState {
1723 fn new(ctx: Arc<AppContext>) -> Self {
1724 Self {
1725 ctx,
1726 epoch: Arc::new(RwLock::new(())),
1727 read_inflight: 0,
1728 lsp_inflight: false,
1729 actor_total_inflight: 0,
1730 writer_inflight: false,
1731 maintenance_commit_inflight: false,
1732 mutating_inflight: None,
1733 reader_admissions_while_promoted_writer_waited: 0,
1734 deficit: 0,
1735 interactive: ClassQueues::new(),
1736 maintenance: ClassQueues::new(),
1737 fatal: false,
1738 }
1739 }
1740
1741 fn push_job(&mut self, job_class: JobClass, lane: Lane, job: QueuedJob) {
1742 self.class_queues_mut(job_class).push_job(lane, job);
1743 }
1744
1745 fn has_queued_jobs(&self) -> bool {
1746 self.interactive.has_queued_jobs() || self.maintenance.has_queued_jobs()
1747 }
1748
1749 fn is_idle(&self) -> bool {
1750 self.actor_total_inflight == 0
1751 && !self.has_queued_jobs()
1752 && !view_publication::running_for_context(&self.ctx)
1753 }
1754
1755 fn has_queued_jobs_for(&self, job_class: JobClass) -> bool {
1756 self.class_queues(job_class).has_queued_jobs()
1757 }
1758
1759 fn front_lane(&self, job_class: JobClass) -> Option<Lane> {
1760 self.class_queues(job_class).front_lane()
1761 }
1762
1763 fn pop_front_job(&mut self, job_class: JobClass, lane: Lane) -> Option<QueuedJob> {
1764 self.class_queues_mut(job_class).pop_front_job(lane)
1765 }
1766
1767 fn higher_priority_writer_barrier_blocks(&self, job_class: JobClass) -> bool {
1768 matches!(job_class, JobClass::Maintenance)
1772 && !self.interactive.queue(Lane::Mutating).is_empty()
1773 }
1774
1775 fn class_queues(&self, job_class: JobClass) -> &ClassQueues {
1776 match job_class {
1777 JobClass::Interactive => &self.interactive,
1778 JobClass::Maintenance => &self.maintenance,
1779 }
1780 }
1781
1782 fn class_queues_mut(&mut self, job_class: JobClass) -> &mut ClassQueues {
1783 match job_class {
1784 JobClass::Interactive => &mut self.interactive,
1785 JobClass::Maintenance => &mut self.maintenance,
1786 }
1787 }
1788
1789 fn fail_queued_jobs(&mut self) {
1790 self.interactive.fail_queued_jobs();
1791 self.maintenance.fail_queued_jobs();
1792 }
1793
1794 fn has_queued_mutating_job(&self, request_id: &str) -> bool {
1795 self.interactive.has_queued_mutating_job(request_id)
1796 || self.maintenance.has_queued_mutating_job(request_id)
1797 }
1798
1799 fn remove_queued_cancellable(&mut self, token: &JobCancellation) -> Option<QueuedJob> {
1800 self.interactive
1801 .remove_cancellable(token)
1802 .or_else(|| self.maintenance.remove_cancellable(token))
1803 }
1804
1805 fn oldest_queued_writer_at(&self) -> Option<Instant> {
1806 [
1807 self.interactive.queue(Lane::Mutating).front(),
1808 self.maintenance.queue(Lane::Mutating).front(),
1809 ]
1810 .into_iter()
1811 .flatten()
1812 .map(|job| job.queued_at)
1813 .min()
1814 }
1815
1816 fn pending_configure_count(&self) -> usize {
1817 usize::from(
1818 self.mutating_inflight
1819 .as_ref()
1820 .is_some_and(|job| is_configure_request(&job.request_id)),
1821 ) + self.interactive.queued_configure_count()
1822 + self.maintenance.queued_configure_count()
1823 }
1824}
1825
1826struct ClassQueues {
1827 order: VecDeque<Lane>,
1828 pure_reads: VecDeque<QueuedJob>,
1829 lsp_status: VecDeque<QueuedJob>,
1830 heavy_init: VecDeque<QueuedJob>,
1831 mutating: VecDeque<QueuedJob>,
1832 maintenance_commit: VecDeque<QueuedJob>,
1833}
1834
1835impl ClassQueues {
1836 fn new() -> Self {
1837 Self {
1838 order: VecDeque::new(),
1839 pure_reads: VecDeque::new(),
1840 lsp_status: VecDeque::new(),
1841 heavy_init: VecDeque::new(),
1842 mutating: VecDeque::new(),
1843 maintenance_commit: VecDeque::new(),
1844 }
1845 }
1846
1847 fn push_job(&mut self, lane: Lane, job: QueuedJob) {
1848 self.order.push_back(lane);
1849 self.queue_mut(lane).push_back(job);
1850 }
1851
1852 fn has_queued_jobs(&self) -> bool {
1853 !self.order.is_empty()
1854 }
1855
1856 fn front_lane(&self) -> Option<Lane> {
1857 self.order.front().copied()
1858 }
1859
1860 fn next_interactive_lane(&self, now: Instant) -> Option<Lane> {
1866 let starved_writer = self.mutating.iter().any(|job| {
1867 now.saturating_duration_since(job.queued_at) >= INTERACTIVE_WRITER_PROMOTION_AGE
1868 });
1869 if starved_writer {
1870 return Some(Lane::Mutating);
1873 }
1874 if !self.pure_reads.is_empty() {
1875 return Some(Lane::PureRead);
1876 }
1877 self.order
1878 .iter()
1879 .copied()
1880 .find(|lane| *lane != Lane::PureRead)
1881 }
1882
1883 fn pop_front_job(&mut self, lane: Lane) -> Option<QueuedJob> {
1884 let position = self.order.iter().position(|queued| *queued == lane)?;
1888 self.order.remove(position);
1889 self.queue_mut(lane).pop_front()
1890 }
1891
1892 fn queued_count(&self) -> usize {
1893 self.order.len()
1894 }
1895
1896 fn has_maintenance_coalesce_key(&self, key: MaintenanceCoalesceKey) -> bool {
1897 [
1898 Lane::PureRead,
1899 Lane::SerialLspStatus,
1900 Lane::HeavyInit,
1901 Lane::Mutating,
1902 Lane::MaintenanceCommit,
1903 ]
1904 .into_iter()
1905 .any(|lane| {
1906 self.queue(lane)
1907 .iter()
1908 .any(|job| job.maintenance_coalesce_key == Some(key))
1909 })
1910 }
1911
1912 fn remove_duplicate_maintenance_jobs(&mut self) -> Vec<QueuedJob> {
1914 let mut seen = HashSet::new();
1915 let mut lane_positions = [0usize; 5];
1916 let mut duplicates = Vec::new();
1917
1918 for (order_position, lane) in self.order.iter().copied().enumerate() {
1919 let lane_index = lane_index(lane);
1920 let queue_position = lane_positions[lane_index];
1921 lane_positions[lane_index] += 1;
1922 let Some(key) = self
1923 .queue(lane)
1924 .get(queue_position)
1925 .and_then(|job| job.maintenance_coalesce_key)
1926 else {
1927 continue;
1928 };
1929 if !seen.insert(key) {
1930 duplicates.push((order_position, lane, queue_position));
1931 }
1932 }
1933
1934 let mut removed = Vec::with_capacity(duplicates.len());
1935 for (order_position, lane, queue_position) in duplicates.into_iter().rev() {
1936 self.order.remove(order_position);
1937 if let Some(job) = self.queue_mut(lane).remove(queue_position) {
1938 removed.push(job);
1939 }
1940 }
1941 removed
1942 }
1943
1944 fn oldest_queued_at(&self) -> Option<Instant> {
1945 self.front_lane()
1946 .and_then(|lane| self.queue(lane).front().map(|job| job.queued_at))
1947 }
1948
1949 fn fail_queued_jobs(&mut self) {
1950 self.order.clear();
1951 fail_queued_job_queue(&mut self.pure_reads);
1952 fail_queued_job_queue(&mut self.lsp_status);
1953 fail_queued_job_queue(&mut self.heavy_init);
1954 fail_queued_job_queue(&mut self.mutating);
1955 fail_queued_job_queue(&mut self.maintenance_commit);
1956 }
1957
1958 fn cancel_queued_jobs(&mut self) -> usize {
1959 self.order.clear();
1960 cancel_queued_job_queue(&mut self.pure_reads)
1961 + cancel_queued_job_queue(&mut self.lsp_status)
1962 + cancel_queued_job_queue(&mut self.heavy_init)
1963 + cancel_queued_job_queue(&mut self.mutating)
1964 + cancel_queued_job_queue(&mut self.maintenance_commit)
1965 }
1966
1967 fn has_queued_mutating_job(&self, request_id: &str) -> bool {
1968 self.mutating.iter().any(|job| job.request_id == request_id)
1969 }
1970
1971 fn remove_cancellable(&mut self, token: &JobCancellation) -> Option<QueuedJob> {
1980 for lane in [
1981 Lane::PureRead,
1982 Lane::SerialLspStatus,
1983 Lane::HeavyInit,
1984 Lane::Mutating,
1985 Lane::MaintenanceCommit,
1986 ] {
1987 let queue = self.queue_mut(lane);
1988 let position = queue.iter().position(|queued| {
1989 queued
1990 .cancellation
1991 .as_ref()
1992 .is_some_and(|candidate| candidate.same_token(token))
1993 });
1994 if let Some(position) = position {
1995 let removed = queue.remove(position);
1996 let mut occurrence = 0usize;
1997 if let Some(order_position) = self.order.iter().position(|entry| {
1998 if *entry != lane {
1999 return false;
2000 }
2001 let matched = occurrence == position;
2002 occurrence += 1;
2003 matched
2004 }) {
2005 self.order.remove(order_position);
2006 }
2007 return removed;
2008 }
2009 }
2010 None
2011 }
2012
2013 fn queued_configure_count(&self) -> usize {
2014 self.mutating
2015 .iter()
2016 .filter(|job| is_configure_request(&job.request_id))
2017 .count()
2018 }
2019
2020 fn queue(&self, lane: Lane) -> &VecDeque<QueuedJob> {
2021 match lane {
2022 Lane::PureRead => &self.pure_reads,
2023 Lane::SerialLspStatus => &self.lsp_status,
2024 Lane::HeavyInit => &self.heavy_init,
2025 Lane::Mutating => &self.mutating,
2026 Lane::MaintenanceCommit => &self.maintenance_commit,
2027 }
2028 }
2029
2030 fn queue_mut(&mut self, lane: Lane) -> &mut VecDeque<QueuedJob> {
2031 match lane {
2032 Lane::PureRead => &mut self.pure_reads,
2033 Lane::SerialLspStatus => &mut self.lsp_status,
2034 Lane::HeavyInit => &mut self.heavy_init,
2035 Lane::Mutating => &mut self.mutating,
2036 Lane::MaintenanceCommit => &mut self.maintenance_commit,
2037 }
2038 }
2039}
2040
2041struct QueuedJob {
2042 job: ExecutorJob,
2043 completion: CompletionSender,
2044 request_id: String,
2045 command: String,
2046 queued_at: Instant,
2047 cancellation: Option<JobCancellation>,
2048 maintenance_coalesce_key: Option<MaintenanceCoalesceKey>,
2049}
2050
2051fn lane_index(lane: Lane) -> usize {
2052 match lane {
2053 Lane::PureRead => 0,
2054 Lane::SerialLspStatus => 1,
2055 Lane::HeavyInit => 2,
2056 Lane::Mutating => 3,
2057 Lane::MaintenanceCommit => 4,
2058 }
2059}
2060
2061fn fail_queued_job_queue(queue: &mut VecDeque<QueuedJob>) {
2062 for queued in queue.drain(..) {
2063 queued
2064 .completion
2065 .send(actor_fatal_response(queued.request_id));
2066 }
2067}
2068
2069fn cancel_queued_job_queue(queue: &mut VecDeque<QueuedJob>) -> usize {
2070 let cancelled = queue.len();
2071 for queued in queue.drain(..) {
2072 queued.completion.send(Response::error(
2073 queued.request_id,
2074 "maintenance_cancelled",
2075 "maintenance cancelled because the actor has no bound routes",
2076 ));
2077 }
2078 cancelled
2079}
2080
2081fn job_command(job_class: JobClass, lane: Lane) -> String {
2082 format!("executor::{job_class:?}::{lane:?}")
2083}
2084
2085fn maintenance_cancelled_response(
2086 request_id: impl Into<String>,
2087 message: impl Into<String>,
2088) -> Response {
2089 Response::error(request_id, "maintenance_cancelled", message)
2090}
2091
2092fn maintenance_backpressure_response(request_id: impl Into<String>) -> Response {
2093 Response::error_with_data(
2094 request_id,
2095 "maintenance_backpressure",
2096 format!("maintenance queue reached its per-actor capacity of {MAINTENANCE_QUEUE_CAP} jobs"),
2097 serde_json::json!({
2098 "retryable": true,
2099 "queue_cap": MAINTENANCE_QUEUE_CAP,
2100 }),
2101 )
2102}
2103
2104fn actor_fatal_response(request_id: impl Into<String>) -> Response {
2105 Response::error(
2106 request_id,
2107 "actor_fatal",
2108 "executor actor is fatal after a mutating job panic",
2109 )
2110}
2111
2112fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> String {
2113 if let Some(message) = payload.downcast_ref::<&'static str>() {
2114 (*message).to_string()
2115 } else if let Some(message) = payload.downcast_ref::<String>() {
2116 message.clone()
2117 } else {
2118 "unknown panic payload".to_string()
2119 }
2120}
2121
2122fn panic_response(
2123 request_id: impl Into<String>,
2124 command: &str,
2125 payload: &(dyn std::any::Any + Send),
2126) -> Response {
2127 let panic_message = panic_payload_message(payload);
2128 Response::error(
2129 request_id,
2130 "actor_fatal",
2131 format!("command '{command}' panicked: {panic_message}"),
2132 )
2133}
2134
2135enum CompletionSender {
2136 Sync(Sender<Response>),
2137 Async(oneshot::Sender<Response>),
2138}
2139
2140impl CompletionSender {
2141 fn send(self, response: Response) {
2142 match self {
2143 Self::Sync(tx) => {
2144 let _ = tx.send(response);
2145 }
2146 Self::Async(tx) => {
2147 let _ = tx.send(response);
2148 }
2149 }
2150 }
2151}
2152
2153struct RunJob {
2154 root_id: ProjectRootId,
2155 job_class: JobClass,
2156 lane: Lane,
2157 ctx: Arc<AppContext>,
2158 epoch: Arc<RwLock<()>>,
2159 job: ExecutorJob,
2160 completion: Option<CompletionSender>,
2161 request_id: String,
2162 command: String,
2163 heavy_permit: Option<HeavyPermit>,
2164 cancellation: Option<JobCancellation>,
2165 execution_started: Arc<AtomicBool>,
2166 completion_guard: Option<JobCompletionGuard>,
2167}
2168
2169struct JobCompletionOwnership;
2170
2171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2172enum JobCompletionOutcome {
2173 Completed,
2174 Panicked,
2175 Abandoned,
2176}
2177
2178struct CompletionEvent {
2179 job_id: u64,
2180 heavy_permit: Option<HeavyPermit>,
2181 outcome: JobCompletionOutcome,
2182 ownership: Arc<JobCompletionOwnership>,
2183}
2184
2185struct JobCompletionGuard {
2186 event_tx: Sender<SchedulerEvent>,
2187 event: Option<CompletionEvent>,
2188}
2189
2190impl JobCompletionGuard {
2191 fn new(event_tx: Sender<SchedulerEvent>, event: CompletionEvent) -> Self {
2192 Self {
2193 event_tx,
2194 event: Some(event),
2195 }
2196 }
2197
2198 fn set_outcome(&mut self, outcome: JobCompletionOutcome) {
2199 if let Some(event) = self.event.as_mut() {
2200 event.outcome = outcome;
2201 }
2202 }
2203}
2204
2205impl Drop for JobCompletionGuard {
2206 fn drop(&mut self) {
2207 let Some(mut event) = self.event.take() else {
2208 return;
2209 };
2210 if std::thread::panicking() {
2211 event.outcome = JobCompletionOutcome::Panicked;
2212 }
2213 let _ = self.event_tx.send(SchedulerEvent::Completed(event));
2214 }
2215}
2216
2217enum SchedulerEvent {
2218 Wake,
2219 Completed(CompletionEvent),
2220 Shutdown,
2221}
2222
2223fn scheduler_loop(
2224 state: Arc<Mutex<SchedulerState>>,
2225 heavy: Arc<HeavySemaphore>,
2226 run_tx: Sender<RunJob>,
2227 event_tx: Sender<SchedulerEvent>,
2228 event_rx: Receiver<SchedulerEvent>,
2229 nonrunnable_dispatches: Arc<AtomicUsize>,
2230 completed_interactive: Arc<AtomicU64>,
2231 completed_maintenance: Arc<AtomicU64>,
2232 dispatch_liveness: Arc<DispatchLivenessAtomics>,
2233) {
2234 while let Ok(event) = event_rx.recv() {
2235 let shutdown;
2236 {
2237 let mut state = state.lock();
2238 shutdown = process_scheduler_event_batch(
2239 event,
2240 &event_rx,
2241 &mut state,
2242 &completed_interactive,
2243 &completed_maintenance,
2244 );
2245
2246 if !shutdown {
2247 dispatch_runnable(
2248 &mut state,
2249 &heavy,
2250 &run_tx,
2251 &event_tx,
2252 &nonrunnable_dispatches,
2253 );
2254 }
2255 dispatch_liveness.record(&state.dispatch_liveness_snapshot());
2256 }
2257
2258 if shutdown {
2259 break;
2260 }
2261 }
2262}
2263
2264fn process_scheduler_event_batch(
2265 first: SchedulerEvent,
2266 event_rx: &Receiver<SchedulerEvent>,
2267 state: &mut SchedulerState,
2268 completed_interactive: &AtomicU64,
2269 completed_maintenance: &AtomicU64,
2270) -> bool {
2271 let mut event = Some(first);
2272 for _ in 0..SCHEDULER_EVENT_BATCH_CAP {
2273 let Some(current) = event.take().or_else(|| event_rx.try_recv().ok()) else {
2274 break;
2275 };
2276 if process_scheduler_event(current, state, completed_interactive, completed_maintenance) {
2277 return true;
2278 }
2279 }
2280 false
2281}
2282
2283fn note_completed_class(
2284 job_class: JobClass,
2285 completed_interactive: &AtomicU64,
2286 completed_maintenance: &AtomicU64,
2287) {
2288 match job_class {
2289 JobClass::Interactive => {
2290 completed_interactive.fetch_add(1, Ordering::Relaxed);
2291 }
2292 JobClass::Maintenance => {
2293 completed_maintenance.fetch_add(1, Ordering::Relaxed);
2294 }
2295 }
2296}
2297
2298fn process_scheduler_event(
2299 event: SchedulerEvent,
2300 state: &mut SchedulerState,
2301 completed_interactive: &AtomicU64,
2302 completed_maintenance: &AtomicU64,
2303) -> bool {
2304 match event {
2305 SchedulerEvent::Wake => false,
2306 SchedulerEvent::Completed(event) => {
2307 if let Some(job_class) = complete_job(state, event) {
2308 note_completed_class(job_class, completed_interactive, completed_maintenance);
2309 }
2310 false
2311 }
2312 SchedulerEvent::Shutdown => true,
2313 }
2314}
2315
2316fn complete_job(state: &mut SchedulerState, event: CompletionEvent) -> Option<JobClass> {
2317 let CompletionEvent {
2318 job_id,
2319 heavy_permit,
2320 outcome,
2321 ownership,
2322 } = event;
2323 let Some(running) = state.running_jobs.remove(&job_id) else {
2324 log::error!("executor completion: unknown job_id={job_id} outcome={outcome:?}");
2325 return None;
2326 };
2327 debug_assert!(running
2328 .completion_ownership
2329 .upgrade()
2330 .is_some_and(|current| Arc::ptr_eq(¤t, &ownership)));
2331
2332 let RunningJob {
2333 root_id,
2334 request_id,
2335 job_class,
2336 lane,
2337 ..
2338 } = running;
2339 log::debug!(
2340 "executor completion: job_id={job_id} class={job_class:?} lane={lane:?} outcome={outcome:?} request_id={request_id} root={}",
2341 root_id.as_path().display()
2342 );
2343
2344 match job_class {
2345 JobClass::Interactive => {
2346 debug_assert!(state.interactive_inflight > 0);
2347 state.interactive_inflight = state.interactive_inflight.saturating_sub(1);
2348 }
2349 JobClass::Maintenance => {
2350 debug_assert!(state.maintenance_inflight > 0);
2351 state.maintenance_inflight = state.maintenance_inflight.saturating_sub(1);
2352 }
2353 }
2354
2355 if let Some(actor) = state.actors.get_mut(&root_id) {
2356 actor.actor_total_inflight = actor.actor_total_inflight.saturating_sub(1);
2357 match lane {
2358 Lane::PureRead => {
2359 actor.read_inflight = actor.read_inflight.saturating_sub(1);
2360 }
2361 Lane::SerialLspStatus => {
2362 actor.lsp_inflight = false;
2363 }
2364 Lane::HeavyInit => {}
2365 Lane::Mutating => {
2366 actor.writer_inflight = false;
2367 actor.mutating_inflight = None;
2368 }
2369 Lane::MaintenanceCommit => {
2370 actor.maintenance_commit_inflight = false;
2371 }
2372 }
2373
2374 if outcome == JobCompletionOutcome::Panicked && lane == Lane::Mutating {
2375 actor.fatal = true;
2376 actor.fail_queued_jobs();
2377 }
2378 }
2379
2380 drop(heavy_permit);
2381 drop(ownership);
2382 state.idle_workers += 1;
2383 state.assert_running_invariants();
2384 Some(job_class)
2385}
2386
2387fn dispatch_runnable(
2388 state: &mut SchedulerState,
2389 heavy: &Arc<HeavySemaphore>,
2390 run_tx: &Sender<RunJob>,
2391 event_tx: &Sender<SchedulerEvent>,
2392 nonrunnable_dispatches: &AtomicUsize,
2393) {
2394 while state.idle_workers > 0 && !state.actor_order.is_empty() {
2395 let mut made_progress = false;
2396 let mut dispatch_failed = false;
2397
2398 made_progress |= dispatch_runnable_class(
2399 state,
2400 JobClass::Interactive,
2401 heavy,
2402 run_tx,
2403 event_tx,
2404 nonrunnable_dispatches,
2405 &mut dispatch_failed,
2406 );
2407 if dispatch_failed || state.idle_workers == 0 {
2408 return;
2409 }
2410
2411 if can_dispatch_class(state, JobClass::Maintenance) {
2412 made_progress |= dispatch_runnable_class(
2413 state,
2414 JobClass::Maintenance,
2415 heavy,
2416 run_tx,
2417 event_tx,
2418 nonrunnable_dispatches,
2419 &mut dispatch_failed,
2420 );
2421 if dispatch_failed {
2422 return;
2423 }
2424 }
2425
2426 if !made_progress {
2427 break;
2428 }
2429 }
2430}
2431
2432fn dispatch_runnable_class(
2433 state: &mut SchedulerState,
2434 job_class: JobClass,
2435 heavy: &Arc<HeavySemaphore>,
2436 run_tx: &Sender<RunJob>,
2437 event_tx: &Sender<SchedulerEvent>,
2438 nonrunnable_dispatches: &AtomicUsize,
2439 dispatch_failed: &mut bool,
2440) -> bool {
2441 if !can_dispatch_class(state, job_class) || state.actor_order.is_empty() {
2442 return false;
2443 }
2444
2445 let actor_count = state.actor_order.len();
2446 let mut made_progress = false;
2447
2448 for _ in 0..actor_count {
2449 if !can_dispatch_class(state, job_class) || state.actor_order.is_empty() {
2450 break;
2451 }
2452
2453 if state.cursor >= state.actor_order.len() {
2454 state.cursor = 0;
2455 }
2456 let root_id = state.actor_order[state.cursor].clone();
2457 state.cursor = (state.cursor + 1) % state.actor_order.len();
2458
2459 let run_job = {
2460 let Some(actor) = state.actors.get_mut(&root_id) else {
2461 continue;
2462 };
2463
2464 if actor.fatal {
2465 actor.fail_queued_jobs();
2466 actor.deficit = 0;
2467 continue;
2468 }
2469
2470 if !actor.has_queued_jobs() {
2471 actor.deficit = 0;
2472 continue;
2473 }
2474
2475 if !actor.has_queued_jobs_for(job_class) {
2476 continue;
2477 }
2478
2479 actor.deficit =
2480 (actor.deficit + state.config.drr_quantum).min(state.config.deficit_cap);
2481 if actor.deficit < JOB_COST {
2482 continue;
2483 }
2484
2485 try_admit_actor(&root_id, actor, job_class, &state.config, heavy)
2486 };
2487
2488 if let Some(mut run_job) = run_job {
2489 let job_id = state.next_job_id();
2490 let ownership = Arc::new(JobCompletionOwnership);
2491 run_job.completion_guard = Some(JobCompletionGuard::new(
2492 event_tx.clone(),
2493 CompletionEvent {
2494 job_id,
2495 heavy_permit: run_job.heavy_permit.take(),
2496 outcome: JobCompletionOutcome::Abandoned,
2497 ownership: Arc::clone(&ownership),
2498 },
2499 ));
2500 let replaced = state.running_jobs.insert(
2501 job_id,
2502 RunningJob {
2503 root_id: run_job.root_id.clone(),
2504 request_id: run_job.request_id.clone(),
2505 command: run_job.command.clone(),
2506 job_class: run_job.job_class,
2507 lane: run_job.lane,
2508 started_at: Instant::now(),
2509 execution_started: Arc::clone(&run_job.execution_started),
2510 completion_ownership: Arc::downgrade(&ownership),
2511 occupancy_reported: false,
2512 },
2513 );
2514 debug_assert!(replaced.is_none());
2515 state.idle_workers -= 1;
2516 match job_class {
2517 JobClass::Interactive => state.interactive_inflight += 1,
2518 JobClass::Maintenance => state.maintenance_inflight += 1,
2519 }
2520 state.assert_running_invariants();
2521 log::debug!(
2522 "executor dispatch: job_id={job_id} class={:?} lane={:?} request_id={} root={}",
2523 run_job.job_class,
2524 run_job.lane,
2525 run_job.request_id,
2526 run_job.root_id.as_path().display()
2527 );
2528 made_progress = true;
2529 if run_tx.send(run_job).is_err() {
2530 nonrunnable_dispatches.fetch_add(1, Ordering::AcqRel);
2531 *dispatch_failed = true;
2532 return made_progress;
2533 }
2534 }
2535 }
2536
2537 made_progress
2538}
2539
2540fn can_dispatch_class(state: &SchedulerState, job_class: JobClass) -> bool {
2541 if state.idle_workers == 0 {
2542 return false;
2543 }
2544 match job_class {
2545 JobClass::Interactive => true,
2546 JobClass::Maintenance => {
2547 state.maintenance_inflight < state.config.maintenance_cap
2548 && state.idle_workers > state.config.interactive_reserve
2549 }
2550 }
2551}
2552
2553fn try_admit_actor(
2554 root_id: &ProjectRootId,
2555 actor: &mut ActorState,
2556 job_class: JobClass,
2557 config: &EffectiveConfig,
2558 heavy: &Arc<HeavySemaphore>,
2559) -> Option<RunJob> {
2560 let lane = match job_class {
2561 JobClass::Interactive => actor
2562 .class_queues(JobClass::Interactive)
2563 .next_interactive_lane(Instant::now())?,
2564 JobClass::Maintenance => actor.front_lane(job_class)?,
2565 };
2566 let mut heavy_permit = None;
2567
2568 if actor.writer_inflight || actor.higher_priority_writer_barrier_blocks(job_class) {
2569 return None;
2570 }
2571
2572 let has_epoch_reader =
2573 actor.read_inflight > 0 || actor.lsp_inflight || actor.maintenance_commit_inflight;
2574 let actor_interactive_inflight = actor
2580 .actor_total_inflight
2581 .saturating_sub(usize::from(actor.maintenance_commit_inflight));
2582 let actor_has_interactive_capacity = actor_interactive_inflight < config.actor_cap;
2583 let runnable = match lane {
2584 Lane::PureRead => actor.read_inflight < config.read_cap && actor_has_interactive_capacity,
2585 Lane::SerialLspStatus => !actor.lsp_inflight && actor_has_interactive_capacity,
2586 Lane::HeavyInit => {
2587 if !actor_has_interactive_capacity {
2588 false
2589 } else if let Some(permit) = heavy.try_acquire() {
2590 heavy_permit = Some(permit);
2591 true
2592 } else {
2593 false
2594 }
2595 }
2596 Lane::Mutating => !has_epoch_reader && actor_has_interactive_capacity,
2597 Lane::MaintenanceCommit => !actor.maintenance_commit_inflight,
2601 };
2602
2603 if !runnable {
2604 return None;
2605 }
2606
2607 let promoted_writer_waiting = actor.oldest_queued_writer_at().is_some_and(|queued_at| {
2608 Instant::now().saturating_duration_since(queued_at) >= INTERACTIVE_WRITER_PROMOTION_AGE
2609 });
2610 if promoted_writer_waiting && matches!(lane, Lane::PureRead | Lane::SerialLspStatus) {
2611 actor.reader_admissions_while_promoted_writer_waited = actor
2612 .reader_admissions_while_promoted_writer_waited
2613 .saturating_add(1);
2614 }
2615
2616 let queued = actor.pop_front_job(job_class, lane)?;
2617 actor.deficit -= JOB_COST;
2618 if lane == Lane::Mutating {
2619 actor.mutating_inflight = Some(RunningMutatingJob {
2620 request_id: queued.request_id.clone(),
2621 command: queued.command.clone(),
2622 started_at: Instant::now(),
2623 });
2624 }
2625 match lane {
2626 Lane::PureRead => {
2627 actor.read_inflight += 1;
2628 actor.actor_total_inflight += 1;
2629 }
2630 Lane::SerialLspStatus => {
2631 actor.lsp_inflight = true;
2632 actor.actor_total_inflight += 1;
2633 }
2634 Lane::HeavyInit => {
2635 actor.actor_total_inflight += 1;
2636 }
2637 Lane::Mutating => {
2638 actor.writer_inflight = true;
2639 actor.actor_total_inflight += 1;
2640 }
2641 Lane::MaintenanceCommit => {
2642 actor.maintenance_commit_inflight = true;
2643 actor.actor_total_inflight += 1;
2644 }
2645 }
2646
2647 Some(RunJob {
2648 root_id: root_id.clone(),
2649 job_class,
2650 lane,
2651 ctx: Arc::clone(&actor.ctx),
2652 epoch: Arc::clone(&actor.epoch),
2653 job: queued.job,
2654 completion: Some(queued.completion),
2655 request_id: queued.request_id,
2656 command: queued.command,
2657 heavy_permit,
2658 cancellation: queued.cancellation,
2659 execution_started: Arc::new(AtomicBool::new(false)),
2660 completion_guard: None,
2661 })
2662}
2663
2664fn worker_loop(run_rx: Receiver<RunJob>) {
2665 while let Ok(mut run_job) = run_rx.recv() {
2666 let response =
2667 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run_lane_job(&mut run_job)));
2668 let panicked = response.is_err();
2669 let response = match response {
2670 Ok(response) => response,
2671 Err(payload) => panic_response(
2672 run_job.request_id.clone(),
2673 &run_job.command,
2674 payload.as_ref(),
2675 ),
2676 };
2677
2678 if let Some(completion) = run_job.completion.take() {
2679 completion.send(response);
2680 }
2681 run_job
2682 .completion_guard
2683 .as_mut()
2684 .expect("dispatched executor job is missing its completion guard")
2685 .set_outcome(if panicked {
2686 JobCompletionOutcome::Panicked
2687 } else {
2688 JobCompletionOutcome::Completed
2689 });
2690 }
2691}
2692
2693fn run_lane_job(run_job: &mut RunJob) -> Response {
2694 let _actor_scope =
2695 view_publication::ActorScope::install(Arc::clone(&run_job.ctx), Arc::clone(&run_job.epoch));
2696 let _cancellation_ctx = JobCancellationContextGuard::install(run_job.cancellation.clone());
2697 let missing_request_id = run_job.request_id.clone();
2698 let job = std::mem::replace(
2699 &mut run_job.job,
2700 Box::new(move |_| {
2701 Response::error(
2702 missing_request_id,
2703 "job_missing",
2704 "executor job already taken",
2705 )
2706 }),
2707 );
2708
2709 let run = |job: ExecutorJob| {
2710 let can_start = run_job
2711 .cancellation
2712 .as_ref()
2713 .is_none_or(JobCancellation::mark_running);
2714 if !can_start {
2715 return Response::error(
2716 run_job.request_id.clone(),
2717 "request_cancelled",
2718 "request cancelled before execution",
2719 );
2720 }
2721 run_job.execution_started.store(true, Ordering::Release);
2722 job(&run_job.ctx)
2723 };
2724
2725 match run_job.lane {
2726 Lane::PureRead | Lane::SerialLspStatus => {
2727 if let Some(cancellation) = run_job.cancellation.as_ref() {
2728 loop {
2729 if let Some(_epoch) = run_job.epoch.try_read_for(Duration::from_millis(25)) {
2730 return run(job);
2731 }
2732 if cancellation.cancel_requested_before_commit() {
2733 return Response::error(
2734 run_job.request_id.clone(),
2735 "request_cancelled",
2736 "request cancelled before execution",
2737 );
2738 }
2739 }
2740 }
2741 let _epoch = run_job.epoch.read();
2742 run(job)
2743 }
2744 Lane::HeavyInit => run(job),
2749 Lane::Mutating => {
2750 let _epoch = run_job.epoch.write();
2751 run(job)
2752 }
2753 Lane::MaintenanceCommit => {
2754 let _epoch = run_job.epoch.read();
2758 run(job)
2759 }
2760 }
2761}
2762
2763#[derive(Debug)]
2764struct HeavySemaphore {
2765 available: AtomicUsize,
2766 max: usize,
2767}
2768
2769impl HeavySemaphore {
2770 fn new(permits: usize) -> Self {
2771 Self {
2772 available: AtomicUsize::new(permits),
2773 max: permits,
2774 }
2775 }
2776
2777 fn try_acquire(self: &Arc<Self>) -> Option<HeavyPermit> {
2778 loop {
2779 let available = self.available.load(Ordering::Acquire);
2780 if available == 0 {
2781 return None;
2782 }
2783 if self
2784 .available
2785 .compare_exchange(
2786 available,
2787 available - 1,
2788 Ordering::AcqRel,
2789 Ordering::Acquire,
2790 )
2791 .is_ok()
2792 {
2793 return Some(HeavyPermit {
2794 semaphore: Arc::clone(self),
2795 });
2796 }
2797 }
2798 }
2799}
2800
2801struct HeavyPermit {
2802 semaphore: Arc<HeavySemaphore>,
2803}
2804
2805impl Drop for HeavyPermit {
2806 fn drop(&mut self) {
2807 let previous = self.semaphore.available.fetch_add(1, Ordering::Release);
2808 debug_assert!(previous < self.semaphore.max);
2809 }
2810}