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