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