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