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