Skip to main content

aft/executor/
mod.rs

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;
25pub(crate) const MAINTENANCE_QUEUE_CAP: usize = 512;
26
27/// Scheduler lane for command-handler execution.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum Lane {
30    /// Pure read-only work. Runs under the actor epoch read gate and is capped
31    /// per actor.
32    PureRead,
33    /// LSP/status work. Serialized per actor by scheduler admission while still
34    /// using the shared epoch read gate.
35    SerialLspStatus,
36    /// Heavy lazy initialization. The scheduler acquires a process-wide heavy
37    /// permit before dispatch; the worker runs the build outside the epoch and
38    /// then takes a short write gate for the install point.
39    HeavyInit,
40    /// Mutating work. Becomes a writer barrier at the actor queue head, drains
41    /// in-flight reads, and runs under the actor epoch write gate. Reserved for
42    /// configure and user-initiated tool mutations: background maintenance must
43    /// use `MaintenanceCommit` so it can never exclude interactive reads.
44    Mutating,
45    /// Maintenance work that mutates only subsystem state behind that
46    /// subsystem's own lock (watcher/LSP drains, completed-build installs,
47    /// callgraph store writes). Runs under the actor epoch READ gate and
48    /// overlaps PureReads; serialized to one in-flight per actor so
49    /// maintenance cannot self-stack.
50    MaintenanceCommit,
51}
52
53/// Scheduler class used to keep deferrable maintenance from occupying the
54/// workers reserved for interactive route binds, tool calls, and bash.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub enum JobClass {
57    Interactive,
58    Maintenance,
59}
60
61/// Idempotent maintenance drains that can share one queued execution.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63pub(crate) enum MaintenanceCoalesceKey {
64    WatcherDrain,
65    LspDrain,
66}
67
68pub type ExecutorJob = Box<dyn FnOnce(&AppContext) -> Response + Send + 'static>;
69
70/// Age at which a queued interactive mutating job (route binds, edits) jumps
71/// ahead of pure reads in interactive admission. Reads-first admission would
72/// otherwise let a sustained read stream starve queued writers; this bounds
73/// that wait. For binds it matches the half-deadline breadcrumb point: the
74/// daemon rejects binds at 12s, so promotion at 6s leaves half the budget for
75/// draining readers and running the configure itself.
76const INTERACTIVE_WRITER_PROMOTION_AGE: Duration = Duration::from_secs(6);
77
78#[derive(Debug, Clone)]
79pub struct ExecutorConfig {
80    pub pool_size: usize,
81    pub read_cap: usize,
82    pub actor_cap: usize,
83    pub heavy_permits: usize,
84    pub drr_quantum: isize,
85}
86
87impl Default for ExecutorConfig {
88    fn default() -> Self {
89        let available = thread::available_parallelism()
90            .map(usize::from)
91            .unwrap_or(2);
92        let pool_size = available.saturating_sub(1).clamp(2, 8);
93        let actor_cap = pool_size.saturating_sub(1).clamp(1, 4);
94        let read_cap = actor_cap.clamp(1, 4);
95        let heavy_permits = pool_size.saturating_sub(1).clamp(2, 3);
96
97        Self {
98            pool_size,
99            read_cap,
100            actor_cap,
101            heavy_permits,
102            drr_quantum: 1,
103        }
104    }
105}
106
107#[derive(Debug, Clone)]
108struct EffectiveConfig {
109    pool_size: usize,
110    read_cap: usize,
111    actor_cap: usize,
112    heavy_permits: usize,
113    drr_quantum: isize,
114    deficit_cap: isize,
115    interactive_reserve: usize,
116    maintenance_cap: usize,
117}
118
119impl ExecutorConfig {
120    fn effective(&self) -> EffectiveConfig {
121        let pool_size = self.pool_size.clamp(2, 8);
122        let max_actor_cap = pool_size.saturating_sub(1).max(1);
123        let actor_cap = self.actor_cap.max(1).min(max_actor_cap);
124        let read_cap = self.read_cap.max(1).min(actor_cap).min(4);
125        // HeavyInit jobs share workers with RouteBind/configure. Keep one worker
126        // available even in a two-worker pool so a heavy-init storm cannot hold
127        // a fresh bind behind every executor worker.
128        let heavy_permits = self
129            .heavy_permits
130            .max(1)
131            .min(pool_size.saturating_sub(1).max(1))
132            .min(3);
133        let drr_quantum = self.drr_quantum.max(1);
134        let deficit_cap = (actor_cap.max(1) as isize) * 4;
135        let interactive_reserve = if pool_size >= 4 { 2 } else { 1 };
136        let maintenance_cap = pool_size.saturating_sub(interactive_reserve).max(1);
137
138        EffectiveConfig {
139            pool_size,
140            read_cap,
141            actor_cap,
142            heavy_permits,
143            drr_quantum,
144            deficit_cap,
145            interactive_reserve,
146            maintenance_cap,
147        }
148    }
149}
150
151/// Synchronous completion handle used by the executor tests and the
152/// future standalone bridge.
153pub struct CompletionHandle {
154    rx: Receiver<Response>,
155}
156
157impl CompletionHandle {
158    pub fn recv(self) -> Result<Response, RecvError> {
159        self.rx.recv()
160    }
161
162    pub fn recv_timeout(&self, timeout: Duration) -> Result<Response, RecvTimeoutError> {
163        self.rx.recv_timeout(timeout)
164    }
165
166    pub fn into_receiver(self) -> Receiver<Response> {
167        self.rx
168    }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct DispatchClassQueueSnapshot {
173    pub queued: usize,
174    pub oldest_age_ms: Option<u64>,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct DispatchRunningSnapshot {
179    pub interactive: usize,
180    pub maintenance: usize,
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct DispatchLivenessSnapshot {
185    pub interactive: DispatchClassQueueSnapshot,
186    pub maintenance: DispatchClassQueueSnapshot,
187    pub running: DispatchRunningSnapshot,
188    pub interactive_reserve: usize,
189    pub maintenance_cap: usize,
190}
191
192/// Scheduler-owned mirror read by health probes without taking the actor map
193/// lock or scanning every root. Oldest queue timestamps use `0` for absent and
194/// elapsed-milliseconds-plus-one for a present timestamp.
195struct DispatchLivenessAtomics {
196    origin: Instant,
197    interactive_queued: AtomicUsize,
198    interactive_oldest_enqueued_ms_plus_one: AtomicU64,
199    maintenance_queued: AtomicUsize,
200    maintenance_oldest_enqueued_ms_plus_one: AtomicU64,
201    interactive_running: AtomicUsize,
202    maintenance_running: AtomicUsize,
203}
204
205impl DispatchLivenessAtomics {
206    fn new() -> Self {
207        Self {
208            origin: Instant::now(),
209            interactive_queued: AtomicUsize::new(0),
210            interactive_oldest_enqueued_ms_plus_one: AtomicU64::new(0),
211            maintenance_queued: AtomicUsize::new(0),
212            maintenance_oldest_enqueued_ms_plus_one: AtomicU64::new(0),
213            interactive_running: AtomicUsize::new(0),
214            maintenance_running: AtomicUsize::new(0),
215        }
216    }
217
218    fn now_ms(&self) -> u64 {
219        duration_millis_u64(self.origin.elapsed())
220    }
221
222    fn record(&self, snapshot: &DispatchLivenessSnapshot) {
223        let now_ms = self.now_ms();
224        let encode_oldest = |queued: usize, oldest_age_ms: Option<u64>| {
225            if queued == 0 {
226                0
227            } else {
228                now_ms
229                    .saturating_sub(oldest_age_ms.unwrap_or(0))
230                    .saturating_add(1)
231            }
232        };
233        self.interactive_oldest_enqueued_ms_plus_one.store(
234            encode_oldest(
235                snapshot.interactive.queued,
236                snapshot.interactive.oldest_age_ms,
237            ),
238            Ordering::Relaxed,
239        );
240        self.maintenance_oldest_enqueued_ms_plus_one.store(
241            encode_oldest(
242                snapshot.maintenance.queued,
243                snapshot.maintenance.oldest_age_ms,
244            ),
245            Ordering::Relaxed,
246        );
247        self.interactive_running
248            .store(snapshot.running.interactive, Ordering::Release);
249        self.maintenance_running
250            .store(snapshot.running.maintenance, Ordering::Release);
251        self.interactive_queued
252            .store(snapshot.interactive.queued, Ordering::Release);
253        self.maintenance_queued
254            .store(snapshot.maintenance.queued, Ordering::Release);
255    }
256
257    fn snapshot(&self, config: &EffectiveConfig) -> DispatchLivenessSnapshot {
258        let now_ms = self.now_ms();
259        let decode_oldest = |queued: usize, encoded: u64| {
260            (queued > 0 && encoded > 0).then(|| now_ms.saturating_sub(encoded - 1))
261        };
262        let interactive_queued = self.interactive_queued.load(Ordering::Acquire);
263        let maintenance_queued = self.maintenance_queued.load(Ordering::Acquire);
264        DispatchLivenessSnapshot {
265            interactive: DispatchClassQueueSnapshot {
266                queued: interactive_queued,
267                oldest_age_ms: decode_oldest(
268                    interactive_queued,
269                    self.interactive_oldest_enqueued_ms_plus_one
270                        .load(Ordering::Relaxed),
271                ),
272            },
273            maintenance: DispatchClassQueueSnapshot {
274                queued: maintenance_queued,
275                oldest_age_ms: decode_oldest(
276                    maintenance_queued,
277                    self.maintenance_oldest_enqueued_ms_plus_one
278                        .load(Ordering::Relaxed),
279                ),
280            },
281            running: DispatchRunningSnapshot {
282                interactive: self.interactive_running.load(Ordering::Acquire),
283                maintenance: self.maintenance_running.load(Ordering::Acquire),
284            },
285            interactive_reserve: config.interactive_reserve,
286            maintenance_cap: config.maintenance_cap,
287        }
288    }
289}
290
291#[derive(Debug, Clone, PartialEq, Eq)]
292pub struct MutatingLaneSnapshot {
293    pub root_id: ProjectRootId,
294    pub request_id: String,
295    pub command: String,
296    pub started_age_ms: u64,
297}
298
299/// Non-blocking scheduler explanation attached to a delayed RouteBind warning.
300#[derive(Debug, Clone, PartialEq, Eq)]
301pub struct BindBlockerSnapshot {
302    pub configure_state: &'static str,
303    pub configure_phase_timings: Option<String>,
304    pub blockers: Vec<String>,
305}
306
307/// Cooperative cancellation for one cancellable executor job.
308///
309/// One atomic state machine (pending → running → committed | cancelled) is
310/// shared by the executor, the canceller, and the job. `cancel` and
311/// `try_seal_committed` race through compare-exchange on the same cell, so
312/// exactly one wins: a cancel that lands first makes the seal fail (the job
313/// must abort before mutating), and a seal that lands first makes the cancel
314/// report `RunningCommitted` (the mutation tail finishes normally). A job
315/// observes cancellation only at its own checkpoints
316/// ([`JobCancellation::cancel_requested_before_commit`]).
317#[derive(Debug, Clone)]
318pub struct JobCancellation {
319    inner: Arc<JobCancellationInner>,
320}
321
322#[derive(Debug)]
323struct JobCancellationInner {
324    state: AtomicU8,
325}
326
327const JOB_CANCEL_STATE_PENDING: u8 = 0;
328const JOB_CANCEL_STATE_RUNNING: u8 = 1;
329const JOB_CANCEL_STATE_COMMITTED: u8 = 2;
330const JOB_CANCEL_STATE_CANCELLED: u8 = 3;
331
332impl JobCancellation {
333    pub(crate) fn new() -> Self {
334        Self {
335            inner: Arc::new(JobCancellationInner {
336                state: AtomicU8::new(JOB_CANCEL_STATE_PENDING),
337            }),
338        }
339    }
340
341    fn mark_running(&self) {
342        let _ = self.inner.state.compare_exchange(
343            JOB_CANCEL_STATE_PENDING,
344            JOB_CANCEL_STATE_RUNNING,
345            Ordering::SeqCst,
346            Ordering::SeqCst,
347        );
348    }
349
350    /// Seal the job as committed. Returns `false` when a cancel already won
351    /// the race: the job must return early WITHOUT mutating, because the
352    /// canceller was told `RunningSignalled` and will discard its completion.
353    #[must_use]
354    pub fn try_seal_committed(&self) -> bool {
355        loop {
356            let current = self.state();
357            match current {
358                JOB_CANCEL_STATE_CANCELLED => return false,
359                JOB_CANCEL_STATE_COMMITTED => return true,
360                _ => {
361                    if self
362                        .inner
363                        .state
364                        .compare_exchange(
365                            current,
366                            JOB_CANCEL_STATE_COMMITTED,
367                            Ordering::SeqCst,
368                            Ordering::SeqCst,
369                        )
370                        .is_ok()
371                    {
372                        return true;
373                    }
374                }
375            }
376        }
377    }
378
379    /// Move to cancelled unless the job already sealed its commit. Returns the
380    /// state the transition observed (`COMMITTED` when the seal won).
381    fn signal_cancel(&self) -> u8 {
382        loop {
383            let current = self.state();
384            match current {
385                JOB_CANCEL_STATE_COMMITTED | JOB_CANCEL_STATE_CANCELLED => return current,
386                _ => {
387                    if self
388                        .inner
389                        .state
390                        .compare_exchange(
391                            current,
392                            JOB_CANCEL_STATE_CANCELLED,
393                            Ordering::SeqCst,
394                            Ordering::SeqCst,
395                        )
396                        .is_ok()
397                    {
398                        return current;
399                    }
400                }
401            }
402        }
403    }
404
405    fn state(&self) -> u8 {
406        self.inner.state.load(Ordering::SeqCst)
407    }
408
409    /// True when a cancel won the state race and the job must abort.
410    pub fn cancel_requested_before_commit(&self) -> bool {
411        self.state() == JOB_CANCEL_STATE_CANCELLED
412    }
413
414    fn same_token(&self, other: &JobCancellation) -> bool {
415        Arc::ptr_eq(&self.inner, &other.inner)
416    }
417}
418
419/// Outcome of [`Executor::cancel_job`].
420#[derive(Debug, Clone, Copy, PartialEq, Eq)]
421pub enum JobCancelOutcome {
422    /// The job had not started; it was removed from the queue and its
423    /// completion settled with `request_cancelled`.
424    QueuedRemoved,
425    /// The job is running; its token is signalled and the job returns through
426    /// its normal completion path at the next checkpoint.
427    RunningSignalled,
428    /// The job already sealed its commit; it finishes normally and the caller
429    /// must discard its late completion.
430    RunningCommitted,
431    /// No queued or tracked job matched the token.
432    NotFound,
433}
434
435thread_local! {
436    static CURRENT_JOB_CANCELLATION: std::cell::RefCell<Option<JobCancellation>> =
437        const { std::cell::RefCell::new(None) };
438}
439
440/// The cancellation token of the job running on this worker thread, when the
441/// job was submitted through [`Executor::submit_cancellable_async`].
442pub fn current_job_cancellation() -> Option<JobCancellation> {
443    CURRENT_JOB_CANCELLATION.with(|slot| slot.borrow().clone())
444}
445
446struct CurrentJobCancellationGuard {
447    previous: Option<JobCancellation>,
448}
449
450impl CurrentJobCancellationGuard {
451    fn install(token: Option<JobCancellation>) -> Self {
452        let previous = CURRENT_JOB_CANCELLATION.with(|slot| slot.replace(token));
453        Self { previous }
454    }
455}
456
457impl Drop for CurrentJobCancellationGuard {
458    fn drop(&mut self) {
459        CURRENT_JOB_CANCELLATION.with(|slot| {
460            *slot.borrow_mut() = self.previous.take();
461        });
462    }
463}
464
465#[derive(Debug, Clone)]
466struct RunningJob {
467    root_id: ProjectRootId,
468    request_id: String,
469    command: String,
470    job_class: JobClass,
471    lane: Lane,
472    started_at: Instant,
473}
474
475#[derive(Debug, Clone)]
476struct RunningMutatingJob {
477    request_id: String,
478    command: String,
479    started_at: Instant,
480}
481
482/// Concurrent scheduler-dispatch executor.
483pub struct Executor {
484    inner: Arc<ExecutorInner>,
485}
486
487impl Executor {
488    pub fn new() -> Self {
489        Self::with_config(ExecutorConfig::default())
490    }
491
492    pub fn with_config(config: ExecutorConfig) -> Self {
493        let effective = config.effective();
494        let state = Arc::new(Mutex::new(SchedulerState::new(effective.clone())));
495        let heavy = Arc::new(HeavySemaphore::new(effective.heavy_permits));
496        let nonrunnable_dispatches = Arc::new(AtomicUsize::new(0));
497        let completed_interactive = Arc::new(AtomicU64::new(0));
498        let completed_maintenance = Arc::new(AtomicU64::new(0));
499        let dispatch_liveness = Arc::new(DispatchLivenessAtomics::new());
500        let (run_tx, run_rx) = crossbeam_channel::unbounded();
501        let (event_tx, event_rx) = crossbeam_channel::unbounded();
502
503        let scheduler_state = Arc::clone(&state);
504        let scheduler_heavy = Arc::clone(&heavy);
505        let scheduler_violations = Arc::clone(&nonrunnable_dispatches);
506        let scheduler_completed_interactive = Arc::clone(&completed_interactive);
507        let scheduler_completed_maintenance = Arc::clone(&completed_maintenance);
508        let scheduler_dispatch_liveness = Arc::clone(&dispatch_liveness);
509        let scheduler_handle = thread::Builder::new()
510            .name("aft-executor-scheduler".to_string())
511            .spawn(move || {
512                scheduler_loop(
513                    scheduler_state,
514                    scheduler_heavy,
515                    run_tx,
516                    event_rx,
517                    scheduler_violations,
518                    scheduler_completed_interactive,
519                    scheduler_completed_maintenance,
520                    scheduler_dispatch_liveness,
521                );
522            })
523            .expect("spawn AFT executor scheduler");
524
525        let mut worker_handles = Vec::with_capacity(effective.pool_size);
526        for worker_id in 0..effective.pool_size {
527            let worker_rx = run_rx.clone();
528            let worker_events = event_tx.clone();
529            let handle = thread::Builder::new()
530                .name(format!("aft-executor-worker-{worker_id}"))
531                .spawn(move || worker_loop(worker_rx, worker_events))
532                .expect("spawn AFT executor worker");
533            worker_handles.push(handle);
534        }
535
536        Self {
537            inner: Arc::new(ExecutorInner {
538                state,
539                event_tx,
540                scheduler_handle: Mutex::new(Some(scheduler_handle)),
541                worker_handles: Mutex::new(worker_handles),
542                config: effective,
543                nonrunnable_dispatches,
544                completed_interactive,
545                completed_maintenance,
546                dispatch_liveness,
547            }),
548        }
549    }
550
551    /// Register an actor if one is not already present.
552    ///
553    /// Existing actors keep their current context and scheduler state; subc
554    /// routing reuses them and reconfigures through the Mutating lane
555    /// rather than replacing the per-root [`AppContext`]. Returns `true` when a
556    /// new actor was inserted.
557    pub fn register_actor(&self, root_id: ProjectRootId, ctx: Arc<AppContext>) -> bool {
558        let memory_root = root_id.as_path().to_path_buf();
559        let inserted = {
560            let mut state = self.inner.state.lock();
561            if state.actors.contains_key(&root_id) {
562                false
563            } else {
564                state.actor_order.push(root_id.clone());
565                state
566                    .actors
567                    .insert(root_id, ActorState::new(Arc::clone(&ctx)));
568                true
569            }
570        };
571        if inserted {
572            let app = ctx.app();
573            crate::root_cache::register_live_scope(&ctx.storage_dir(), &memory_root);
574            app.register_memory_context(memory_root, &ctx);
575            app.actor_root_registered();
576        }
577        self.wake_scheduler();
578        inserted
579    }
580
581    /// Remove an actor from scheduler state.
582    ///
583    /// This is intentionally minimal: subc uses it only for a just-created
584    /// RouteBind actor whose configure failed before any route was installed, so
585    /// there is no in-flight work to quiesce. The removed [`AppContext`] is
586    /// dropped after releasing the scheduler lock so watcher/LSP teardown never
587    /// runs under that mutex.
588    pub fn remove_actor(&self, root_id: &ProjectRootId) {
589        let removed = {
590            let mut state = self.inner.state.lock();
591            state.actor_order.retain(|actor_root| actor_root != root_id);
592            state.actors.remove(root_id)
593        };
594        if let Some(actor) = removed.as_ref() {
595            let app = actor.ctx.app();
596            crate::root_cache::unregister_live_scope(&actor.ctx.storage_dir(), root_id.as_path());
597            app.unregister_memory_context(root_id.as_path(), &actor.ctx);
598            app.actor_root_unregistered();
599        }
600        drop(removed);
601        self.wake_scheduler();
602    }
603
604    /// Return true only when the actor has no queued or running executor work.
605    pub fn actor_is_idle(&self, root_id: &ProjectRootId) -> bool {
606        let state = self.inner.state.lock();
607        state.actors.get(root_id).is_some_and(ActorState::is_idle)
608    }
609
610    /// Non-blocking idle probe for maintenance sweeps. A contended scheduler is
611    /// reported separately so root retirement can conservatively wait for the
612    /// next sweep without stalling the module loop.
613    pub fn try_actor_is_idle(&self, root_id: &ProjectRootId) -> Option<bool> {
614        let state = self.inner.state.try_lock()?;
615        Some(state.actors.get(root_id).is_some_and(ActorState::is_idle))
616    }
617
618    /// Forget an idle actor and drop its root-scoped registries off the scheduler
619    /// thread. This is reserved for roots whose project directory no longer
620    /// exists; retained existing roots are reused on a later bind.
621    pub fn retire_idle_actor_in_background(&self, root_id: &ProjectRootId) -> bool {
622        let removed = {
623            let mut state = self.inner.state.lock();
624            if !state.actors.get(root_id).is_some_and(ActorState::is_idle) {
625                return false;
626            }
627            state.actor_order.retain(|actor_root| actor_root != root_id);
628            state.actors.remove(root_id)
629        };
630        let Some(actor) = removed else {
631            return false;
632        };
633        let app = actor.ctx.app();
634        crate::root_cache::unregister_live_scope(&actor.ctx.storage_dir(), root_id.as_path());
635        app.unregister_memory_context(root_id.as_path(), &actor.ctx);
636        app.actor_root_unregistered();
637        std::thread::spawn(move || {
638            actor.ctx.teardown_deleted_root();
639            drop(actor);
640        });
641        self.wake_scheduler();
642        true
643    }
644
645    /// Cancel maintenance jobs that have not started for one retained actor.
646    ///
647    /// Interactive work and already-running maintenance remain untouched. Each
648    /// cancelled job receives a normal completion so its caller can settle
649    /// bookkeeping through the same path as an executed job.
650    pub fn cancel_queued_maintenance(&self, root_id: &ProjectRootId) -> usize {
651        let cancelled = {
652            let mut state = self.inner.state.lock();
653            state
654                .actors
655                .get_mut(root_id)
656                .map(|actor| actor.maintenance.cancel_queued_jobs())
657                .unwrap_or(0)
658        };
659        if cancelled > 0 {
660            self.wake_scheduler();
661        }
662        cancelled
663    }
664
665    /// Return whether scheduler state currently has an actor for this root.
666    pub fn actor_registered(&self, root_id: &ProjectRootId) -> bool {
667        let state = self.inner.state.lock();
668        state.actors.contains_key(root_id)
669    }
670
671    /// Snapshot one actor context without retaining the scheduler lock while
672    /// maintenance drops root-scoped resources.
673    pub fn actor_context(&self, root_id: &ProjectRootId) -> Option<Arc<AppContext>> {
674        let state = self.inner.state.lock();
675        state
676            .actors
677            .get(root_id)
678            .map(|actor| Arc::clone(&actor.ctx))
679    }
680
681    /// Snapshot the registered actor contexts.
682    ///
683    /// The returned [`Arc`]s keep contexts alive after the scheduler lock is
684    /// released, so callers can run teardown without holding executor state.
685    pub fn actor_contexts(&self) -> Vec<Arc<AppContext>> {
686        let state = self.inner.state.lock();
687        state
688            .actors
689            .values()
690            .map(|actor_state| Arc::clone(&actor_state.ctx))
691            .collect()
692    }
693
694    /// Snapshot the registered root ids paired with their actor contexts.
695    pub fn actor_entries(&self) -> Vec<(ProjectRootId, Arc<AppContext>)> {
696        let state = self.inner.state.lock();
697        state
698            .actors
699            .iter()
700            .map(|(root_id, actor_state)| (root_id.clone(), Arc::clone(&actor_state.ctx)))
701            .collect()
702    }
703
704    /// Non-blocking variant for the health path: the probe reply must stay
705    /// cheap under any load, so it skips the actor list (reported as busy)
706    /// rather than waiting on the scheduler state lock.
707    pub fn try_actor_entries(&self) -> Option<Vec<(ProjectRootId, Arc<AppContext>)>> {
708        let state = self.inner.state.try_lock()?;
709        Some(
710            state
711                .actors
712                .iter()
713                .map(|(root_id, actor_state)| (root_id.clone(), Arc::clone(&actor_state.ctx)))
714                .collect(),
715        )
716    }
717
718    /// Constant-time scheduler contention signal for the health reply path.
719    pub fn try_actor_count(&self) -> Option<usize> {
720        self.inner.state.try_lock().map(|state| state.actors.len())
721    }
722
723    pub fn submit(
724        &self,
725        root_id: ProjectRootId,
726        lane: Lane,
727        request_id: String,
728        job: ExecutorJob,
729    ) -> CompletionHandle {
730        let (completion_tx, completion_rx) = crossbeam_channel::bounded(1);
731        self.submit_with_completion(
732            root_id,
733            JobClass::Interactive,
734            lane,
735            request_id,
736            job,
737            CompletionSender::Sync(completion_tx),
738        );
739        CompletionHandle { rx: completion_rx }
740    }
741
742    pub fn submit_async(
743        &self,
744        root_id: ProjectRootId,
745        lane: Lane,
746        request_id: String,
747        job: ExecutorJob,
748    ) -> oneshot::Receiver<Response> {
749        let (completion_tx, completion_rx) = oneshot::channel();
750        self.submit_with_completion(
751            root_id,
752            JobClass::Interactive,
753            lane,
754            request_id,
755            job,
756            CompletionSender::Async(completion_tx),
757        );
758        completion_rx
759    }
760
761    /// Submit an interactive job with an exact-job cancellation token.
762    ///
763    /// The returned token cancels THIS job only (queued: removed and settled
764    /// with `request_cancelled`; running: signalled cooperatively). The job
765    /// observes the token via [`current_job_cancellation`].
766    pub fn submit_cancellable_async(
767        &self,
768        root_id: ProjectRootId,
769        lane: Lane,
770        request_id: String,
771        job: ExecutorJob,
772    ) -> (oneshot::Receiver<Response>, JobCancellation) {
773        let cancellation = JobCancellation::new();
774        let (completion_tx, completion_rx) = oneshot::channel();
775        self.submit_with_completion_cancellable(
776            root_id,
777            JobClass::Interactive,
778            lane,
779            request_id,
780            job,
781            CompletionSender::Async(completion_tx),
782            Some(cancellation.clone()),
783            None,
784        );
785        (completion_rx, cancellation)
786    }
787
788    /// Cancel one exact job by its token.
789    ///
790    /// Queued jobs are removed and settled immediately; running jobs are
791    /// signalled and return through their normal completion path at the next
792    /// cooperative checkpoint. Jobs that sealed their commit finish normally.
793    pub fn cancel_job(&self, root_id: &ProjectRootId, token: &JobCancellation) -> JobCancelOutcome {
794        // Signal BEFORE actor lookup, deliberately: a job whose actor was
795        // already torn down (fatal teardown, root removal) must still abort at
796        // its next checkpoint, so the token is cancelled regardless. The
797        // outcome must then reflect what the signal actually did — reporting
798        // NotFound for a token that was RUNNING at signal time would mislead
799        // the caller into treating a signalled job as nonexistent.
800        let observed = token.signal_cancel();
801        let (outcome, settled) = {
802            let mut state = self.inner.state.lock();
803            let Some(actor) = state.actors.get_mut(root_id) else {
804                return match observed {
805                    JOB_CANCEL_STATE_COMMITTED => JobCancelOutcome::RunningCommitted,
806                    JOB_CANCEL_STATE_RUNNING | JOB_CANCEL_STATE_PENDING => {
807                        JobCancelOutcome::RunningSignalled
808                    }
809                    _ => JobCancelOutcome::NotFound,
810                };
811            };
812            match actor.remove_queued_cancellable(token) {
813                Some(queued) => (JobCancelOutcome::QueuedRemoved, Some(queued)),
814                None => match observed {
815                    // The seal won the race: the job commits and finishes.
816                    JOB_CANCEL_STATE_COMMITTED => (JobCancelOutcome::RunningCommitted, None),
817                    // RUNNING at signal time, or PENDING at signal time but
818                    // dispatched before we took the scheduler lock: either way
819                    // the job aborts at its next checkpoint.
820                    JOB_CANCEL_STATE_RUNNING | JOB_CANCEL_STATE_PENDING => {
821                        (JobCancelOutcome::RunningSignalled, None)
822                    }
823                    // Already cancelled by an earlier call and no longer queued.
824                    _ => (JobCancelOutcome::NotFound, None),
825                },
826            }
827        };
828        if let Some(queued) = settled {
829            queued.completion.send(Response::error(
830                queued.request_id,
831                "request_cancelled",
832                "request cancelled before execution",
833            ));
834            self.wake_scheduler();
835        }
836        outcome
837    }
838
839    pub fn submit_maintenance_async(
840        &self,
841        root_id: ProjectRootId,
842        lane: Lane,
843        request_id: String,
844        job: ExecutorJob,
845    ) -> oneshot::Receiver<Response> {
846        self.submit_maintenance_async_with_key(root_id, lane, request_id, job, None)
847    }
848
849    pub(crate) fn submit_coalescable_maintenance_async(
850        &self,
851        root_id: ProjectRootId,
852        lane: Lane,
853        request_id: String,
854        coalesce_key: MaintenanceCoalesceKey,
855        job: ExecutorJob,
856    ) -> oneshot::Receiver<Response> {
857        self.submit_maintenance_async_with_key(root_id, lane, request_id, job, Some(coalesce_key))
858    }
859
860    fn submit_maintenance_async_with_key(
861        &self,
862        root_id: ProjectRootId,
863        lane: Lane,
864        request_id: String,
865        job: ExecutorJob,
866        coalesce_key: Option<MaintenanceCoalesceKey>,
867    ) -> oneshot::Receiver<Response> {
868        let (completion_tx, completion_rx) = oneshot::channel();
869        self.submit_with_completion_cancellable(
870            root_id,
871            JobClass::Maintenance,
872            lane,
873            request_id,
874            job,
875            CompletionSender::Async(completion_tx),
876            None,
877            coalesce_key,
878        );
879        completion_rx
880    }
881
882    fn submit_with_completion(
883        &self,
884        root_id: ProjectRootId,
885        job_class: JobClass,
886        lane: Lane,
887        request_id: String,
888        job: ExecutorJob,
889        completion: CompletionSender,
890    ) {
891        self.submit_with_completion_cancellable(
892            root_id, job_class, lane, request_id, job, completion, None, None,
893        );
894    }
895
896    #[allow(clippy::too_many_arguments)]
897    fn submit_with_completion_cancellable(
898        &self,
899        root_id: ProjectRootId,
900        job_class: JobClass,
901        lane: Lane,
902        request_id: String,
903        job: ExecutorJob,
904        completion: CompletionSender,
905        cancellation: Option<JobCancellation>,
906        maintenance_coalesce_key: Option<MaintenanceCoalesceKey>,
907    ) {
908        let command = job_command(job_class, lane);
909        let mut job = Some(job);
910        let mut completion = Some(completion);
911        let mut duplicate_victims = Vec::new();
912
913        let response = {
914            let mut state = self.inner.state.lock();
915            match state.actors.get_mut(&root_id) {
916                Some(actor) if actor.fatal => Some(actor_fatal_response(request_id.clone())),
917                Some(actor) => {
918                    let mut admission_error = None;
919                    if job_class == JobClass::Maintenance {
920                        if maintenance_coalesce_key
921                            .is_some_and(|key| actor.maintenance.has_maintenance_coalesce_key(key))
922                        {
923                            admission_error = Some(maintenance_cancelled_response(
924                                request_id.clone(),
925                                "maintenance drain coalesced behind an identical queued drain",
926                            ));
927                        } else {
928                            if actor.maintenance.queued_count() >= MAINTENANCE_QUEUE_CAP {
929                                duplicate_victims =
930                                    actor.maintenance.remove_duplicate_maintenance_jobs();
931                            }
932                            if actor.maintenance.queued_count() >= MAINTENANCE_QUEUE_CAP {
933                                admission_error =
934                                    Some(maintenance_backpressure_response(request_id.clone()));
935                            }
936                        }
937                    }
938
939                    if admission_error.is_none() {
940                        actor.push_job(
941                            job_class,
942                            lane,
943                            QueuedJob {
944                                job: job.take().expect("executor job already queued"),
945                                completion: completion
946                                    .take()
947                                    .expect("executor completion already queued"),
948                                request_id: request_id.clone(),
949                                command,
950                                queued_at: Instant::now(),
951                                cancellation: cancellation.clone(),
952                                maintenance_coalesce_key,
953                            },
954                        );
955                    }
956                    admission_error
957                }
958                None => Some(Response::error(
959                    request_id.clone(),
960                    "actor_not_registered",
961                    "executor actor is not registered",
962                )),
963            }
964        };
965
966        for victim in duplicate_victims {
967            victim.completion.send(maintenance_cancelled_response(
968                victim.request_id,
969                "duplicate maintenance drain removed to preserve queue capacity",
970            ));
971        }
972
973        if let Some(response) = response {
974            if let Some(completion) = completion {
975                completion.send(response);
976            }
977            return;
978        }
979
980        self.wake_scheduler();
981    }
982
983    pub fn pool_size(&self) -> usize {
984        self.inner.config.pool_size
985    }
986
987    pub fn actor_cap(&self) -> usize {
988        self.inner.config.actor_cap
989    }
990
991    pub fn read_cap(&self) -> usize {
992        self.inner.config.read_cap
993    }
994
995    pub fn heavy_permits(&self) -> usize {
996        self.inner.config.heavy_permits
997    }
998
999    pub fn interactive_reserve(&self) -> usize {
1000        self.inner.config.interactive_reserve
1001    }
1002
1003    pub fn maintenance_cap(&self) -> usize {
1004        self.inner.config.maintenance_cap
1005    }
1006
1007    pub fn try_dispatch_liveness_snapshot(&self) -> Option<DispatchLivenessSnapshot> {
1008        Some(self.inner.dispatch_liveness.snapshot(&self.inner.config))
1009    }
1010
1011    pub fn try_mutating_lane_snapshots(&self) -> Option<Vec<MutatingLaneSnapshot>> {
1012        self.inner
1013            .state
1014            .try_lock()
1015            .map(|state| state.mutating_lane_snapshots())
1016    }
1017
1018    pub fn try_mutating_job_state_label(
1019        &self,
1020        root_id: &ProjectRootId,
1021        request_id: &str,
1022    ) -> Option<&'static str> {
1023        self.inner
1024            .state
1025            .try_lock()
1026            .map(|state| state.mutating_job_state_label(root_id, request_id))
1027    }
1028
1029    /// Snapshot RouteBind blockers without waiting on scheduler state. The subc
1030    /// health path uses this only for a delayed-bind breadcrumb, so contention
1031    /// is reported as scheduler busy rather than delaying the transport loop.
1032    pub fn try_bind_blocker_snapshot(
1033        &self,
1034        root_id: &ProjectRootId,
1035        request_id: &str,
1036    ) -> Option<BindBlockerSnapshot> {
1037        self.inner
1038            .state
1039            .try_lock()
1040            .map(|state| state.bind_blocker_snapshot(root_id, request_id))
1041    }
1042
1043    pub fn nonrunnable_dispatch_count(&self) -> usize {
1044        self.inner.nonrunnable_dispatches.load(Ordering::Acquire)
1045    }
1046
1047    pub fn completion_counts(&self) -> (u64, u64) {
1048        (
1049            self.inner.completed_interactive.load(Ordering::Relaxed),
1050            self.inner.completed_maintenance.load(Ordering::Relaxed),
1051        )
1052    }
1053
1054    pub fn actor_is_fatal(&self, root_id: &ProjectRootId) -> bool {
1055        self.inner
1056            .state
1057            .lock()
1058            .actors
1059            .get(root_id)
1060            .map(|actor| actor.fatal)
1061            .unwrap_or(false)
1062    }
1063
1064    fn wake_scheduler(&self) {
1065        let _ = self.inner.event_tx.send(SchedulerEvent::Wake);
1066    }
1067}
1068
1069impl Default for Executor {
1070    fn default() -> Self {
1071        Self::new()
1072    }
1073}
1074
1075struct ExecutorInner {
1076    state: Arc<Mutex<SchedulerState>>,
1077    event_tx: Sender<SchedulerEvent>,
1078    scheduler_handle: Mutex<Option<JoinHandle<()>>>,
1079    worker_handles: Mutex<Vec<JoinHandle<()>>>,
1080    config: EffectiveConfig,
1081    nonrunnable_dispatches: Arc<AtomicUsize>,
1082    completed_interactive: Arc<AtomicU64>,
1083    completed_maintenance: Arc<AtomicU64>,
1084    dispatch_liveness: Arc<DispatchLivenessAtomics>,
1085}
1086
1087impl Drop for ExecutorInner {
1088    fn drop(&mut self) {
1089        let _ = self.event_tx.send(SchedulerEvent::Shutdown);
1090
1091        if let Some(handle) = self.scheduler_handle.lock().take() {
1092            let _ = handle.join();
1093        }
1094
1095        let mut workers = self.worker_handles.lock();
1096        for handle in workers.drain(..) {
1097            let _ = handle.join();
1098        }
1099    }
1100}
1101
1102struct SchedulerState {
1103    actors: HashMap<ProjectRootId, ActorState>,
1104    actor_order: Vec<ProjectRootId>,
1105    cursor: usize,
1106    idle_workers: usize,
1107    interactive_inflight: usize,
1108    maintenance_inflight: usize,
1109    config: EffectiveConfig,
1110    running_jobs: HashMap<(ProjectRootId, String), RunningJob>,
1111}
1112
1113impl SchedulerState {
1114    fn new(config: EffectiveConfig) -> Self {
1115        Self {
1116            actors: HashMap::new(),
1117            actor_order: Vec::new(),
1118            cursor: 0,
1119            idle_workers: config.pool_size,
1120            interactive_inflight: 0,
1121            maintenance_inflight: 0,
1122            config,
1123            running_jobs: HashMap::new(),
1124        }
1125    }
1126
1127    fn dispatch_liveness_snapshot(&self) -> DispatchLivenessSnapshot {
1128        let now = Instant::now();
1129        let mut interactive = QueueSnapshotAccumulator::default();
1130        let mut maintenance = QueueSnapshotAccumulator::default();
1131        for actor in self.actors.values() {
1132            interactive.add(actor.class_queues(JobClass::Interactive), now);
1133            maintenance.add(actor.class_queues(JobClass::Maintenance), now);
1134        }
1135
1136        DispatchLivenessSnapshot {
1137            interactive: interactive.finish(),
1138            maintenance: maintenance.finish(),
1139            running: DispatchRunningSnapshot {
1140                interactive: self.interactive_inflight,
1141                maintenance: self.maintenance_inflight,
1142            },
1143            interactive_reserve: self.config.interactive_reserve,
1144            maintenance_cap: self.config.maintenance_cap,
1145        }
1146    }
1147
1148    fn mutating_lane_snapshots(&self) -> Vec<MutatingLaneSnapshot> {
1149        let now = Instant::now();
1150        let mut snapshots: Vec<_> = self
1151            .actors
1152            .iter()
1153            .filter_map(|(root_id, actor)| {
1154                actor
1155                    .mutating_inflight
1156                    .as_ref()
1157                    .map(|job| MutatingLaneSnapshot {
1158                        root_id: root_id.clone(),
1159                        request_id: job.request_id.clone(),
1160                        command: job.command.clone(),
1161                        started_age_ms: duration_millis_u64(
1162                            now.saturating_duration_since(job.started_at),
1163                        ),
1164                    })
1165            })
1166            .collect();
1167        snapshots.sort_by(|left, right| left.root_id.as_path().cmp(right.root_id.as_path()));
1168        snapshots
1169    }
1170
1171    fn mutating_job_state_label(&self, root_id: &ProjectRootId, request_id: &str) -> &'static str {
1172        let Some(actor) = self.actors.get(root_id) else {
1173            return "actor_missing";
1174        };
1175        if actor
1176            .mutating_inflight
1177            .as_ref()
1178            .is_some_and(|job| job.request_id == request_id)
1179        {
1180            return "running";
1181        }
1182        if actor.has_queued_mutating_job(request_id) {
1183            return "queued";
1184        }
1185        if actor.writer_inflight {
1186            return "blocked_by_other_mutating";
1187        }
1188        "not_found"
1189    }
1190
1191    fn bind_blocker_snapshot(
1192        &self,
1193        root_id: &ProjectRootId,
1194        request_id: &str,
1195    ) -> BindBlockerSnapshot {
1196        let configure_state = self.mutating_job_state_label(root_id, request_id);
1197        let mut blockers = Vec::new();
1198
1199        if let Some(actor) = self.actors.get(root_id) {
1200            if configure_state == "queued" {
1201                let configure_count = actor.pending_configure_count();
1202                if configure_count > 0 {
1203                    blockers.push(format!("queued_behind_configure({configure_count})"));
1204                }
1205                if actor.read_inflight > 0 || actor.lsp_inflight {
1206                    blockers.push("waiting_on_readers".to_string());
1207                }
1208            }
1209        }
1210
1211        if configure_state == "queued" {
1212            let maintenance: Vec<_> = self
1213                .running_jobs
1214                .values()
1215                .filter(|job| job.job_class == JobClass::Maintenance)
1216                .collect();
1217            if !maintenance.is_empty() {
1218                blockers.push(format!(
1219                    "queued_behind_maintenance({})",
1220                    format_running_jobs(&maintenance)
1221                ));
1222            }
1223        }
1224
1225        if self.idle_workers == 0 {
1226            let running: Vec<_> = self.running_jobs.values().collect();
1227            blockers.push(format!(
1228                "idle_workers==0({})",
1229                format_running_jobs(&running)
1230            ));
1231        }
1232
1233        BindBlockerSnapshot {
1234            configure_state,
1235            configure_phase_timings: self
1236                .actors
1237                .get(root_id)
1238                .map(|actor| actor.ctx.configure_ack_phase_snapshot()),
1239            blockers,
1240        }
1241    }
1242}
1243
1244fn is_configure_request(request_id: &str) -> bool {
1245    request_id.starts_with("subc-bind-")
1246}
1247
1248fn format_running_jobs(jobs: &[&RunningJob]) -> String {
1249    let now = Instant::now();
1250    let mut labels: Vec<_> = jobs
1251        .iter()
1252        .map(|job| {
1253            format!(
1254                "job={} command={} lane={:?} root={} age_ms={}",
1255                job.request_id,
1256                job.command,
1257                job.lane,
1258                job.root_id.as_path().display(),
1259                duration_millis_u64(now.saturating_duration_since(job.started_at))
1260            )
1261        })
1262        .collect();
1263    labels.sort();
1264    labels.truncate(4);
1265    labels.join("; ")
1266}
1267
1268#[derive(Default)]
1269struct QueueSnapshotAccumulator {
1270    queued: usize,
1271    oldest_age_ms: Option<u64>,
1272}
1273
1274impl QueueSnapshotAccumulator {
1275    fn add(&mut self, queues: &ClassQueues, now: Instant) {
1276        self.queued += queues.queued_count();
1277        if let Some(queued_at) = queues.oldest_queued_at() {
1278            let age_ms = duration_millis_u64(now.saturating_duration_since(queued_at));
1279            self.oldest_age_ms = Some(
1280                self.oldest_age_ms
1281                    .map_or(age_ms, |oldest| oldest.max(age_ms)),
1282            );
1283        }
1284    }
1285
1286    fn finish(self) -> DispatchClassQueueSnapshot {
1287        DispatchClassQueueSnapshot {
1288            queued: self.queued,
1289            oldest_age_ms: self.oldest_age_ms,
1290        }
1291    }
1292}
1293
1294fn duration_millis_u64(duration: Duration) -> u64 {
1295    duration.as_millis().min(u128::from(u64::MAX)) as u64
1296}
1297
1298struct ActorState {
1299    ctx: Arc<AppContext>,
1300    epoch: Arc<RwLock<()>>,
1301    read_inflight: usize,
1302    lsp_inflight: bool,
1303    actor_total_inflight: usize,
1304    writer_inflight: bool,
1305    maintenance_commit_inflight: bool,
1306    mutating_inflight: Option<RunningMutatingJob>,
1307    deficit: isize,
1308    interactive: ClassQueues,
1309    maintenance: ClassQueues,
1310    fatal: bool,
1311}
1312
1313impl ActorState {
1314    fn new(ctx: Arc<AppContext>) -> Self {
1315        Self {
1316            ctx,
1317            epoch: Arc::new(RwLock::new(())),
1318            read_inflight: 0,
1319            lsp_inflight: false,
1320            actor_total_inflight: 0,
1321            writer_inflight: false,
1322            maintenance_commit_inflight: false,
1323            mutating_inflight: None,
1324            deficit: 0,
1325            interactive: ClassQueues::new(),
1326            maintenance: ClassQueues::new(),
1327            fatal: false,
1328        }
1329    }
1330
1331    fn push_job(&mut self, job_class: JobClass, lane: Lane, job: QueuedJob) {
1332        self.class_queues_mut(job_class).push_job(lane, job);
1333    }
1334
1335    fn has_queued_jobs(&self) -> bool {
1336        self.interactive.has_queued_jobs() || self.maintenance.has_queued_jobs()
1337    }
1338
1339    fn is_idle(&self) -> bool {
1340        self.actor_total_inflight == 0 && !self.has_queued_jobs()
1341    }
1342
1343    fn has_queued_jobs_for(&self, job_class: JobClass) -> bool {
1344        self.class_queues(job_class).has_queued_jobs()
1345    }
1346
1347    fn front_lane(&self, job_class: JobClass) -> Option<Lane> {
1348        self.class_queues(job_class).front_lane()
1349    }
1350
1351    fn pop_front_job(&mut self, job_class: JobClass, lane: Lane) -> Option<QueuedJob> {
1352        self.class_queues_mut(job_class).pop_front_job(lane)
1353    }
1354
1355    fn higher_priority_writer_barrier_blocks(&self, job_class: JobClass) -> bool {
1356        // Maintenance must not start while interactive mutating work (tool
1357        // mutations, route binds) waits: a maintenance job that takes the
1358        // actor's writer slot would push the interactive writer behind it.
1359        matches!(job_class, JobClass::Maintenance)
1360            && !self.interactive.queue(Lane::Mutating).is_empty()
1361    }
1362
1363    fn class_queues(&self, job_class: JobClass) -> &ClassQueues {
1364        match job_class {
1365            JobClass::Interactive => &self.interactive,
1366            JobClass::Maintenance => &self.maintenance,
1367        }
1368    }
1369
1370    fn class_queues_mut(&mut self, job_class: JobClass) -> &mut ClassQueues {
1371        match job_class {
1372            JobClass::Interactive => &mut self.interactive,
1373            JobClass::Maintenance => &mut self.maintenance,
1374        }
1375    }
1376
1377    fn fail_queued_jobs(&mut self) {
1378        self.interactive.fail_queued_jobs();
1379        self.maintenance.fail_queued_jobs();
1380    }
1381
1382    fn has_queued_mutating_job(&self, request_id: &str) -> bool {
1383        self.interactive.has_queued_mutating_job(request_id)
1384            || self.maintenance.has_queued_mutating_job(request_id)
1385    }
1386
1387    fn remove_queued_cancellable(&mut self, token: &JobCancellation) -> Option<QueuedJob> {
1388        self.interactive
1389            .remove_cancellable(token)
1390            .or_else(|| self.maintenance.remove_cancellable(token))
1391    }
1392
1393    fn pending_configure_count(&self) -> usize {
1394        usize::from(
1395            self.mutating_inflight
1396                .as_ref()
1397                .is_some_and(|job| is_configure_request(&job.request_id)),
1398        ) + self.interactive.queued_configure_count()
1399            + self.maintenance.queued_configure_count()
1400    }
1401}
1402
1403struct ClassQueues {
1404    order: VecDeque<Lane>,
1405    pure_reads: VecDeque<QueuedJob>,
1406    lsp_status: VecDeque<QueuedJob>,
1407    heavy_init: VecDeque<QueuedJob>,
1408    mutating: VecDeque<QueuedJob>,
1409    maintenance_commit: VecDeque<QueuedJob>,
1410}
1411
1412impl ClassQueues {
1413    fn new() -> Self {
1414        Self {
1415            order: VecDeque::new(),
1416            pure_reads: VecDeque::new(),
1417            lsp_status: VecDeque::new(),
1418            heavy_init: VecDeque::new(),
1419            mutating: VecDeque::new(),
1420            maintenance_commit: VecDeque::new(),
1421        }
1422    }
1423
1424    fn push_job(&mut self, lane: Lane, job: QueuedJob) {
1425        self.order.push_back(lane);
1426        self.queue_mut(lane).push_back(job);
1427    }
1428
1429    fn has_queued_jobs(&self) -> bool {
1430        !self.order.is_empty()
1431    }
1432
1433    fn front_lane(&self) -> Option<Lane> {
1434        self.order.front().copied()
1435    }
1436
1437    /// Interactive admission order: a hard-starved configure (queued RouteBind
1438    /// older than the promotion age) preempts everything so its daemon deadline
1439    /// survives; otherwise pure reads go first (they overlap each other and
1440    /// never barrier the actor), then remaining lanes in arrival order.
1441    /// Maintenance keeps strict arrival order via `front_lane`.
1442    fn next_interactive_lane(&self, now: Instant) -> Option<Lane> {
1443        let starved_writer = self.mutating.iter().any(|job| {
1444            now.saturating_duration_since(job.queued_at) >= INTERACTIVE_WRITER_PROMOTION_AGE
1445        });
1446        if starved_writer {
1447            // Also stops NEW readers from being admitted on this actor while
1448            // the promoted writer waits for in-flight readers to drain.
1449            return Some(Lane::Mutating);
1450        }
1451        if !self.pure_reads.is_empty() {
1452            return Some(Lane::PureRead);
1453        }
1454        self.order
1455            .iter()
1456            .copied()
1457            .find(|lane| *lane != Lane::PureRead)
1458    }
1459
1460    fn pop_front_job(&mut self, lane: Lane) -> Option<QueuedJob> {
1461        // Keep `order` consistent with per-lane queues when admission picks a
1462        // lane other than the arrival-order head: remove the FIRST occurrence
1463        // of the chosen lane from `order`, not necessarily the front.
1464        let position = self.order.iter().position(|queued| *queued == lane)?;
1465        self.order.remove(position);
1466        self.queue_mut(lane).pop_front()
1467    }
1468
1469    fn queued_count(&self) -> usize {
1470        self.order.len()
1471    }
1472
1473    fn has_maintenance_coalesce_key(&self, key: MaintenanceCoalesceKey) -> bool {
1474        [
1475            Lane::PureRead,
1476            Lane::SerialLspStatus,
1477            Lane::HeavyInit,
1478            Lane::Mutating,
1479            Lane::MaintenanceCommit,
1480        ]
1481        .into_iter()
1482        .any(|lane| {
1483            self.queue(lane)
1484                .iter()
1485                .any(|job| job.maintenance_coalesce_key == Some(key))
1486        })
1487    }
1488
1489    /// Remove only redundant idempotent drains while preserving survivor order.
1490    fn remove_duplicate_maintenance_jobs(&mut self) -> Vec<QueuedJob> {
1491        let mut seen = HashSet::new();
1492        let mut lane_positions = [0usize; 5];
1493        let mut duplicates = Vec::new();
1494
1495        for (order_position, lane) in self.order.iter().copied().enumerate() {
1496            let lane_index = lane_index(lane);
1497            let queue_position = lane_positions[lane_index];
1498            lane_positions[lane_index] += 1;
1499            let Some(key) = self
1500                .queue(lane)
1501                .get(queue_position)
1502                .and_then(|job| job.maintenance_coalesce_key)
1503            else {
1504                continue;
1505            };
1506            if !seen.insert(key) {
1507                duplicates.push((order_position, lane, queue_position));
1508            }
1509        }
1510
1511        let mut removed = Vec::with_capacity(duplicates.len());
1512        for (order_position, lane, queue_position) in duplicates.into_iter().rev() {
1513            self.order.remove(order_position);
1514            if let Some(job) = self.queue_mut(lane).remove(queue_position) {
1515                removed.push(job);
1516            }
1517        }
1518        removed
1519    }
1520
1521    fn oldest_queued_at(&self) -> Option<Instant> {
1522        self.front_lane()
1523            .and_then(|lane| self.queue(lane).front().map(|job| job.queued_at))
1524    }
1525
1526    fn fail_queued_jobs(&mut self) {
1527        self.order.clear();
1528        fail_queued_job_queue(&mut self.pure_reads);
1529        fail_queued_job_queue(&mut self.lsp_status);
1530        fail_queued_job_queue(&mut self.heavy_init);
1531        fail_queued_job_queue(&mut self.mutating);
1532        fail_queued_job_queue(&mut self.maintenance_commit);
1533    }
1534
1535    fn cancel_queued_jobs(&mut self) -> usize {
1536        self.order.clear();
1537        cancel_queued_job_queue(&mut self.pure_reads)
1538            + cancel_queued_job_queue(&mut self.lsp_status)
1539            + cancel_queued_job_queue(&mut self.heavy_init)
1540            + cancel_queued_job_queue(&mut self.mutating)
1541            + cancel_queued_job_queue(&mut self.maintenance_commit)
1542    }
1543
1544    fn has_queued_mutating_job(&self, request_id: &str) -> bool {
1545        self.mutating.iter().any(|job| job.request_id == request_id)
1546    }
1547
1548    /// Remove the queued job carrying this exact cancellation token.
1549    ///
1550    /// `order` holds one lane entry per push, and per-lane entries pair FIFO
1551    /// with the lane queue: the k-th occurrence of a lane in `order`
1552    /// corresponds to the k-th element of that lane's queue. Removing the
1553    /// FIRST matching order entry for a job deeper in its lane queue would
1554    /// shift the pairing and reorder the survivors, so the occurrence at the
1555    /// job's own queue position is removed instead.
1556    fn remove_cancellable(&mut self, token: &JobCancellation) -> Option<QueuedJob> {
1557        for lane in [
1558            Lane::PureRead,
1559            Lane::SerialLspStatus,
1560            Lane::HeavyInit,
1561            Lane::Mutating,
1562            Lane::MaintenanceCommit,
1563        ] {
1564            let queue = self.queue_mut(lane);
1565            let position = queue.iter().position(|queued| {
1566                queued
1567                    .cancellation
1568                    .as_ref()
1569                    .is_some_and(|candidate| candidate.same_token(token))
1570            });
1571            if let Some(position) = position {
1572                let removed = queue.remove(position);
1573                let mut occurrence = 0usize;
1574                if let Some(order_position) = self.order.iter().position(|entry| {
1575                    if *entry != lane {
1576                        return false;
1577                    }
1578                    let matched = occurrence == position;
1579                    occurrence += 1;
1580                    matched
1581                }) {
1582                    self.order.remove(order_position);
1583                }
1584                return removed;
1585            }
1586        }
1587        None
1588    }
1589
1590    fn queued_configure_count(&self) -> usize {
1591        self.mutating
1592            .iter()
1593            .filter(|job| is_configure_request(&job.request_id))
1594            .count()
1595    }
1596
1597    fn queue(&self, lane: Lane) -> &VecDeque<QueuedJob> {
1598        match lane {
1599            Lane::PureRead => &self.pure_reads,
1600            Lane::SerialLspStatus => &self.lsp_status,
1601            Lane::HeavyInit => &self.heavy_init,
1602            Lane::Mutating => &self.mutating,
1603            Lane::MaintenanceCommit => &self.maintenance_commit,
1604        }
1605    }
1606
1607    fn queue_mut(&mut self, lane: Lane) -> &mut VecDeque<QueuedJob> {
1608        match lane {
1609            Lane::PureRead => &mut self.pure_reads,
1610            Lane::SerialLspStatus => &mut self.lsp_status,
1611            Lane::HeavyInit => &mut self.heavy_init,
1612            Lane::Mutating => &mut self.mutating,
1613            Lane::MaintenanceCommit => &mut self.maintenance_commit,
1614        }
1615    }
1616}
1617
1618struct QueuedJob {
1619    job: ExecutorJob,
1620    completion: CompletionSender,
1621    request_id: String,
1622    command: String,
1623    queued_at: Instant,
1624    cancellation: Option<JobCancellation>,
1625    maintenance_coalesce_key: Option<MaintenanceCoalesceKey>,
1626}
1627
1628fn lane_index(lane: Lane) -> usize {
1629    match lane {
1630        Lane::PureRead => 0,
1631        Lane::SerialLspStatus => 1,
1632        Lane::HeavyInit => 2,
1633        Lane::Mutating => 3,
1634        Lane::MaintenanceCommit => 4,
1635    }
1636}
1637
1638fn fail_queued_job_queue(queue: &mut VecDeque<QueuedJob>) {
1639    for queued in queue.drain(..) {
1640        queued
1641            .completion
1642            .send(actor_fatal_response(queued.request_id));
1643    }
1644}
1645
1646fn cancel_queued_job_queue(queue: &mut VecDeque<QueuedJob>) -> usize {
1647    let cancelled = queue.len();
1648    for queued in queue.drain(..) {
1649        queued.completion.send(Response::error(
1650            queued.request_id,
1651            "maintenance_cancelled",
1652            "maintenance cancelled because the actor has no bound routes",
1653        ));
1654    }
1655    cancelled
1656}
1657
1658fn job_command(job_class: JobClass, lane: Lane) -> String {
1659    format!("executor::{job_class:?}::{lane:?}")
1660}
1661
1662fn maintenance_cancelled_response(
1663    request_id: impl Into<String>,
1664    message: impl Into<String>,
1665) -> Response {
1666    Response::error(request_id, "maintenance_cancelled", message)
1667}
1668
1669fn maintenance_backpressure_response(request_id: impl Into<String>) -> Response {
1670    Response::error_with_data(
1671        request_id,
1672        "maintenance_backpressure",
1673        format!("maintenance queue reached its per-actor capacity of {MAINTENANCE_QUEUE_CAP} jobs"),
1674        serde_json::json!({
1675            "retryable": true,
1676            "queue_cap": MAINTENANCE_QUEUE_CAP,
1677        }),
1678    )
1679}
1680
1681fn actor_fatal_response(request_id: impl Into<String>) -> Response {
1682    Response::error(
1683        request_id,
1684        "actor_fatal",
1685        "executor actor is fatal after a mutating job panic",
1686    )
1687}
1688
1689fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> String {
1690    if let Some(message) = payload.downcast_ref::<&'static str>() {
1691        (*message).to_string()
1692    } else if let Some(message) = payload.downcast_ref::<String>() {
1693        message.clone()
1694    } else {
1695        "unknown panic payload".to_string()
1696    }
1697}
1698
1699fn panic_response(
1700    request_id: impl Into<String>,
1701    command: &str,
1702    payload: &(dyn std::any::Any + Send),
1703) -> Response {
1704    let panic_message = panic_payload_message(payload);
1705    Response::error(
1706        request_id,
1707        "actor_fatal",
1708        format!("command '{command}' panicked: {panic_message}"),
1709    )
1710}
1711
1712enum CompletionSender {
1713    Sync(Sender<Response>),
1714    Async(oneshot::Sender<Response>),
1715}
1716
1717impl CompletionSender {
1718    fn send(self, response: Response) {
1719        match self {
1720            Self::Sync(tx) => {
1721                let _ = tx.send(response);
1722            }
1723            Self::Async(tx) => {
1724                let _ = tx.send(response);
1725            }
1726        }
1727    }
1728}
1729
1730struct RunJob {
1731    root_id: ProjectRootId,
1732    job_class: JobClass,
1733    lane: Lane,
1734    ctx: Arc<AppContext>,
1735    epoch: Arc<RwLock<()>>,
1736    job: ExecutorJob,
1737    completion: Option<CompletionSender>,
1738    request_id: String,
1739    command: String,
1740    heavy_permit: Option<HeavyPermit>,
1741    cancellation: Option<JobCancellation>,
1742}
1743
1744struct CompletionEvent {
1745    root_id: ProjectRootId,
1746    request_id: String,
1747    job_class: JobClass,
1748    lane: Lane,
1749    heavy_permit: Option<HeavyPermit>,
1750    panicked: bool,
1751}
1752
1753enum SchedulerEvent {
1754    Wake,
1755    Completed(CompletionEvent),
1756    Shutdown,
1757}
1758
1759fn scheduler_loop(
1760    state: Arc<Mutex<SchedulerState>>,
1761    heavy: Arc<HeavySemaphore>,
1762    run_tx: Sender<RunJob>,
1763    event_rx: Receiver<SchedulerEvent>,
1764    nonrunnable_dispatches: Arc<AtomicUsize>,
1765    completed_interactive: Arc<AtomicU64>,
1766    completed_maintenance: Arc<AtomicU64>,
1767    dispatch_liveness: Arc<DispatchLivenessAtomics>,
1768) {
1769    while let Ok(event) = event_rx.recv() {
1770        let mut shutdown = false;
1771        {
1772            let mut state = state.lock();
1773            note_completion_event(&event, &completed_interactive, &completed_maintenance);
1774            shutdown |= process_scheduler_event(event, &mut state);
1775            while !shutdown {
1776                match event_rx.try_recv() {
1777                    Ok(event) => {
1778                        note_completion_event(
1779                            &event,
1780                            &completed_interactive,
1781                            &completed_maintenance,
1782                        );
1783                        shutdown |= process_scheduler_event(event, &mut state)
1784                    }
1785                    Err(_) => break,
1786                }
1787            }
1788
1789            if !shutdown {
1790                dispatch_runnable(&mut state, &heavy, &run_tx, &nonrunnable_dispatches);
1791            }
1792            dispatch_liveness.record(&state.dispatch_liveness_snapshot());
1793        }
1794
1795        if shutdown {
1796            break;
1797        }
1798    }
1799}
1800
1801fn note_completion_event(
1802    event: &SchedulerEvent,
1803    completed_interactive: &AtomicU64,
1804    completed_maintenance: &AtomicU64,
1805) {
1806    let SchedulerEvent::Completed(event) = event else {
1807        return;
1808    };
1809    match event.job_class {
1810        JobClass::Interactive => {
1811            completed_interactive.fetch_add(1, Ordering::Relaxed);
1812        }
1813        JobClass::Maintenance => {
1814            completed_maintenance.fetch_add(1, Ordering::Relaxed);
1815        }
1816    }
1817}
1818
1819fn process_scheduler_event(event: SchedulerEvent, state: &mut SchedulerState) -> bool {
1820    match event {
1821        SchedulerEvent::Wake => false,
1822        SchedulerEvent::Completed(event) => {
1823            complete_job(state, event);
1824            false
1825        }
1826        SchedulerEvent::Shutdown => true,
1827    }
1828}
1829
1830fn complete_job(state: &mut SchedulerState, event: CompletionEvent) {
1831    let CompletionEvent {
1832        root_id,
1833        request_id,
1834        job_class,
1835        lane,
1836        heavy_permit,
1837        panicked,
1838    } = event;
1839    state.running_jobs.remove(&(root_id.clone(), request_id));
1840
1841    match job_class {
1842        JobClass::Interactive => {
1843            state.interactive_inflight = state.interactive_inflight.saturating_sub(1);
1844        }
1845        JobClass::Maintenance => {
1846            state.maintenance_inflight = state.maintenance_inflight.saturating_sub(1);
1847        }
1848    }
1849
1850    if let Some(actor) = state.actors.get_mut(&root_id) {
1851        actor.actor_total_inflight = actor.actor_total_inflight.saturating_sub(1);
1852        match lane {
1853            Lane::PureRead => {
1854                actor.read_inflight = actor.read_inflight.saturating_sub(1);
1855            }
1856            Lane::SerialLspStatus => {
1857                actor.lsp_inflight = false;
1858            }
1859            Lane::HeavyInit => {}
1860            Lane::Mutating => {
1861                actor.writer_inflight = false;
1862                actor.mutating_inflight = None;
1863            }
1864            Lane::MaintenanceCommit => {
1865                actor.maintenance_commit_inflight = false;
1866            }
1867        }
1868
1869        if panicked && lane == Lane::Mutating {
1870            actor.fatal = true;
1871            actor.fail_queued_jobs();
1872        }
1873    }
1874
1875    drop(heavy_permit);
1876    state.idle_workers += 1;
1877}
1878
1879fn dispatch_runnable(
1880    state: &mut SchedulerState,
1881    heavy: &Arc<HeavySemaphore>,
1882    run_tx: &Sender<RunJob>,
1883    nonrunnable_dispatches: &AtomicUsize,
1884) {
1885    while state.idle_workers > 0 && !state.actor_order.is_empty() {
1886        let mut made_progress = false;
1887        let mut dispatch_failed = false;
1888
1889        made_progress |= dispatch_runnable_class(
1890            state,
1891            JobClass::Interactive,
1892            heavy,
1893            run_tx,
1894            nonrunnable_dispatches,
1895            &mut dispatch_failed,
1896        );
1897        if dispatch_failed || state.idle_workers == 0 {
1898            return;
1899        }
1900
1901        if can_dispatch_class(state, JobClass::Maintenance) {
1902            made_progress |= dispatch_runnable_class(
1903                state,
1904                JobClass::Maintenance,
1905                heavy,
1906                run_tx,
1907                nonrunnable_dispatches,
1908                &mut dispatch_failed,
1909            );
1910            if dispatch_failed {
1911                return;
1912            }
1913        }
1914
1915        if !made_progress {
1916            break;
1917        }
1918    }
1919}
1920
1921fn dispatch_runnable_class(
1922    state: &mut SchedulerState,
1923    job_class: JobClass,
1924    heavy: &Arc<HeavySemaphore>,
1925    run_tx: &Sender<RunJob>,
1926    nonrunnable_dispatches: &AtomicUsize,
1927    dispatch_failed: &mut bool,
1928) -> bool {
1929    if !can_dispatch_class(state, job_class) || state.actor_order.is_empty() {
1930        return false;
1931    }
1932
1933    let actor_count = state.actor_order.len();
1934    let mut made_progress = false;
1935
1936    for _ in 0..actor_count {
1937        if !can_dispatch_class(state, job_class) || state.actor_order.is_empty() {
1938            break;
1939        }
1940
1941        if state.cursor >= state.actor_order.len() {
1942            state.cursor = 0;
1943        }
1944        let root_id = state.actor_order[state.cursor].clone();
1945        state.cursor = (state.cursor + 1) % state.actor_order.len();
1946
1947        let run_job = {
1948            let Some(actor) = state.actors.get_mut(&root_id) else {
1949                continue;
1950            };
1951
1952            if actor.fatal {
1953                actor.fail_queued_jobs();
1954                actor.deficit = 0;
1955                continue;
1956            }
1957
1958            if !actor.has_queued_jobs() {
1959                actor.deficit = 0;
1960                continue;
1961            }
1962
1963            if !actor.has_queued_jobs_for(job_class) {
1964                continue;
1965            }
1966
1967            actor.deficit =
1968                (actor.deficit + state.config.drr_quantum).min(state.config.deficit_cap);
1969            if actor.deficit < JOB_COST {
1970                continue;
1971            }
1972
1973            try_admit_actor(&root_id, actor, job_class, &state.config, heavy)
1974        };
1975
1976        if let Some(run_job) = run_job {
1977            state.running_jobs.insert(
1978                (run_job.root_id.clone(), run_job.request_id.clone()),
1979                RunningJob {
1980                    root_id: run_job.root_id.clone(),
1981                    request_id: run_job.request_id.clone(),
1982                    command: run_job.command.clone(),
1983                    job_class: run_job.job_class,
1984                    lane: run_job.lane,
1985                    started_at: Instant::now(),
1986                },
1987            );
1988            state.idle_workers -= 1;
1989            match job_class {
1990                JobClass::Interactive => state.interactive_inflight += 1,
1991                JobClass::Maintenance => state.maintenance_inflight += 1,
1992            }
1993            made_progress = true;
1994            if run_tx.send(run_job).is_err() {
1995                nonrunnable_dispatches.fetch_add(1, Ordering::AcqRel);
1996                *dispatch_failed = true;
1997                return made_progress;
1998            }
1999        }
2000    }
2001
2002    made_progress
2003}
2004
2005fn can_dispatch_class(state: &SchedulerState, job_class: JobClass) -> bool {
2006    if state.idle_workers == 0 {
2007        return false;
2008    }
2009    match job_class {
2010        JobClass::Interactive => true,
2011        JobClass::Maintenance => {
2012            state.maintenance_inflight < state.config.maintenance_cap
2013                && state.idle_workers > state.config.interactive_reserve
2014        }
2015    }
2016}
2017
2018fn try_admit_actor(
2019    root_id: &ProjectRootId,
2020    actor: &mut ActorState,
2021    job_class: JobClass,
2022    config: &EffectiveConfig,
2023    heavy: &Arc<HeavySemaphore>,
2024) -> Option<RunJob> {
2025    let lane = match job_class {
2026        JobClass::Interactive => actor
2027            .class_queues(JobClass::Interactive)
2028            .next_interactive_lane(Instant::now())?,
2029        JobClass::Maintenance => actor.front_lane(job_class)?,
2030    };
2031    let mut heavy_permit = None;
2032
2033    if actor.writer_inflight || actor.higher_priority_writer_barrier_blocks(job_class) {
2034        return None;
2035    }
2036
2037    let has_epoch_reader =
2038        actor.read_inflight > 0 || actor.lsp_inflight || actor.maintenance_commit_inflight;
2039    let actor_has_capacity = actor.actor_total_inflight < config.actor_cap;
2040    let runnable = match lane {
2041        Lane::PureRead => actor.read_inflight < config.read_cap && actor_has_capacity,
2042        Lane::SerialLspStatus => !actor.lsp_inflight && actor_has_capacity,
2043        Lane::HeavyInit => {
2044            if !actor_has_capacity {
2045                false
2046            } else if let Some(permit) = heavy.try_acquire() {
2047                heavy_permit = Some(permit);
2048                true
2049            } else {
2050                false
2051            }
2052        }
2053        Lane::Mutating => !has_epoch_reader && actor_has_capacity,
2054        // Overlaps reads (epoch read gate); one in flight per actor so
2055        // maintenance cannot stack; a running writer blocks it like reads.
2056        Lane::MaintenanceCommit => !actor.maintenance_commit_inflight && actor_has_capacity,
2057    };
2058
2059    if !runnable {
2060        return None;
2061    }
2062
2063    let queued = actor.pop_front_job(job_class, lane)?;
2064    actor.deficit -= JOB_COST;
2065    if let Some(cancellation) = queued.cancellation.as_ref() {
2066        cancellation.mark_running();
2067    }
2068    if lane == Lane::Mutating {
2069        actor.mutating_inflight = Some(RunningMutatingJob {
2070            request_id: queued.request_id.clone(),
2071            command: queued.command.clone(),
2072            started_at: Instant::now(),
2073        });
2074    }
2075    match lane {
2076        Lane::PureRead => {
2077            actor.read_inflight += 1;
2078            actor.actor_total_inflight += 1;
2079        }
2080        Lane::SerialLspStatus => {
2081            actor.lsp_inflight = true;
2082            actor.actor_total_inflight += 1;
2083        }
2084        Lane::HeavyInit => {
2085            actor.actor_total_inflight += 1;
2086        }
2087        Lane::Mutating => {
2088            actor.writer_inflight = true;
2089            actor.actor_total_inflight += 1;
2090        }
2091        Lane::MaintenanceCommit => {
2092            actor.maintenance_commit_inflight = true;
2093            actor.actor_total_inflight += 1;
2094        }
2095    }
2096
2097    Some(RunJob {
2098        root_id: root_id.clone(),
2099        job_class,
2100        lane,
2101        ctx: Arc::clone(&actor.ctx),
2102        epoch: Arc::clone(&actor.epoch),
2103        job: queued.job,
2104        completion: Some(queued.completion),
2105        request_id: queued.request_id,
2106        command: queued.command,
2107        heavy_permit,
2108        cancellation: queued.cancellation,
2109    })
2110}
2111
2112fn worker_loop(run_rx: Receiver<RunJob>, event_tx: Sender<SchedulerEvent>) {
2113    while let Ok(mut run_job) = run_rx.recv() {
2114        let response =
2115            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run_lane_job(&mut run_job)));
2116        let panicked = response.is_err();
2117        let response = match response {
2118            Ok(response) => response,
2119            Err(payload) => panic_response(
2120                run_job.request_id.clone(),
2121                &run_job.command,
2122                payload.as_ref(),
2123            ),
2124        };
2125
2126        if let Some(completion) = run_job.completion.take() {
2127            completion.send(response);
2128        }
2129        let completion = CompletionEvent {
2130            root_id: run_job.root_id,
2131            request_id: run_job.request_id,
2132            job_class: run_job.job_class,
2133            lane: run_job.lane,
2134            heavy_permit: run_job.heavy_permit.take(),
2135            panicked,
2136        };
2137        let _ = event_tx.send(SchedulerEvent::Completed(completion));
2138    }
2139}
2140
2141fn run_lane_job(run_job: &mut RunJob) -> Response {
2142    let _cancellation_ctx = CurrentJobCancellationGuard::install(run_job.cancellation.clone());
2143    let missing_request_id = run_job.request_id.clone();
2144    let job = std::mem::replace(
2145        &mut run_job.job,
2146        Box::new(move |_| {
2147            Response::error(
2148                missing_request_id,
2149                "job_missing",
2150                "executor job already taken",
2151            )
2152        }),
2153    );
2154
2155    match run_job.lane {
2156        Lane::PureRead | Lane::SerialLspStatus => {
2157            let _epoch = run_job.epoch.read();
2158            job(&run_job.ctx)
2159        }
2160        Lane::HeavyInit => {
2161            let response = job(&run_job.ctx);
2162            {
2163                let _install = run_job.epoch.write();
2164            }
2165            response
2166        }
2167        Lane::Mutating => {
2168            let _epoch = run_job.epoch.write();
2169            job(&run_job.ctx)
2170        }
2171        Lane::MaintenanceCommit => {
2172            // Same gate as reads: the job's mutations are protected by the
2173            // touched subsystems' own locks, and holding only the read gate
2174            // lets interactive PureReads overlap freely.
2175            let _epoch = run_job.epoch.read();
2176            job(&run_job.ctx)
2177        }
2178    }
2179}
2180
2181#[derive(Debug)]
2182struct HeavySemaphore {
2183    available: AtomicUsize,
2184    max: usize,
2185}
2186
2187impl HeavySemaphore {
2188    fn new(permits: usize) -> Self {
2189        Self {
2190            available: AtomicUsize::new(permits),
2191            max: permits,
2192        }
2193    }
2194
2195    fn try_acquire(self: &Arc<Self>) -> Option<HeavyPermit> {
2196        loop {
2197            let available = self.available.load(Ordering::Acquire);
2198            if available == 0 {
2199                return None;
2200            }
2201            if self
2202                .available
2203                .compare_exchange(
2204                    available,
2205                    available - 1,
2206                    Ordering::AcqRel,
2207                    Ordering::Acquire,
2208                )
2209                .is_ok()
2210            {
2211                return Some(HeavyPermit {
2212                    semaphore: Arc::clone(self),
2213                });
2214            }
2215        }
2216    }
2217}
2218
2219struct HeavyPermit {
2220    semaphore: Arc<HeavySemaphore>,
2221}
2222
2223impl Drop for HeavyPermit {
2224    fn drop(&mut self) {
2225        let previous = self.semaphore.available.fetch_add(1, Ordering::Release);
2226        debug_assert!(previous < self.semaphore.max);
2227    }
2228}