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