1mod single_flight;
2
3#[cfg(test)]
4mod tests;
5
6use std::{
7 collections::{HashMap, VecDeque},
8 sync::{
9 atomic::{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;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum Lane {
29 PureRead,
32 SerialLspStatus,
35 HeavyInit,
39 Mutating,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub enum JobClass {
48 Interactive,
49 Maintenance,
50}
51
52pub type ExecutorJob = Box<dyn FnOnce(&AppContext) -> Response + Send + 'static>;
53
54#[derive(Debug, Clone)]
55pub struct ExecutorConfig {
56 pub pool_size: usize,
57 pub read_cap: usize,
58 pub actor_cap: usize,
59 pub heavy_permits: usize,
60 pub drr_quantum: isize,
61}
62
63impl Default for ExecutorConfig {
64 fn default() -> Self {
65 let available = thread::available_parallelism()
66 .map(usize::from)
67 .unwrap_or(2);
68 let pool_size = available.saturating_sub(1).clamp(2, 8);
69 let actor_cap = pool_size.saturating_sub(1).clamp(1, 4);
70 let read_cap = actor_cap.clamp(1, 4);
71 let heavy_permits = pool_size.saturating_sub(1).clamp(2, 3);
72
73 Self {
74 pool_size,
75 read_cap,
76 actor_cap,
77 heavy_permits,
78 drr_quantum: 1,
79 }
80 }
81}
82
83#[derive(Debug, Clone)]
84struct EffectiveConfig {
85 pool_size: usize,
86 read_cap: usize,
87 actor_cap: usize,
88 heavy_permits: usize,
89 drr_quantum: isize,
90 deficit_cap: isize,
91 interactive_reserve: usize,
92 maintenance_cap: usize,
93}
94
95impl ExecutorConfig {
96 fn effective(&self) -> EffectiveConfig {
97 let pool_size = self.pool_size.clamp(2, 8);
98 let max_actor_cap = pool_size.saturating_sub(1).max(1);
99 let actor_cap = self.actor_cap.max(1).min(max_actor_cap);
100 let read_cap = self.read_cap.max(1).min(actor_cap).min(4);
101 let heavy_permits = self.heavy_permits.clamp(2, 3);
102 let drr_quantum = self.drr_quantum.max(1);
103 let deficit_cap = (actor_cap.max(1) as isize) * 4;
104 let interactive_reserve = if pool_size >= 4 { 2 } else { 1 };
105 let maintenance_cap = pool_size.saturating_sub(interactive_reserve).max(1);
106
107 EffectiveConfig {
108 pool_size,
109 read_cap,
110 actor_cap,
111 heavy_permits,
112 drr_quantum,
113 deficit_cap,
114 interactive_reserve,
115 maintenance_cap,
116 }
117 }
118}
119
120pub struct CompletionHandle {
123 rx: Receiver<Response>,
124}
125
126impl CompletionHandle {
127 pub fn recv(self) -> Result<Response, RecvError> {
128 self.rx.recv()
129 }
130
131 pub fn recv_timeout(&self, timeout: Duration) -> Result<Response, RecvTimeoutError> {
132 self.rx.recv_timeout(timeout)
133 }
134
135 pub fn into_receiver(self) -> Receiver<Response> {
136 self.rx
137 }
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct DispatchClassQueueSnapshot {
142 pub queued: usize,
143 pub oldest_age_ms: Option<u64>,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct DispatchRunningSnapshot {
148 pub interactive: usize,
149 pub maintenance: usize,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct DispatchLivenessSnapshot {
154 pub interactive: DispatchClassQueueSnapshot,
155 pub maintenance: DispatchClassQueueSnapshot,
156 pub running: DispatchRunningSnapshot,
157 pub interactive_reserve: usize,
158 pub maintenance_cap: usize,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct MutatingLaneSnapshot {
163 pub root_id: ProjectRootId,
164 pub request_id: String,
165 pub command: String,
166 pub started_age_ms: u64,
167}
168
169#[derive(Debug, Clone)]
170struct RunningMutatingJob {
171 request_id: String,
172 command: String,
173 started_at: Instant,
174}
175
176pub struct Executor {
178 inner: Arc<ExecutorInner>,
179}
180
181impl Executor {
182 pub fn new() -> Self {
183 Self::with_config(ExecutorConfig::default())
184 }
185
186 pub fn with_config(config: ExecutorConfig) -> Self {
187 let effective = config.effective();
188 let state = Arc::new(Mutex::new(SchedulerState::new(effective.clone())));
189 let heavy = Arc::new(HeavySemaphore::new(effective.heavy_permits));
190 let nonrunnable_dispatches = Arc::new(AtomicUsize::new(0));
191 let (run_tx, run_rx) = crossbeam_channel::unbounded();
192 let (event_tx, event_rx) = crossbeam_channel::unbounded();
193
194 let scheduler_state = Arc::clone(&state);
195 let scheduler_heavy = Arc::clone(&heavy);
196 let scheduler_violations = Arc::clone(&nonrunnable_dispatches);
197 let scheduler_handle = thread::Builder::new()
198 .name("aft-executor-scheduler".to_string())
199 .spawn(move || {
200 scheduler_loop(
201 scheduler_state,
202 scheduler_heavy,
203 run_tx,
204 event_rx,
205 scheduler_violations,
206 );
207 })
208 .expect("spawn AFT executor scheduler");
209
210 let mut worker_handles = Vec::with_capacity(effective.pool_size);
211 for worker_id in 0..effective.pool_size {
212 let worker_rx = run_rx.clone();
213 let worker_events = event_tx.clone();
214 let handle = thread::Builder::new()
215 .name(format!("aft-executor-worker-{worker_id}"))
216 .spawn(move || worker_loop(worker_rx, worker_events))
217 .expect("spawn AFT executor worker");
218 worker_handles.push(handle);
219 }
220
221 Self {
222 inner: Arc::new(ExecutorInner {
223 state,
224 event_tx,
225 scheduler_handle: Mutex::new(Some(scheduler_handle)),
226 worker_handles: Mutex::new(worker_handles),
227 config: effective,
228 nonrunnable_dispatches,
229 }),
230 }
231 }
232
233 pub fn register_actor(&self, root_id: ProjectRootId, ctx: Arc<AppContext>) -> bool {
240 let inserted = {
241 let mut state = self.inner.state.lock();
242 if state.actors.contains_key(&root_id) {
243 false
244 } else {
245 state.actor_order.push(root_id.clone());
246 state.actors.insert(root_id, ActorState::new(ctx));
247 true
248 }
249 };
250 self.wake_scheduler();
251 inserted
252 }
253
254 pub fn remove_actor(&self, root_id: &ProjectRootId) {
262 let removed = {
263 let mut state = self.inner.state.lock();
264 state.actor_order.retain(|actor_root| actor_root != root_id);
265 state.actors.remove(root_id)
266 };
267 drop(removed);
268 self.wake_scheduler();
269 }
270
271 pub fn actor_contexts(&self) -> Vec<Arc<AppContext>> {
276 let state = self.inner.state.lock();
277 state
278 .actors
279 .values()
280 .map(|actor_state| Arc::clone(&actor_state.ctx))
281 .collect()
282 }
283
284 pub fn actor_entries(&self) -> Vec<(ProjectRootId, Arc<AppContext>)> {
286 let state = self.inner.state.lock();
287 state
288 .actors
289 .iter()
290 .map(|(root_id, actor_state)| (root_id.clone(), Arc::clone(&actor_state.ctx)))
291 .collect()
292 }
293
294 pub fn try_actor_entries(&self) -> Option<Vec<(ProjectRootId, Arc<AppContext>)>> {
298 let state = self.inner.state.try_lock()?;
299 Some(
300 state
301 .actors
302 .iter()
303 .map(|(root_id, actor_state)| (root_id.clone(), Arc::clone(&actor_state.ctx)))
304 .collect(),
305 )
306 }
307
308 pub fn submit(
309 &self,
310 root_id: ProjectRootId,
311 lane: Lane,
312 request_id: String,
313 job: ExecutorJob,
314 ) -> CompletionHandle {
315 let (completion_tx, completion_rx) = crossbeam_channel::bounded(1);
316 self.submit_with_completion(
317 root_id,
318 JobClass::Interactive,
319 lane,
320 request_id,
321 job,
322 CompletionSender::Sync(completion_tx),
323 );
324 CompletionHandle { rx: completion_rx }
325 }
326
327 pub fn submit_async(
328 &self,
329 root_id: ProjectRootId,
330 lane: Lane,
331 request_id: String,
332 job: ExecutorJob,
333 ) -> oneshot::Receiver<Response> {
334 let (completion_tx, completion_rx) = oneshot::channel();
335 self.submit_with_completion(
336 root_id,
337 JobClass::Interactive,
338 lane,
339 request_id,
340 job,
341 CompletionSender::Async(completion_tx),
342 );
343 completion_rx
344 }
345
346 pub fn submit_maintenance_async(
347 &self,
348 root_id: ProjectRootId,
349 lane: Lane,
350 request_id: String,
351 job: ExecutorJob,
352 ) -> oneshot::Receiver<Response> {
353 let (completion_tx, completion_rx) = oneshot::channel();
354 self.submit_with_completion(
355 root_id,
356 JobClass::Maintenance,
357 lane,
358 request_id,
359 job,
360 CompletionSender::Async(completion_tx),
361 );
362 completion_rx
363 }
364
365 fn submit_with_completion(
366 &self,
367 root_id: ProjectRootId,
368 job_class: JobClass,
369 lane: Lane,
370 request_id: String,
371 job: ExecutorJob,
372 completion: CompletionSender,
373 ) {
374 let command = job_command(job_class, lane);
375 let mut job = Some(job);
376 let mut completion = Some(completion);
377
378 let response = {
379 let mut state = self.inner.state.lock();
380 match state.actors.get_mut(&root_id) {
381 Some(actor) if actor.fatal => Some(actor_fatal_response(request_id.clone())),
382 Some(actor) => {
383 actor.push_job(
384 job_class,
385 lane,
386 QueuedJob {
387 job: job.take().expect("executor job already queued"),
388 completion: completion
389 .take()
390 .expect("executor completion already queued"),
391 request_id: request_id.clone(),
392 command,
393 queued_at: Instant::now(),
394 },
395 );
396 None
397 }
398 None => Some(Response::error(
399 request_id.clone(),
400 "actor_not_registered",
401 "executor actor is not registered",
402 )),
403 }
404 };
405
406 if let Some(response) = response {
407 if let Some(completion) = completion {
408 completion.send(response);
409 }
410 return;
411 }
412
413 self.wake_scheduler();
414 }
415
416 pub fn pool_size(&self) -> usize {
417 self.inner.config.pool_size
418 }
419
420 pub fn actor_cap(&self) -> usize {
421 self.inner.config.actor_cap
422 }
423
424 pub fn read_cap(&self) -> usize {
425 self.inner.config.read_cap
426 }
427
428 pub fn heavy_permits(&self) -> usize {
429 self.inner.config.heavy_permits
430 }
431
432 pub fn interactive_reserve(&self) -> usize {
433 self.inner.config.interactive_reserve
434 }
435
436 pub fn maintenance_cap(&self) -> usize {
437 self.inner.config.maintenance_cap
438 }
439
440 pub fn try_dispatch_liveness_snapshot(&self) -> Option<DispatchLivenessSnapshot> {
441 self.inner
442 .state
443 .try_lock()
444 .map(|state| state.dispatch_liveness_snapshot())
445 }
446
447 pub fn try_mutating_lane_snapshots(&self) -> Option<Vec<MutatingLaneSnapshot>> {
448 self.inner
449 .state
450 .try_lock()
451 .map(|state| state.mutating_lane_snapshots())
452 }
453
454 pub fn try_mutating_job_state_label(
455 &self,
456 root_id: &ProjectRootId,
457 request_id: &str,
458 ) -> Option<&'static str> {
459 self.inner
460 .state
461 .try_lock()
462 .map(|state| state.mutating_job_state_label(root_id, request_id))
463 }
464
465 pub fn nonrunnable_dispatch_count(&self) -> usize {
466 self.inner.nonrunnable_dispatches.load(Ordering::Acquire)
467 }
468
469 pub fn actor_is_fatal(&self, root_id: &ProjectRootId) -> bool {
470 self.inner
471 .state
472 .lock()
473 .actors
474 .get(root_id)
475 .map(|actor| actor.fatal)
476 .unwrap_or(false)
477 }
478
479 fn wake_scheduler(&self) {
480 let _ = self.inner.event_tx.send(SchedulerEvent::Wake);
481 }
482}
483
484impl Default for Executor {
485 fn default() -> Self {
486 Self::new()
487 }
488}
489
490struct ExecutorInner {
491 state: Arc<Mutex<SchedulerState>>,
492 event_tx: Sender<SchedulerEvent>,
493 scheduler_handle: Mutex<Option<JoinHandle<()>>>,
494 worker_handles: Mutex<Vec<JoinHandle<()>>>,
495 config: EffectiveConfig,
496 nonrunnable_dispatches: Arc<AtomicUsize>,
497}
498
499impl Drop for ExecutorInner {
500 fn drop(&mut self) {
501 let _ = self.event_tx.send(SchedulerEvent::Shutdown);
502
503 if let Some(handle) = self.scheduler_handle.lock().take() {
504 let _ = handle.join();
505 }
506
507 let mut workers = self.worker_handles.lock();
508 for handle in workers.drain(..) {
509 let _ = handle.join();
510 }
511 }
512}
513
514struct SchedulerState {
515 actors: HashMap<ProjectRootId, ActorState>,
516 actor_order: Vec<ProjectRootId>,
517 cursor: usize,
518 idle_workers: usize,
519 interactive_inflight: usize,
520 maintenance_inflight: usize,
521 config: EffectiveConfig,
522}
523
524impl SchedulerState {
525 fn new(config: EffectiveConfig) -> Self {
526 Self {
527 actors: HashMap::new(),
528 actor_order: Vec::new(),
529 cursor: 0,
530 idle_workers: config.pool_size,
531 interactive_inflight: 0,
532 maintenance_inflight: 0,
533 config,
534 }
535 }
536
537 fn dispatch_liveness_snapshot(&self) -> DispatchLivenessSnapshot {
538 let now = Instant::now();
539 let mut interactive = QueueSnapshotAccumulator::default();
540 let mut maintenance = QueueSnapshotAccumulator::default();
541 for actor in self.actors.values() {
542 interactive.add(actor.class_queues(JobClass::Interactive), now);
543 maintenance.add(actor.class_queues(JobClass::Maintenance), now);
544 }
545
546 DispatchLivenessSnapshot {
547 interactive: interactive.finish(),
548 maintenance: maintenance.finish(),
549 running: DispatchRunningSnapshot {
550 interactive: self.interactive_inflight,
551 maintenance: self.maintenance_inflight,
552 },
553 interactive_reserve: self.config.interactive_reserve,
554 maintenance_cap: self.config.maintenance_cap,
555 }
556 }
557
558 fn mutating_lane_snapshots(&self) -> Vec<MutatingLaneSnapshot> {
559 let now = Instant::now();
560 let mut snapshots: Vec<_> = self
561 .actors
562 .iter()
563 .filter_map(|(root_id, actor)| {
564 actor
565 .mutating_inflight
566 .as_ref()
567 .map(|job| MutatingLaneSnapshot {
568 root_id: root_id.clone(),
569 request_id: job.request_id.clone(),
570 command: job.command.clone(),
571 started_age_ms: duration_millis_u64(
572 now.saturating_duration_since(job.started_at),
573 ),
574 })
575 })
576 .collect();
577 snapshots.sort_by(|left, right| left.root_id.as_path().cmp(right.root_id.as_path()));
578 snapshots
579 }
580
581 fn mutating_job_state_label(&self, root_id: &ProjectRootId, request_id: &str) -> &'static str {
582 let Some(actor) = self.actors.get(root_id) else {
583 return "actor_missing";
584 };
585 if actor
586 .mutating_inflight
587 .as_ref()
588 .is_some_and(|job| job.request_id == request_id)
589 {
590 return "running";
591 }
592 if actor.has_queued_mutating_job(request_id) {
593 return "queued";
594 }
595 if actor.writer_inflight {
596 return "blocked_by_other_mutating";
597 }
598 "not_found"
599 }
600}
601
602#[derive(Default)]
603struct QueueSnapshotAccumulator {
604 queued: usize,
605 oldest_age_ms: Option<u64>,
606}
607
608impl QueueSnapshotAccumulator {
609 fn add(&mut self, queues: &ClassQueues, now: Instant) {
610 self.queued += queues.queued_count();
611 if let Some(queued_at) = queues.oldest_queued_at() {
612 let age_ms = duration_millis_u64(now.saturating_duration_since(queued_at));
613 self.oldest_age_ms = Some(
614 self.oldest_age_ms
615 .map_or(age_ms, |oldest| oldest.max(age_ms)),
616 );
617 }
618 }
619
620 fn finish(self) -> DispatchClassQueueSnapshot {
621 DispatchClassQueueSnapshot {
622 queued: self.queued,
623 oldest_age_ms: self.oldest_age_ms,
624 }
625 }
626}
627
628fn duration_millis_u64(duration: Duration) -> u64 {
629 duration.as_millis().min(u128::from(u64::MAX)) as u64
630}
631
632struct ActorState {
633 ctx: Arc<AppContext>,
634 epoch: Arc<RwLock<()>>,
635 read_inflight: usize,
636 lsp_inflight: bool,
637 actor_total_inflight: usize,
638 writer_inflight: bool,
639 mutating_inflight: Option<RunningMutatingJob>,
640 deficit: isize,
641 interactive: ClassQueues,
642 maintenance: ClassQueues,
643 fatal: bool,
644}
645
646impl ActorState {
647 fn new(ctx: Arc<AppContext>) -> Self {
648 Self {
649 ctx,
650 epoch: Arc::new(RwLock::new(())),
651 read_inflight: 0,
652 lsp_inflight: false,
653 actor_total_inflight: 0,
654 writer_inflight: false,
655 mutating_inflight: None,
656 deficit: 0,
657 interactive: ClassQueues::new(),
658 maintenance: ClassQueues::new(),
659 fatal: false,
660 }
661 }
662
663 fn push_job(&mut self, job_class: JobClass, lane: Lane, job: QueuedJob) {
664 self.class_queues_mut(job_class).push_job(lane, job);
665 }
666
667 fn has_queued_jobs(&self) -> bool {
668 self.interactive.has_queued_jobs() || self.maintenance.has_queued_jobs()
669 }
670
671 fn has_queued_jobs_for(&self, job_class: JobClass) -> bool {
672 self.class_queues(job_class).has_queued_jobs()
673 }
674
675 fn front_lane(&self, job_class: JobClass) -> Option<Lane> {
676 self.class_queues(job_class).front_lane()
677 }
678
679 fn pop_front_job(&mut self, job_class: JobClass, lane: Lane) -> Option<QueuedJob> {
680 self.class_queues_mut(job_class).pop_front_job(lane)
681 }
682
683 fn higher_priority_writer_barrier_blocks(&self, job_class: JobClass) -> bool {
684 matches!(job_class, JobClass::Maintenance)
685 && self.front_lane(JobClass::Interactive) == Some(Lane::Mutating)
686 }
687
688 fn class_queues(&self, job_class: JobClass) -> &ClassQueues {
689 match job_class {
690 JobClass::Interactive => &self.interactive,
691 JobClass::Maintenance => &self.maintenance,
692 }
693 }
694
695 fn class_queues_mut(&mut self, job_class: JobClass) -> &mut ClassQueues {
696 match job_class {
697 JobClass::Interactive => &mut self.interactive,
698 JobClass::Maintenance => &mut self.maintenance,
699 }
700 }
701
702 fn fail_queued_jobs(&mut self) {
703 self.interactive.fail_queued_jobs();
704 self.maintenance.fail_queued_jobs();
705 }
706
707 fn has_queued_mutating_job(&self, request_id: &str) -> bool {
708 self.interactive.has_queued_mutating_job(request_id)
709 || self.maintenance.has_queued_mutating_job(request_id)
710 }
711}
712
713struct ClassQueues {
714 order: VecDeque<Lane>,
715 pure_reads: VecDeque<QueuedJob>,
716 lsp_status: VecDeque<QueuedJob>,
717 heavy_init: VecDeque<QueuedJob>,
718 mutating: VecDeque<QueuedJob>,
719}
720
721impl ClassQueues {
722 fn new() -> Self {
723 Self {
724 order: VecDeque::new(),
725 pure_reads: VecDeque::new(),
726 lsp_status: VecDeque::new(),
727 heavy_init: VecDeque::new(),
728 mutating: VecDeque::new(),
729 }
730 }
731
732 fn push_job(&mut self, lane: Lane, job: QueuedJob) {
733 self.order.push_back(lane);
734 self.queue_mut(lane).push_back(job);
735 }
736
737 fn has_queued_jobs(&self) -> bool {
738 !self.order.is_empty()
739 }
740
741 fn front_lane(&self) -> Option<Lane> {
742 self.order.front().copied()
743 }
744
745 fn pop_front_job(&mut self, lane: Lane) -> Option<QueuedJob> {
746 let order_lane = self.order.pop_front()?;
747 debug_assert_eq!(order_lane, lane);
748 self.queue_mut(lane).pop_front()
749 }
750
751 fn queued_count(&self) -> usize {
752 self.order.len()
753 }
754
755 fn oldest_queued_at(&self) -> Option<Instant> {
756 self.front_lane()
757 .and_then(|lane| self.queue(lane).front().map(|job| job.queued_at))
758 }
759
760 fn fail_queued_jobs(&mut self) {
761 self.order.clear();
762 fail_queued_job_queue(&mut self.pure_reads);
763 fail_queued_job_queue(&mut self.lsp_status);
764 fail_queued_job_queue(&mut self.heavy_init);
765 fail_queued_job_queue(&mut self.mutating);
766 }
767
768 fn has_queued_mutating_job(&self, request_id: &str) -> bool {
769 self.mutating.iter().any(|job| job.request_id == request_id)
770 }
771
772 fn queue(&self, lane: Lane) -> &VecDeque<QueuedJob> {
773 match lane {
774 Lane::PureRead => &self.pure_reads,
775 Lane::SerialLspStatus => &self.lsp_status,
776 Lane::HeavyInit => &self.heavy_init,
777 Lane::Mutating => &self.mutating,
778 }
779 }
780
781 fn queue_mut(&mut self, lane: Lane) -> &mut VecDeque<QueuedJob> {
782 match lane {
783 Lane::PureRead => &mut self.pure_reads,
784 Lane::SerialLspStatus => &mut self.lsp_status,
785 Lane::HeavyInit => &mut self.heavy_init,
786 Lane::Mutating => &mut self.mutating,
787 }
788 }
789}
790
791struct QueuedJob {
792 job: ExecutorJob,
793 completion: CompletionSender,
794 request_id: String,
795 command: String,
796 queued_at: Instant,
797}
798
799fn fail_queued_job_queue(queue: &mut VecDeque<QueuedJob>) {
800 for queued in queue.drain(..) {
801 queued
802 .completion
803 .send(actor_fatal_response(queued.request_id));
804 }
805}
806
807fn job_command(job_class: JobClass, lane: Lane) -> String {
808 format!("executor::{job_class:?}::{lane:?}")
809}
810
811fn actor_fatal_response(request_id: impl Into<String>) -> Response {
812 Response::error(
813 request_id,
814 "actor_fatal",
815 "executor actor is fatal after a mutating job panic",
816 )
817}
818
819fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> String {
820 if let Some(message) = payload.downcast_ref::<&'static str>() {
821 (*message).to_string()
822 } else if let Some(message) = payload.downcast_ref::<String>() {
823 message.clone()
824 } else {
825 "unknown panic payload".to_string()
826 }
827}
828
829fn panic_response(
830 request_id: impl Into<String>,
831 command: &str,
832 payload: &(dyn std::any::Any + Send),
833) -> Response {
834 let panic_message = panic_payload_message(payload);
835 Response::error(
836 request_id,
837 "actor_fatal",
838 format!("command '{command}' panicked: {panic_message}"),
839 )
840}
841
842enum CompletionSender {
843 Sync(Sender<Response>),
844 Async(oneshot::Sender<Response>),
845}
846
847impl CompletionSender {
848 fn send(self, response: Response) {
849 match self {
850 Self::Sync(tx) => {
851 let _ = tx.send(response);
852 }
853 Self::Async(tx) => {
854 let _ = tx.send(response);
855 }
856 }
857 }
858}
859
860struct RunJob {
861 root_id: ProjectRootId,
862 job_class: JobClass,
863 lane: Lane,
864 ctx: Arc<AppContext>,
865 epoch: Arc<RwLock<()>>,
866 job: ExecutorJob,
867 completion: Option<CompletionSender>,
868 request_id: String,
869 command: String,
870 heavy_permit: Option<HeavyPermit>,
871}
872
873struct CompletionEvent {
874 root_id: ProjectRootId,
875 job_class: JobClass,
876 lane: Lane,
877 heavy_permit: Option<HeavyPermit>,
878 panicked: bool,
879}
880
881enum SchedulerEvent {
882 Wake,
883 Completed(CompletionEvent),
884 Shutdown,
885}
886
887fn scheduler_loop(
888 state: Arc<Mutex<SchedulerState>>,
889 heavy: Arc<HeavySemaphore>,
890 run_tx: Sender<RunJob>,
891 event_rx: Receiver<SchedulerEvent>,
892 nonrunnable_dispatches: Arc<AtomicUsize>,
893) {
894 while let Ok(event) = event_rx.recv() {
895 let mut shutdown = false;
896 {
897 let mut state = state.lock();
898 shutdown |= process_scheduler_event(event, &mut state);
899 while !shutdown {
900 match event_rx.try_recv() {
901 Ok(event) => shutdown |= process_scheduler_event(event, &mut state),
902 Err(_) => break,
903 }
904 }
905
906 if !shutdown {
907 dispatch_runnable(&mut state, &heavy, &run_tx, &nonrunnable_dispatches);
908 }
909 }
910
911 if shutdown {
912 break;
913 }
914 }
915}
916
917fn process_scheduler_event(event: SchedulerEvent, state: &mut SchedulerState) -> bool {
918 match event {
919 SchedulerEvent::Wake => false,
920 SchedulerEvent::Completed(event) => {
921 complete_job(state, event);
922 false
923 }
924 SchedulerEvent::Shutdown => true,
925 }
926}
927
928fn complete_job(state: &mut SchedulerState, event: CompletionEvent) {
929 let CompletionEvent {
930 root_id,
931 job_class,
932 lane,
933 heavy_permit,
934 panicked,
935 } = event;
936
937 match job_class {
938 JobClass::Interactive => {
939 state.interactive_inflight = state.interactive_inflight.saturating_sub(1);
940 }
941 JobClass::Maintenance => {
942 state.maintenance_inflight = state.maintenance_inflight.saturating_sub(1);
943 }
944 }
945
946 if let Some(actor) = state.actors.get_mut(&root_id) {
947 actor.actor_total_inflight = actor.actor_total_inflight.saturating_sub(1);
948 match lane {
949 Lane::PureRead => {
950 actor.read_inflight = actor.read_inflight.saturating_sub(1);
951 }
952 Lane::SerialLspStatus => {
953 actor.lsp_inflight = false;
954 }
955 Lane::HeavyInit => {}
956 Lane::Mutating => {
957 actor.writer_inflight = false;
958 actor.mutating_inflight = None;
959 }
960 }
961
962 if panicked && lane == Lane::Mutating {
963 actor.fatal = true;
964 actor.fail_queued_jobs();
965 }
966 }
967
968 drop(heavy_permit);
969 state.idle_workers += 1;
970}
971
972fn dispatch_runnable(
973 state: &mut SchedulerState,
974 heavy: &Arc<HeavySemaphore>,
975 run_tx: &Sender<RunJob>,
976 nonrunnable_dispatches: &AtomicUsize,
977) {
978 while state.idle_workers > 0 && !state.actor_order.is_empty() {
979 let mut made_progress = false;
980 let mut dispatch_failed = false;
981
982 made_progress |= dispatch_runnable_class(
983 state,
984 JobClass::Interactive,
985 heavy,
986 run_tx,
987 nonrunnable_dispatches,
988 &mut dispatch_failed,
989 );
990 if dispatch_failed || state.idle_workers == 0 {
991 return;
992 }
993
994 if can_dispatch_class(state, JobClass::Maintenance) {
995 made_progress |= dispatch_runnable_class(
996 state,
997 JobClass::Maintenance,
998 heavy,
999 run_tx,
1000 nonrunnable_dispatches,
1001 &mut dispatch_failed,
1002 );
1003 if dispatch_failed {
1004 return;
1005 }
1006 }
1007
1008 if !made_progress {
1009 break;
1010 }
1011 }
1012}
1013
1014fn dispatch_runnable_class(
1015 state: &mut SchedulerState,
1016 job_class: JobClass,
1017 heavy: &Arc<HeavySemaphore>,
1018 run_tx: &Sender<RunJob>,
1019 nonrunnable_dispatches: &AtomicUsize,
1020 dispatch_failed: &mut bool,
1021) -> bool {
1022 if !can_dispatch_class(state, job_class) || state.actor_order.is_empty() {
1023 return false;
1024 }
1025
1026 let actor_count = state.actor_order.len();
1027 let mut made_progress = false;
1028
1029 for _ in 0..actor_count {
1030 if !can_dispatch_class(state, job_class) || state.actor_order.is_empty() {
1031 break;
1032 }
1033
1034 if state.cursor >= state.actor_order.len() {
1035 state.cursor = 0;
1036 }
1037 let root_id = state.actor_order[state.cursor].clone();
1038 state.cursor = (state.cursor + 1) % state.actor_order.len();
1039
1040 let run_job = {
1041 let Some(actor) = state.actors.get_mut(&root_id) else {
1042 continue;
1043 };
1044
1045 if actor.fatal {
1046 actor.fail_queued_jobs();
1047 actor.deficit = 0;
1048 continue;
1049 }
1050
1051 if !actor.has_queued_jobs() {
1052 actor.deficit = 0;
1053 continue;
1054 }
1055
1056 if !actor.has_queued_jobs_for(job_class) {
1057 continue;
1058 }
1059
1060 actor.deficit =
1061 (actor.deficit + state.config.drr_quantum).min(state.config.deficit_cap);
1062 if actor.deficit < JOB_COST {
1063 continue;
1064 }
1065
1066 try_admit_actor(&root_id, actor, job_class, &state.config, heavy)
1067 };
1068
1069 if let Some(run_job) = run_job {
1070 state.idle_workers -= 1;
1071 match job_class {
1072 JobClass::Interactive => state.interactive_inflight += 1,
1073 JobClass::Maintenance => state.maintenance_inflight += 1,
1074 }
1075 made_progress = true;
1076 if run_tx.send(run_job).is_err() {
1077 nonrunnable_dispatches.fetch_add(1, Ordering::AcqRel);
1078 *dispatch_failed = true;
1079 return made_progress;
1080 }
1081 }
1082 }
1083
1084 made_progress
1085}
1086
1087fn can_dispatch_class(state: &SchedulerState, job_class: JobClass) -> bool {
1088 if state.idle_workers == 0 {
1089 return false;
1090 }
1091 match job_class {
1092 JobClass::Interactive => true,
1093 JobClass::Maintenance => {
1094 state.maintenance_inflight < state.config.maintenance_cap
1095 && state.idle_workers > state.config.interactive_reserve
1096 }
1097 }
1098}
1099
1100fn try_admit_actor(
1101 root_id: &ProjectRootId,
1102 actor: &mut ActorState,
1103 job_class: JobClass,
1104 config: &EffectiveConfig,
1105 heavy: &Arc<HeavySemaphore>,
1106) -> Option<RunJob> {
1107 let lane = actor.front_lane(job_class)?;
1108 let mut heavy_permit = None;
1109
1110 if actor.writer_inflight || actor.higher_priority_writer_barrier_blocks(job_class) {
1111 return None;
1112 }
1113
1114 let has_epoch_reader = actor.read_inflight > 0 || actor.lsp_inflight;
1115 let actor_has_capacity = actor.actor_total_inflight < config.actor_cap;
1116 let runnable = match lane {
1117 Lane::PureRead => actor.read_inflight < config.read_cap && actor_has_capacity,
1118 Lane::SerialLspStatus => !actor.lsp_inflight && actor_has_capacity,
1119 Lane::HeavyInit => {
1120 if !actor_has_capacity {
1121 false
1122 } else if let Some(permit) = heavy.try_acquire() {
1123 heavy_permit = Some(permit);
1124 true
1125 } else {
1126 false
1127 }
1128 }
1129 Lane::Mutating => !has_epoch_reader && actor_has_capacity,
1130 };
1131
1132 if !runnable {
1133 return None;
1134 }
1135
1136 let queued = actor.pop_front_job(job_class, lane)?;
1137 actor.deficit -= JOB_COST;
1138 if lane == Lane::Mutating {
1139 actor.mutating_inflight = Some(RunningMutatingJob {
1140 request_id: queued.request_id.clone(),
1141 command: queued.command.clone(),
1142 started_at: Instant::now(),
1143 });
1144 }
1145 match lane {
1146 Lane::PureRead => {
1147 actor.read_inflight += 1;
1148 actor.actor_total_inflight += 1;
1149 }
1150 Lane::SerialLspStatus => {
1151 actor.lsp_inflight = true;
1152 actor.actor_total_inflight += 1;
1153 }
1154 Lane::HeavyInit => {
1155 actor.actor_total_inflight += 1;
1156 }
1157 Lane::Mutating => {
1158 actor.writer_inflight = true;
1159 actor.actor_total_inflight += 1;
1160 }
1161 }
1162
1163 Some(RunJob {
1164 root_id: root_id.clone(),
1165 job_class,
1166 lane,
1167 ctx: Arc::clone(&actor.ctx),
1168 epoch: Arc::clone(&actor.epoch),
1169 job: queued.job,
1170 completion: Some(queued.completion),
1171 request_id: queued.request_id,
1172 command: queued.command,
1173 heavy_permit,
1174 })
1175}
1176
1177fn worker_loop(run_rx: Receiver<RunJob>, event_tx: Sender<SchedulerEvent>) {
1178 while let Ok(mut run_job) = run_rx.recv() {
1179 let response =
1180 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run_lane_job(&mut run_job)));
1181 let panicked = response.is_err();
1182 let response = match response {
1183 Ok(response) => response,
1184 Err(payload) => panic_response(
1185 run_job.request_id.clone(),
1186 &run_job.command,
1187 payload.as_ref(),
1188 ),
1189 };
1190
1191 if let Some(completion) = run_job.completion.take() {
1192 completion.send(response);
1193 }
1194 let completion = CompletionEvent {
1195 root_id: run_job.root_id,
1196 job_class: run_job.job_class,
1197 lane: run_job.lane,
1198 heavy_permit: run_job.heavy_permit.take(),
1199 panicked,
1200 };
1201 let _ = event_tx.send(SchedulerEvent::Completed(completion));
1202 }
1203}
1204
1205fn run_lane_job(run_job: &mut RunJob) -> Response {
1206 let missing_request_id = run_job.request_id.clone();
1207 let job = std::mem::replace(
1208 &mut run_job.job,
1209 Box::new(move |_| {
1210 Response::error(
1211 missing_request_id,
1212 "job_missing",
1213 "executor job already taken",
1214 )
1215 }),
1216 );
1217
1218 match run_job.lane {
1219 Lane::PureRead | Lane::SerialLspStatus => {
1220 let _epoch = run_job.epoch.read();
1221 job(&run_job.ctx)
1222 }
1223 Lane::HeavyInit => {
1224 let response = job(&run_job.ctx);
1225 {
1226 let _install = run_job.epoch.write();
1227 }
1228 response
1229 }
1230 Lane::Mutating => {
1231 let _epoch = run_job.epoch.write();
1232 job(&run_job.ctx)
1233 }
1234 }
1235}
1236
1237#[derive(Debug)]
1238struct HeavySemaphore {
1239 available: AtomicUsize,
1240 max: usize,
1241}
1242
1243impl HeavySemaphore {
1244 fn new(permits: usize) -> Self {
1245 Self {
1246 available: AtomicUsize::new(permits),
1247 max: permits,
1248 }
1249 }
1250
1251 fn try_acquire(self: &Arc<Self>) -> Option<HeavyPermit> {
1252 loop {
1253 let available = self.available.load(Ordering::Acquire);
1254 if available == 0 {
1255 return None;
1256 }
1257 if self
1258 .available
1259 .compare_exchange(
1260 available,
1261 available - 1,
1262 Ordering::AcqRel,
1263 Ordering::Acquire,
1264 )
1265 .is_ok()
1266 {
1267 return Some(HeavyPermit {
1268 semaphore: Arc::clone(self),
1269 });
1270 }
1271 }
1272 }
1273}
1274
1275struct HeavyPermit {
1276 semaphore: Arc<HeavySemaphore>,
1277}
1278
1279impl Drop for HeavyPermit {
1280 fn drop(&mut self) {
1281 let previous = self.semaphore.available.fetch_add(1, Ordering::Release);
1282 debug_assert!(previous < self.semaphore.max);
1283 }
1284}