Skip to main content

aft/executor/
mod.rs

1mod single_flight;
2pub(crate) mod view_publication;
3
4#[cfg(test)]
5mod tests;
6
7use std::{
8    collections::{HashMap, HashSet, VecDeque},
9    sync::{
10        atomic::{AtomicBool, AtomicU64, AtomicU8, AtomicUsize, Ordering},
11        Arc, Weak,
12    },
13    thread::{self, JoinHandle},
14    time::{Duration, Instant},
15};
16
17use crossbeam_channel::{Receiver, RecvError, RecvTimeoutError, Sender};
18use parking_lot::{Condvar, Mutex, RwLock};
19use tokio::sync::oneshot;
20
21use crate::{context::AppContext, path_identity::ProjectRootId, protocol::Response};
22
23pub use single_flight::SingleFlight;
24
25const JOB_COST: isize = 1;
26/// Continuous wake producers must not keep the scheduler mutex across an
27/// unbounded receiver drain; health probes and submitters get a lock turn
28/// between batches.
29const SCHEDULER_EVENT_BATCH_CAP: usize = 64;
30pub(crate) const MAINTENANCE_QUEUE_CAP: usize = 512;
31
32/// Scheduler lane for command-handler execution.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub enum Lane {
35    /// Pure read-only work. Runs under the actor epoch read gate and is capped
36    /// per actor.
37    PureRead,
38    /// LSP/status work. Serialized per actor by scheduler admission while still
39    /// using the shared epoch read gate.
40    SerialLspStatus,
41    /// Heavy lazy initialization. The scheduler acquires a process-wide heavy
42    /// permit before dispatch; cache and index builders reject stale generations
43    /// and serialize publication internally, so no actor epoch guard wraps the
44    /// command or the delivery of its response.
45    HeavyInit,
46    /// Mutating work. Becomes a writer barrier at the actor queue head, drains
47    /// in-flight reads, and runs under the actor epoch write gate. Reserved for
48    /// configure and user-initiated tool mutations: background maintenance must
49    /// use `MaintenanceCommit` so it can never exclude interactive reads.
50    Mutating,
51    /// Maintenance work that mutates only subsystem state behind that
52    /// subsystem's own lock (watcher/LSP drains, completed-build installs,
53    /// callgraph store writes). Runs under the actor epoch READ gate and
54    /// overlaps PureReads; serialized to one in-flight per actor so
55    /// maintenance cannot self-stack.
56    MaintenanceCommit,
57}
58
59/// Scheduler class used to keep deferrable maintenance from occupying the
60/// workers reserved for interactive route binds, tool calls, and bash.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub enum JobClass {
63    Interactive,
64    Maintenance,
65}
66
67/// Idempotent maintenance drains that can share one queued execution.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub(crate) enum MaintenanceCoalesceKey {
70    WatcherDrain,
71    LspDrain,
72    /// Standing maintenance is coalesced inside each actor queue. The executor
73    /// already scopes queue state by root, so this key cannot coalesce work for
74    /// separate standing roots.
75    StandingPass,
76}
77
78pub type ExecutorJob = Box<dyn FnOnce(&AppContext) -> Response + Send + 'static>;
79
80/// Age at which a queued interactive mutating job (route binds, edits) jumps
81/// ahead of pure reads in interactive admission. Reads-first admission would
82/// otherwise let a sustained read stream starve queued writers; this bounds
83/// that wait. For binds it matches the half-deadline breadcrumb point: the
84/// daemon rejects binds at 12s, so promotion at 6s leaves half the budget for
85/// draining readers and running the configure itself.
86const INTERACTIVE_WRITER_PROMOTION_AGE: Duration = Duration::from_secs(6);
87/// Readers older than this still block queued writer jobs; report their job
88/// metadata instead of only the generic `waiting_on_readers` diagnosis.
89const READER_STUCK_CENSUS_AGE: Duration = Duration::from_secs(60);
90/// Stack for executor worker threads: the standalone main thread's size, so a
91/// request that fits there cannot overflow only under the daemon. Stack pages
92/// are committed on touch, so idle workers cost nothing extra.
93const EXECUTOR_WORKER_STACK_BYTES: usize = 8 * 1024 * 1024;
94
95#[derive(Debug, Clone)]
96pub struct ExecutorConfig {
97    pub pool_size: usize,
98    pub read_cap: usize,
99    pub actor_cap: usize,
100    pub heavy_permits: usize,
101    pub drr_quantum: isize,
102}
103
104impl Default for ExecutorConfig {
105    fn default() -> Self {
106        let available = thread::available_parallelism()
107            .map(usize::from)
108            .unwrap_or(2);
109        let pool_size = available.saturating_sub(1).clamp(2, 8);
110        let actor_cap = pool_size.saturating_sub(1).clamp(1, 4);
111        let read_cap = actor_cap.clamp(1, 4);
112        let heavy_permits = pool_size.saturating_sub(1).clamp(2, 3);
113
114        Self {
115            pool_size,
116            read_cap,
117            actor_cap,
118            heavy_permits,
119            drr_quantum: 1,
120        }
121    }
122}
123
124#[derive(Debug, Clone)]
125struct EffectiveConfig {
126    pool_size: usize,
127    read_cap: usize,
128    actor_cap: usize,
129    heavy_permits: usize,
130    drr_quantum: isize,
131    deficit_cap: isize,
132    interactive_reserve: usize,
133    maintenance_cap: usize,
134}
135
136impl ExecutorConfig {
137    fn effective(&self) -> EffectiveConfig {
138        let pool_size = self.pool_size.clamp(2, 8);
139        let max_actor_cap = pool_size.saturating_sub(1).max(1);
140        let actor_cap = self.actor_cap.max(1).min(max_actor_cap);
141        let read_cap = self.read_cap.max(1).min(actor_cap).min(4);
142        // HeavyInit jobs share workers with RouteBind/configure. Keep one worker
143        // available even in a two-worker pool so a heavy-init storm cannot hold
144        // a fresh bind behind every executor worker.
145        let heavy_permits = self
146            .heavy_permits
147            .max(1)
148            .min(pool_size.saturating_sub(1).max(1))
149            .min(3);
150        let drr_quantum = self.drr_quantum.max(1);
151        let deficit_cap = (actor_cap.max(1) as isize) * 4;
152        let interactive_reserve = if pool_size >= 4 { 2 } else { 1 };
153        let maintenance_cap = pool_size.saturating_sub(interactive_reserve).max(1);
154
155        EffectiveConfig {
156            pool_size,
157            read_cap,
158            actor_cap,
159            heavy_permits,
160            drr_quantum,
161            deficit_cap,
162            interactive_reserve,
163            maintenance_cap,
164        }
165    }
166}
167
168/// Synchronous completion handle used by the executor tests and the
169/// future standalone bridge.
170pub struct CompletionHandle {
171    rx: Receiver<Response>,
172}
173
174impl CompletionHandle {
175    pub fn recv(self) -> Result<Response, RecvError> {
176        self.rx.recv()
177    }
178
179    pub fn recv_timeout(&self, timeout: Duration) -> Result<Response, RecvTimeoutError> {
180        self.rx.recv_timeout(timeout)
181    }
182
183    pub fn into_receiver(self) -> Receiver<Response> {
184        self.rx
185    }
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct DispatchClassQueueSnapshot {
190    pub queued: usize,
191    pub oldest_age_ms: Option<u64>,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct DispatchRunningSnapshot {
196    pub interactive: usize,
197    pub maintenance: usize,
198    pub completion_owned_interactive: usize,
199    pub completion_owned_maintenance: usize,
200    /// Age of the oldest maintenance reservation still owned by a dispatched
201    /// worker. This distinguishes a blocked worker from a phantom reservation.
202    pub oldest_worker_owned_maintenance_age_ms: Option<u64>,
203    pub phantom_interactive: usize,
204    pub phantom_maintenance: usize,
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct DispatchLivenessSnapshot {
209    pub interactive: DispatchClassQueueSnapshot,
210    pub maintenance: DispatchClassQueueSnapshot,
211    pub running: DispatchRunningSnapshot,
212    pub phantom_running_maintenance: usize,
213    pub interactive_reserve: usize,
214    pub maintenance_cap: usize,
215}
216
217/// Scheduler-owned mirror read by health probes without taking the actor map
218/// lock or scanning every root. Oldest queue timestamps use `0` for absent and
219/// elapsed-milliseconds-plus-one for a present timestamp.
220struct DispatchLivenessAtomics {
221    origin: Instant,
222    interactive_queued: AtomicUsize,
223    interactive_oldest_enqueued_ms_plus_one: AtomicU64,
224    maintenance_queued: AtomicUsize,
225    maintenance_oldest_enqueued_ms_plus_one: AtomicU64,
226    interactive_running: AtomicUsize,
227    maintenance_running: AtomicUsize,
228    completion_owned_interactive: AtomicUsize,
229    completion_owned_maintenance: AtomicUsize,
230    oldest_worker_owned_maintenance_started_ms_plus_one: AtomicU64,
231    phantom_interactive: AtomicUsize,
232    phantom_maintenance: AtomicUsize,
233}
234
235impl DispatchLivenessAtomics {
236    fn new() -> Self {
237        Self {
238            origin: Instant::now(),
239            interactive_queued: AtomicUsize::new(0),
240            interactive_oldest_enqueued_ms_plus_one: AtomicU64::new(0),
241            maintenance_queued: AtomicUsize::new(0),
242            maintenance_oldest_enqueued_ms_plus_one: AtomicU64::new(0),
243            interactive_running: AtomicUsize::new(0),
244            maintenance_running: AtomicUsize::new(0),
245            completion_owned_interactive: AtomicUsize::new(0),
246            completion_owned_maintenance: AtomicUsize::new(0),
247            oldest_worker_owned_maintenance_started_ms_plus_one: AtomicU64::new(0),
248            phantom_interactive: AtomicUsize::new(0),
249            phantom_maintenance: AtomicUsize::new(0),
250        }
251    }
252
253    fn now_ms(&self) -> u64 {
254        duration_millis_u64(self.origin.elapsed())
255    }
256
257    fn record(&self, snapshot: &DispatchLivenessSnapshot) {
258        let now_ms = self.now_ms();
259        let encode_oldest = |queued: usize, oldest_age_ms: Option<u64>| {
260            if queued == 0 {
261                0
262            } else {
263                now_ms
264                    .saturating_sub(oldest_age_ms.unwrap_or(0))
265                    .saturating_add(1)
266            }
267        };
268        self.interactive_oldest_enqueued_ms_plus_one.store(
269            encode_oldest(
270                snapshot.interactive.queued,
271                snapshot.interactive.oldest_age_ms,
272            ),
273            Ordering::Relaxed,
274        );
275        self.maintenance_oldest_enqueued_ms_plus_one.store(
276            encode_oldest(
277                snapshot.maintenance.queued,
278                snapshot.maintenance.oldest_age_ms,
279            ),
280            Ordering::Relaxed,
281        );
282        self.interactive_running
283            .store(snapshot.running.interactive, Ordering::Release);
284        self.maintenance_running
285            .store(snapshot.running.maintenance, Ordering::Release);
286        self.completion_owned_interactive.store(
287            snapshot.running.completion_owned_interactive,
288            Ordering::Release,
289        );
290        self.completion_owned_maintenance.store(
291            snapshot.running.completion_owned_maintenance,
292            Ordering::Release,
293        );
294        self.oldest_worker_owned_maintenance_started_ms_plus_one
295            .store(
296                encode_oldest(
297                    snapshot.running.completion_owned_maintenance,
298                    snapshot.running.oldest_worker_owned_maintenance_age_ms,
299                ),
300                Ordering::Relaxed,
301            );
302        self.phantom_interactive
303            .store(snapshot.running.phantom_interactive, Ordering::Release);
304        self.phantom_maintenance
305            .store(snapshot.running.phantom_maintenance, Ordering::Release);
306        self.interactive_queued
307            .store(snapshot.interactive.queued, Ordering::Release);
308        self.maintenance_queued
309            .store(snapshot.maintenance.queued, Ordering::Release);
310    }
311
312    fn snapshot(&self, config: &EffectiveConfig) -> DispatchLivenessSnapshot {
313        let now_ms = self.now_ms();
314        let decode_oldest = |queued: usize, encoded: u64| {
315            (queued > 0 && encoded > 0).then(|| now_ms.saturating_sub(encoded - 1))
316        };
317        let interactive_queued = self.interactive_queued.load(Ordering::Acquire);
318        let maintenance_queued = self.maintenance_queued.load(Ordering::Acquire);
319        DispatchLivenessSnapshot {
320            interactive: DispatchClassQueueSnapshot {
321                queued: interactive_queued,
322                oldest_age_ms: decode_oldest(
323                    interactive_queued,
324                    self.interactive_oldest_enqueued_ms_plus_one
325                        .load(Ordering::Relaxed),
326                ),
327            },
328            maintenance: DispatchClassQueueSnapshot {
329                queued: maintenance_queued,
330                oldest_age_ms: decode_oldest(
331                    maintenance_queued,
332                    self.maintenance_oldest_enqueued_ms_plus_one
333                        .load(Ordering::Relaxed),
334                ),
335            },
336            running: DispatchRunningSnapshot {
337                interactive: self.interactive_running.load(Ordering::Acquire),
338                maintenance: self.maintenance_running.load(Ordering::Acquire),
339                completion_owned_interactive: self
340                    .completion_owned_interactive
341                    .load(Ordering::Acquire),
342                completion_owned_maintenance: self
343                    .completion_owned_maintenance
344                    .load(Ordering::Acquire),
345                oldest_worker_owned_maintenance_age_ms: decode_oldest(
346                    self.completion_owned_maintenance.load(Ordering::Acquire),
347                    self.oldest_worker_owned_maintenance_started_ms_plus_one
348                        .load(Ordering::Relaxed),
349                ),
350                phantom_interactive: self.phantom_interactive.load(Ordering::Acquire),
351                phantom_maintenance: self.phantom_maintenance.load(Ordering::Acquire),
352            },
353            phantom_running_maintenance: self.phantom_maintenance.load(Ordering::Acquire),
354            interactive_reserve: config.interactive_reserve,
355            maintenance_cap: config.maintenance_cap,
356        }
357    }
358}
359
360#[derive(Debug, Clone, PartialEq, Eq)]
361pub struct MutatingLaneSnapshot {
362    pub root_id: ProjectRootId,
363    pub request_id: String,
364    pub command: String,
365    pub started_age_ms: u64,
366}
367
368/// Non-blocking scheduler explanation attached to a delayed RouteBind warning.
369#[derive(Debug, Clone, PartialEq, Eq)]
370pub struct BindBlockerReaderSnapshot {
371    pub request_id: String,
372    pub command: String,
373    pub lane: Lane,
374    pub started_age_ms: u64,
375    pub execution_started: bool,
376    pub started_before_oldest_writer: bool,
377}
378
379#[derive(Debug, Clone, PartialEq, Eq)]
380pub struct BindBlockerSnapshot {
381    pub configure_state: &'static str,
382    pub configure_phase_timings: Option<String>,
383    pub blockers: Vec<String>,
384    pub oldest_queued_writer_age_ms: Option<u64>,
385    pub in_flight_readers: Vec<BindBlockerReaderSnapshot>,
386    pub reader_admissions_while_promoted_writer_waited: u64,
387}
388
389/// Cooperative cancellation for one cancellable executor job.
390///
391/// One atomic state machine (pending → running → committed | cancelled) is
392/// shared by the executor, the canceller, and the job. `cancel` and
393/// `try_seal_committed` race through compare-exchange on the same cell, so
394/// exactly one wins: a cancel that lands first makes the seal fail (the job
395/// must abort before mutating), and a seal that lands first makes the cancel
396/// report `RunningCommitted` (the mutation tail finishes normally). A job
397/// observes cancellation only at its own checkpoints
398/// ([`JobCancellation::cancel_requested_before_commit`]).
399#[derive(Debug, Clone)]
400pub struct JobCancellation {
401    inner: Arc<JobCancellationInner>,
402}
403
404#[derive(Debug)]
405struct JobCancellationInner {
406    state: AtomicU8,
407    wait_lock: Mutex<()>,
408    wake: Condvar,
409}
410
411const JOB_CANCEL_STATE_PENDING: u8 = 0;
412const JOB_CANCEL_STATE_RUNNING: u8 = 1;
413const JOB_CANCEL_STATE_COMMITTED: u8 = 2;
414const JOB_CANCEL_STATE_CANCELLED: u8 = 3;
415
416impl JobCancellation {
417    pub fn new() -> Self {
418        Self {
419            inner: Arc::new(JobCancellationInner {
420                state: AtomicU8::new(JOB_CANCEL_STATE_PENDING),
421                wait_lock: Mutex::new(()),
422                wake: Condvar::new(),
423            }),
424        }
425    }
426
427    fn mark_running(&self) -> bool {
428        match self.inner.state.compare_exchange(
429            JOB_CANCEL_STATE_PENDING,
430            JOB_CANCEL_STATE_RUNNING,
431            Ordering::SeqCst,
432            Ordering::SeqCst,
433        ) {
434            Ok(_) => true,
435            Err(state) => state != JOB_CANCEL_STATE_CANCELLED,
436        }
437    }
438
439    fn try_cancel_before_running(&self) -> bool {
440        let cancelled = self
441            .inner
442            .state
443            .compare_exchange(
444                JOB_CANCEL_STATE_PENDING,
445                JOB_CANCEL_STATE_CANCELLED,
446                Ordering::SeqCst,
447                Ordering::SeqCst,
448            )
449            .is_ok();
450        if cancelled {
451            let _wait_guard = self.inner.wait_lock.lock();
452            self.inner.wake.notify_all();
453        }
454        cancelled
455    }
456
457    /// Seal the job as committed. Returns `false` when a cancel already won
458    /// the race: the job must return early WITHOUT mutating, because the
459    /// canceller was told `RunningSignalled` and will discard its completion.
460    #[must_use]
461    pub fn try_seal_committed(&self) -> bool {
462        loop {
463            let current = self.state();
464            match current {
465                JOB_CANCEL_STATE_CANCELLED => return false,
466                JOB_CANCEL_STATE_COMMITTED => return true,
467                _ => {
468                    if self
469                        .inner
470                        .state
471                        .compare_exchange(
472                            current,
473                            JOB_CANCEL_STATE_COMMITTED,
474                            Ordering::SeqCst,
475                            Ordering::SeqCst,
476                        )
477                        .is_ok()
478                    {
479                        return true;
480                    }
481                }
482            }
483        }
484    }
485
486    /// Move to cancelled unless the job already sealed its commit. Returns the
487    /// state the transition observed (`COMMITTED` when the seal won).
488    fn signal_cancel(&self) -> u8 {
489        let observed = loop {
490            let current = self.state();
491            match current {
492                JOB_CANCEL_STATE_COMMITTED | JOB_CANCEL_STATE_CANCELLED => break current,
493                _ => {
494                    if self
495                        .inner
496                        .state
497                        .compare_exchange(
498                            current,
499                            JOB_CANCEL_STATE_CANCELLED,
500                            Ordering::SeqCst,
501                            Ordering::SeqCst,
502                        )
503                        .is_ok()
504                    {
505                        break current;
506                    }
507                }
508            }
509        };
510        if observed != JOB_CANCEL_STATE_COMMITTED {
511            let _wait_guard = self.inner.wait_lock.lock();
512            self.inner.wake.notify_all();
513        }
514        observed
515    }
516
517    fn state(&self) -> u8 {
518        self.inner.state.load(Ordering::SeqCst)
519    }
520
521    /// Request cooperative cancellation without requiring an executor actor.
522    /// Detached continuations use this after their setup job has already left
523    /// the scheduler's running-job table.
524    pub fn request_cancel(&self) {
525        self.signal_cancel();
526    }
527
528    /// True when a cancel won the state race and the job must abort.
529    pub fn cancel_requested_before_commit(&self) -> bool {
530        self.state() == JOB_CANCEL_STATE_CANCELLED
531    }
532
533    /// Sleep until cancellation is requested or the timeout expires.
534    ///
535    /// Cancellation takes the same lock before notifying, so a waiter cannot
536    /// miss a signal between its state check and the condition-variable wait.
537    pub fn wait_for_cancellation(&self, timeout: Duration) -> bool {
538        if self.cancel_requested_before_commit() {
539            return true;
540        }
541        let mut guard = self.inner.wait_lock.lock();
542        if self.cancel_requested_before_commit() {
543            return true;
544        }
545        self.inner.wake.wait_for(&mut guard, timeout);
546        self.cancel_requested_before_commit()
547    }
548
549    fn same_token(&self, other: &JobCancellation) -> bool {
550        Arc::ptr_eq(&self.inner, &other.inner)
551    }
552}
553
554/// Outcome of [`Executor::cancel_job`].
555#[derive(Debug, Clone, Copy, PartialEq, Eq)]
556pub enum JobCancelOutcome {
557    /// The job had not started; it was removed from the queue and its
558    /// completion settled with `request_cancelled`.
559    QueuedRemoved,
560    /// The job is running; its token is signalled and the job returns through
561    /// its normal completion path at the next checkpoint.
562    RunningSignalled,
563    /// The job already sealed its commit; it finishes normally and the caller
564    /// must discard its late completion.
565    RunningCommitted,
566    /// No queued or tracked job matched the token.
567    NotFound,
568}
569
570/// Result of cancelling only work whose command closure has not started.
571#[derive(Debug, Clone, Copy, PartialEq, Eq)]
572pub(crate) enum PreExecutionCancelOutcome {
573    QueuedRemoved,
574    DispatchedCancelled,
575    AlreadyStarted,
576}
577
578thread_local! {
579    static CURRENT_JOB_CANCELLATION: std::cell::RefCell<Option<JobCancellation>> =
580        const { std::cell::RefCell::new(None) };
581}
582
583/// The cancellation token of the job running on this worker thread, when the
584/// job was submitted through [`Executor::submit_cancellable_async`].
585pub fn current_job_cancellation() -> Option<JobCancellation> {
586    CURRENT_JOB_CANCELLATION.with(|slot| slot.borrow().clone())
587}
588
589pub fn current_job_cancelled() -> bool {
590    current_job_cancellation().is_some_and(|token| token.cancel_requested_before_commit())
591}
592
593pub struct JobCancellationContextGuard {
594    previous: Option<JobCancellation>,
595}
596
597impl JobCancellationContextGuard {
598    fn install(token: Option<JobCancellation>) -> Self {
599        let previous = CURRENT_JOB_CANCELLATION.with(|slot| slot.replace(token));
600        Self { previous }
601    }
602}
603
604impl Drop for JobCancellationContextGuard {
605    fn drop(&mut self) {
606        CURRENT_JOB_CANCELLATION.with(|slot| {
607            *slot.borrow_mut() = self.previous.take();
608        });
609    }
610}
611
612/// Install a cancellable executor job's token on a continuation thread.
613///
614/// Executor workers install this automatically. A setup job that hands work to
615/// a detached thread must install the same token there so existing cooperative
616/// checkpoints keep observing route or transport abandonment.
617pub fn install_job_cancellation(token: JobCancellation) -> JobCancellationContextGuard {
618    JobCancellationContextGuard::install(Some(token))
619}
620
621#[derive(Debug, Clone)]
622struct RunningJob {
623    root_id: ProjectRootId,
624    request_id: String,
625    command: String,
626    job_class: JobClass,
627    lane: Lane,
628    started_at: Instant,
629    execution_started: Arc<AtomicBool>,
630    completion_ownership: Weak<JobCompletionOwnership>,
631    occupancy_reported: bool,
632}
633
634#[derive(Debug, Clone)]
635pub(crate) struct LongRunningInteractiveJobSnapshot {
636    pub(crate) root_id: ProjectRootId,
637    pub(crate) request_id: String,
638    pub(crate) command: String,
639    pub(crate) lane: Lane,
640    pub(crate) age: Duration,
641    pub(crate) execution_started: bool,
642}
643
644#[derive(Debug, Clone)]
645struct RunningMutatingJob {
646    request_id: String,
647    command: String,
648    started_at: Instant,
649}
650
651/// Concurrent scheduler-dispatch executor.
652pub struct Executor {
653    inner: Arc<ExecutorInner>,
654}
655
656impl Executor {
657    pub fn new() -> Self {
658        Self::with_config(ExecutorConfig::default())
659    }
660
661    pub fn with_config(config: ExecutorConfig) -> Self {
662        let effective = config.effective();
663        let state = Arc::new(Mutex::new(SchedulerState::new(effective.clone())));
664        let heavy = Arc::new(HeavySemaphore::new(effective.heavy_permits));
665        let nonrunnable_dispatches = Arc::new(AtomicUsize::new(0));
666        let completed_interactive = Arc::new(AtomicU64::new(0));
667        let completed_maintenance = Arc::new(AtomicU64::new(0));
668        let dispatch_liveness = Arc::new(DispatchLivenessAtomics::new());
669        let (run_tx, run_rx) = crossbeam_channel::unbounded();
670        let (event_tx, event_rx) = crossbeam_channel::unbounded();
671
672        let scheduler_event_tx = event_tx.clone();
673        let scheduler_state = Arc::clone(&state);
674        let scheduler_heavy = Arc::clone(&heavy);
675        let scheduler_violations = Arc::clone(&nonrunnable_dispatches);
676        let scheduler_completed_interactive = Arc::clone(&completed_interactive);
677        let scheduler_completed_maintenance = Arc::clone(&completed_maintenance);
678        let scheduler_dispatch_liveness = Arc::clone(&dispatch_liveness);
679        let scheduler_handle = thread::Builder::new()
680            .name("aft-executor-scheduler".to_string())
681            .spawn(move || {
682                scheduler_loop(
683                    scheduler_state,
684                    scheduler_heavy,
685                    run_tx,
686                    scheduler_event_tx,
687                    event_rx,
688                    scheduler_violations,
689                    scheduler_completed_interactive,
690                    scheduler_completed_maintenance,
691                    scheduler_dispatch_liveness,
692                );
693            })
694            .expect("spawn AFT executor scheduler");
695
696        let mut worker_handles = Vec::with_capacity(effective.pool_size);
697        for worker_id in 0..effective.pool_size {
698            let worker_rx = run_rx.clone();
699            // Workers run every tool call, including tree-sitter walks over
700            // whatever a URL or file turns out to contain. The standalone loop
701            // runs those on the 8 MiB main thread; the 2 MiB default for spawned
702            // threads let a deep parse tree abort the whole daemon while the same
703            // request succeeded standalone. Match the main thread.
704            let handle = thread::Builder::new()
705                .name(format!("aft-executor-worker-{worker_id}"))
706                .stack_size(EXECUTOR_WORKER_STACK_BYTES)
707                .spawn(move || worker_loop(worker_rx))
708                .expect("spawn AFT executor worker");
709            worker_handles.push(handle);
710        }
711
712        Self {
713            inner: Arc::new(ExecutorInner {
714                state,
715                event_tx,
716                scheduler_handle: Mutex::new(Some(scheduler_handle)),
717                worker_handles: Mutex::new(worker_handles),
718                config: effective,
719                nonrunnable_dispatches,
720                completed_interactive,
721                completed_maintenance,
722                dispatch_liveness,
723            }),
724        }
725    }
726
727    /// Register an actor if one is not already present.
728    ///
729    /// Existing actors keep their current context and scheduler state; subc
730    /// routing reuses them and reconfigures through the Mutating lane
731    /// rather than replacing the per-root [`AppContext`]. Returns `true` when a
732    /// new actor was inserted.
733    pub fn register_actor(&self, root_id: ProjectRootId, ctx: Arc<AppContext>) -> bool {
734        let memory_root = root_id.as_path().to_path_buf();
735        let inserted = {
736            let mut state = self.inner.state.lock();
737            if state.actors.contains_key(&root_id) {
738                false
739            } else {
740                state.actor_order.push(root_id.clone());
741                state
742                    .actors
743                    .insert(root_id, ActorState::new(Arc::clone(&ctx)));
744                true
745            }
746        };
747        if inserted {
748            let app = ctx.app();
749            crate::root_cache::register_live_scope(&ctx.storage_dir(), &memory_root);
750            app.register_memory_context(memory_root, &ctx);
751            app.actor_root_registered();
752        }
753        self.wake_scheduler();
754        inserted
755    }
756
757    /// Remove an actor from scheduler state.
758    ///
759    /// This is intentionally minimal: subc uses it only for a just-created
760    /// RouteBind actor whose configure failed before any route was installed, so
761    /// there is no in-flight work to quiesce. The removed [`AppContext`] is
762    /// dropped after releasing the scheduler lock so watcher/LSP teardown never
763    /// runs under that mutex.
764    pub fn remove_actor(&self, root_id: &ProjectRootId) {
765        let removed = {
766            let mut state = self.inner.state.lock();
767            state.actor_order.retain(|actor_root| actor_root != root_id);
768            state.actors.remove(root_id)
769        };
770        if let Some(actor) = removed.as_ref() {
771            let app = actor.ctx.app();
772            crate::root_cache::unregister_live_scope(&actor.ctx.storage_dir(), root_id.as_path());
773            app.unregister_memory_context(root_id.as_path(), &actor.ctx);
774            app.actor_root_unregistered();
775        }
776        drop(removed);
777        self.wake_scheduler();
778    }
779
780    /// Return true only when the actor has no queued or running executor work.
781    pub fn actor_is_idle(&self, root_id: &ProjectRootId) -> bool {
782        let state = self.inner.state.lock();
783        state.actors.get(root_id).is_some_and(ActorState::is_idle)
784    }
785
786    /// Non-blocking idle probe for maintenance sweeps. A contended scheduler is
787    /// reported separately so root retirement can conservatively wait for the
788    /// next sweep without stalling the module loop.
789    pub fn try_actor_is_idle(&self, root_id: &ProjectRootId) -> Option<bool> {
790        let state = self.inner.state.try_lock()?;
791        Some(state.actors.get(root_id).is_some_and(ActorState::is_idle))
792    }
793
794    /// Forget an idle actor and drop its root-scoped registries off the scheduler
795    /// thread. This is reserved for roots whose project directory no longer
796    /// exists; retained existing roots are reused on a later bind.
797    pub fn retire_idle_actor_in_background(&self, root_id: &ProjectRootId) -> bool {
798        let removed = {
799            let mut state = self.inner.state.lock();
800            if !state.actors.get(root_id).is_some_and(ActorState::is_idle) {
801                return false;
802            }
803            state.actor_order.retain(|actor_root| actor_root != root_id);
804            state.actors.remove(root_id)
805        };
806        let Some(actor) = removed else {
807            return false;
808        };
809        let app = actor.ctx.app();
810        crate::root_cache::unregister_live_scope(&actor.ctx.storage_dir(), root_id.as_path());
811        app.unregister_memory_context(root_id.as_path(), &actor.ctx);
812        app.actor_root_unregistered();
813        std::thread::spawn(move || {
814            actor.ctx.teardown_deleted_root();
815            drop(actor);
816        });
817        self.wake_scheduler();
818        true
819    }
820
821    /// Cancel maintenance jobs that have not started for one retained actor.
822    ///
823    /// Interactive work and already-running maintenance remain untouched. Each
824    /// cancelled job receives a normal completion so its caller can settle
825    /// bookkeeping through the same path as an executed job.
826    pub fn cancel_queued_maintenance(&self, root_id: &ProjectRootId) -> usize {
827        let cancelled = {
828            let mut state = self.inner.state.lock();
829            state
830                .actors
831                .get_mut(root_id)
832                .map(|actor| actor.maintenance.cancel_queued_jobs())
833                .unwrap_or(0)
834        };
835        if cancelled > 0 {
836            self.wake_scheduler();
837        }
838        cancelled
839    }
840
841    /// Return whether scheduler state currently has an actor for this root.
842    pub fn actor_registered(&self, root_id: &ProjectRootId) -> bool {
843        let state = self.inner.state.lock();
844        state.actors.contains_key(root_id)
845    }
846
847    /// Snapshot one actor context without retaining the scheduler lock while
848    /// maintenance drops root-scoped resources.
849    pub fn actor_context(&self, root_id: &ProjectRootId) -> Option<Arc<AppContext>> {
850        let state = self.inner.state.lock();
851        state
852            .actors
853            .get(root_id)
854            .map(|actor| Arc::clone(&actor.ctx))
855    }
856
857    /// Snapshot the registered actor contexts.
858    ///
859    /// The returned [`Arc`]s keep contexts alive after the scheduler lock is
860    /// released, so callers can run teardown without holding executor state.
861    pub fn actor_contexts(&self) -> Vec<Arc<AppContext>> {
862        let state = self.inner.state.lock();
863        state
864            .actors
865            .values()
866            .map(|actor_state| Arc::clone(&actor_state.ctx))
867            .collect()
868    }
869
870    /// Snapshot the registered root ids paired with their actor contexts.
871    pub fn actor_entries(&self) -> Vec<(ProjectRootId, Arc<AppContext>)> {
872        let state = self.inner.state.lock();
873        state
874            .actors
875            .iter()
876            .map(|(root_id, actor_state)| (root_id.clone(), Arc::clone(&actor_state.ctx)))
877            .collect()
878    }
879
880    /// Non-blocking variant for the health path: the probe reply must stay
881    /// cheap under any load, so it skips the actor list (reported as busy)
882    /// rather than waiting on the scheduler state lock.
883    pub fn try_actor_entries(&self) -> Option<Vec<(ProjectRootId, Arc<AppContext>)>> {
884        let state = self.inner.state.try_lock()?;
885        Some(
886            state
887                .actors
888                .iter()
889                .map(|(root_id, actor_state)| (root_id.clone(), Arc::clone(&actor_state.ctx)))
890                .collect(),
891        )
892    }
893
894    /// Registered actor count under the scheduler lock. Tests that assert a
895    /// bind had no actor side effect use this rather than the try-lock form,
896    /// which reports contention (`None`) instead of a count.
897    pub fn actor_count(&self) -> usize {
898        self.inner.state.lock().actors.len()
899    }
900
901    /// Constant-time scheduler contention signal for the health reply path.
902    pub fn try_actor_count(&self) -> Option<usize> {
903        self.inner.state.try_lock().map(|state| state.actors.len())
904    }
905
906    pub fn submit(
907        &self,
908        root_id: ProjectRootId,
909        lane: Lane,
910        request_id: String,
911        job: ExecutorJob,
912    ) -> CompletionHandle {
913        let (completion_tx, completion_rx) = crossbeam_channel::bounded(1);
914        self.submit_with_completion(
915            root_id,
916            JobClass::Interactive,
917            lane,
918            request_id,
919            job,
920            CompletionSender::Sync(completion_tx),
921        );
922        CompletionHandle { rx: completion_rx }
923    }
924
925    pub fn submit_async(
926        &self,
927        root_id: ProjectRootId,
928        lane: Lane,
929        request_id: String,
930        job: ExecutorJob,
931    ) -> oneshot::Receiver<Response> {
932        let (completion_tx, completion_rx) = oneshot::channel();
933        self.submit_with_completion(
934            root_id,
935            JobClass::Interactive,
936            lane,
937            request_id,
938            job,
939            CompletionSender::Async(completion_tx),
940        );
941        completion_rx
942    }
943
944    /// Submit an interactive job with an exact-job cancellation token.
945    ///
946    /// The returned token cancels THIS job only (queued: removed and settled
947    /// with `request_cancelled`; running: signalled cooperatively). The job
948    /// observes the token via [`current_job_cancellation`].
949    pub fn submit_cancellable_async(
950        &self,
951        root_id: ProjectRootId,
952        lane: Lane,
953        request_id: String,
954        job: ExecutorJob,
955    ) -> (oneshot::Receiver<Response>, JobCancellation) {
956        let cancellation = JobCancellation::new();
957        let (completion_tx, completion_rx) = oneshot::channel();
958        self.submit_with_completion_cancellable(
959            root_id,
960            JobClass::Interactive,
961            lane,
962            request_id,
963            job,
964            CompletionSender::Async(completion_tx),
965            Some(cancellation.clone()),
966            None,
967        );
968        (completion_rx, cancellation)
969    }
970
971    /// Cancel one exact job by its token.
972    ///
973    /// Queued jobs are removed and settled immediately; running jobs are
974    /// signalled and return through their normal completion path at the next
975    /// cooperative checkpoint. Jobs that sealed their commit finish normally.
976    pub fn cancel_job(&self, root_id: &ProjectRootId, token: &JobCancellation) -> JobCancelOutcome {
977        // Signal BEFORE actor lookup, deliberately: a job whose actor was
978        // already torn down (fatal teardown, root removal) must still abort at
979        // its next checkpoint, so the token is cancelled regardless. The
980        // outcome must then reflect what the signal actually did — reporting
981        // NotFound for a token that was RUNNING at signal time would mislead
982        // the caller into treating a signalled job as nonexistent.
983        let observed = token.signal_cancel();
984        let (outcome, settled) = {
985            let mut state = self.inner.state.lock();
986            let Some(actor) = state.actors.get_mut(root_id) else {
987                return match observed {
988                    JOB_CANCEL_STATE_COMMITTED => JobCancelOutcome::RunningCommitted,
989                    JOB_CANCEL_STATE_RUNNING | JOB_CANCEL_STATE_PENDING => {
990                        JobCancelOutcome::RunningSignalled
991                    }
992                    _ => JobCancelOutcome::NotFound,
993                };
994            };
995            match actor.remove_queued_cancellable(token) {
996                Some(queued) => (JobCancelOutcome::QueuedRemoved, Some(queued)),
997                None => match observed {
998                    // The seal won the race: the job commits and finishes.
999                    JOB_CANCEL_STATE_COMMITTED => (JobCancelOutcome::RunningCommitted, None),
1000                    // RUNNING at signal time, or PENDING at signal time but
1001                    // dispatched before we took the scheduler lock: either way
1002                    // the job aborts at its next checkpoint.
1003                    JOB_CANCEL_STATE_RUNNING | JOB_CANCEL_STATE_PENDING => {
1004                        (JobCancelOutcome::RunningSignalled, None)
1005                    }
1006                    // Already cancelled by an earlier call and no longer queued.
1007                    _ => (JobCancelOutcome::NotFound, None),
1008                },
1009            }
1010        };
1011        if let Some(queued) = settled {
1012            queued.completion.send(Response::error(
1013                queued.request_id,
1014                "request_cancelled",
1015                "request cancelled before execution",
1016            ));
1017            self.wake_scheduler();
1018        }
1019        outcome
1020    }
1021
1022    /// Cancel a route job only while its command closure has not started.
1023    ///
1024    /// Detaching a route must discard work that cannot produce a live reply,
1025    /// while a command already executing remains eligible for session replay.
1026    /// A dispatched job may already reserve scheduler capacity; its worker sees
1027    /// the cancelled token before invoking the closure and releases that
1028    /// reservation through the normal completion event.
1029    pub(crate) fn cancel_job_before_execution(
1030        &self,
1031        root_id: &ProjectRootId,
1032        token: &JobCancellation,
1033    ) -> PreExecutionCancelOutcome {
1034        let (outcome, settled) = {
1035            let mut state = self.inner.state.lock();
1036            if !token.try_cancel_before_running() {
1037                return PreExecutionCancelOutcome::AlreadyStarted;
1038            }
1039            let settled = state
1040                .actors
1041                .get_mut(root_id)
1042                .and_then(|actor| actor.remove_queued_cancellable(token));
1043            let outcome = if settled.is_some() {
1044                PreExecutionCancelOutcome::QueuedRemoved
1045            } else {
1046                PreExecutionCancelOutcome::DispatchedCancelled
1047            };
1048            (outcome, settled)
1049        };
1050        if let Some(queued) = settled {
1051            queued.completion.send(Response::error(
1052                queued.request_id,
1053                "request_cancelled",
1054                "request cancelled before execution",
1055            ));
1056            self.wake_scheduler();
1057        }
1058        outcome
1059    }
1060
1061    pub fn submit_maintenance_async(
1062        &self,
1063        root_id: ProjectRootId,
1064        lane: Lane,
1065        request_id: String,
1066        job: ExecutorJob,
1067    ) -> oneshot::Receiver<Response> {
1068        self.submit_maintenance_async_with_key(root_id, lane, request_id, job, None)
1069    }
1070
1071    #[cfg(test)]
1072    fn submit_maintenance_cancellable_async(
1073        &self,
1074        root_id: ProjectRootId,
1075        lane: Lane,
1076        request_id: String,
1077        job: ExecutorJob,
1078    ) -> (oneshot::Receiver<Response>, JobCancellation) {
1079        let (completion_tx, completion_rx) = oneshot::channel();
1080        let cancellation = JobCancellation::new();
1081        self.submit_with_completion_cancellable(
1082            root_id,
1083            JobClass::Maintenance,
1084            lane,
1085            request_id,
1086            job,
1087            CompletionSender::Async(completion_tx),
1088            Some(cancellation.clone()),
1089            None,
1090        );
1091        (completion_rx, cancellation)
1092    }
1093
1094    pub(crate) fn submit_coalescable_maintenance_async(
1095        &self,
1096        root_id: ProjectRootId,
1097        lane: Lane,
1098        request_id: String,
1099        coalesce_key: MaintenanceCoalesceKey,
1100        job: ExecutorJob,
1101    ) -> oneshot::Receiver<Response> {
1102        self.submit_maintenance_async_with_key(root_id, lane, request_id, job, Some(coalesce_key))
1103    }
1104
1105    fn submit_maintenance_async_with_key(
1106        &self,
1107        root_id: ProjectRootId,
1108        lane: Lane,
1109        request_id: String,
1110        job: ExecutorJob,
1111        coalesce_key: Option<MaintenanceCoalesceKey>,
1112    ) -> oneshot::Receiver<Response> {
1113        let (completion_tx, completion_rx) = oneshot::channel();
1114        self.submit_with_completion_cancellable(
1115            root_id,
1116            JobClass::Maintenance,
1117            lane,
1118            request_id,
1119            job,
1120            CompletionSender::Async(completion_tx),
1121            None,
1122            coalesce_key,
1123        );
1124        completion_rx
1125    }
1126
1127    fn submit_with_completion(
1128        &self,
1129        root_id: ProjectRootId,
1130        job_class: JobClass,
1131        lane: Lane,
1132        request_id: String,
1133        job: ExecutorJob,
1134        completion: CompletionSender,
1135    ) {
1136        self.submit_with_completion_cancellable(
1137            root_id, job_class, lane, request_id, job, completion, None, None,
1138        );
1139    }
1140
1141    #[allow(clippy::too_many_arguments)]
1142    fn submit_with_completion_cancellable(
1143        &self,
1144        root_id: ProjectRootId,
1145        job_class: JobClass,
1146        lane: Lane,
1147        request_id: String,
1148        job: ExecutorJob,
1149        completion: CompletionSender,
1150        cancellation: Option<JobCancellation>,
1151        maintenance_coalesce_key: Option<MaintenanceCoalesceKey>,
1152    ) {
1153        let command = job_command(job_class, lane);
1154        let mut job = Some(job);
1155        let mut completion = Some(completion);
1156        let mut duplicate_victims = Vec::new();
1157
1158        let response = {
1159            let mut state = self.inner.state.lock();
1160            match state.actors.get_mut(&root_id) {
1161                Some(actor) if actor.fatal => Some(actor_fatal_response(request_id.clone())),
1162                Some(actor) => {
1163                    let mut admission_error = None;
1164                    if job_class == JobClass::Maintenance {
1165                        if maintenance_coalesce_key
1166                            .is_some_and(|key| actor.maintenance.has_maintenance_coalesce_key(key))
1167                        {
1168                            admission_error = Some(maintenance_cancelled_response(
1169                                request_id.clone(),
1170                                "maintenance drain coalesced behind an identical queued drain",
1171                            ));
1172                        } else {
1173                            if actor.maintenance.queued_count() >= MAINTENANCE_QUEUE_CAP {
1174                                duplicate_victims =
1175                                    actor.maintenance.remove_duplicate_maintenance_jobs();
1176                            }
1177                            if actor.maintenance.queued_count() >= MAINTENANCE_QUEUE_CAP {
1178                                admission_error =
1179                                    Some(maintenance_backpressure_response(request_id.clone()));
1180                            }
1181                        }
1182                    }
1183
1184                    if admission_error.is_none() {
1185                        actor.push_job(
1186                            job_class,
1187                            lane,
1188                            QueuedJob {
1189                                job: job.take().expect("executor job already queued"),
1190                                completion: completion
1191                                    .take()
1192                                    .expect("executor completion already queued"),
1193                                request_id: request_id.clone(),
1194                                command,
1195                                queued_at: Instant::now(),
1196                                cancellation: cancellation.clone(),
1197                                maintenance_coalesce_key,
1198                            },
1199                        );
1200                    }
1201                    admission_error
1202                }
1203                None => Some(Response::error(
1204                    request_id.clone(),
1205                    "actor_not_registered",
1206                    "executor actor is not registered",
1207                )),
1208            }
1209        };
1210
1211        for victim in duplicate_victims {
1212            victim.completion.send(maintenance_cancelled_response(
1213                victim.request_id,
1214                "duplicate maintenance drain removed to preserve queue capacity",
1215            ));
1216        }
1217
1218        if let Some(response) = response {
1219            if let Some(completion) = completion {
1220                completion.send(response);
1221            }
1222            return;
1223        }
1224
1225        self.wake_scheduler();
1226    }
1227
1228    pub fn pool_size(&self) -> usize {
1229        self.inner.config.pool_size
1230    }
1231
1232    pub fn actor_cap(&self) -> usize {
1233        self.inner.config.actor_cap
1234    }
1235
1236    pub fn read_cap(&self) -> usize {
1237        self.inner.config.read_cap
1238    }
1239
1240    pub fn heavy_permits(&self) -> usize {
1241        self.inner.config.heavy_permits
1242    }
1243
1244    pub fn interactive_reserve(&self) -> usize {
1245        self.inner.config.interactive_reserve
1246    }
1247
1248    pub fn maintenance_cap(&self) -> usize {
1249        self.inner.config.maintenance_cap
1250    }
1251
1252    pub fn try_dispatch_liveness_snapshot(&self) -> Option<DispatchLivenessSnapshot> {
1253        Some(self.inner.dispatch_liveness.snapshot(&self.inner.config))
1254    }
1255
1256    pub fn try_mutating_lane_snapshots(&self) -> Option<Vec<MutatingLaneSnapshot>> {
1257        self.inner
1258            .state
1259            .try_lock()
1260            .map(|state| state.mutating_lane_snapshots())
1261    }
1262
1263    pub fn try_mutating_job_state_label(
1264        &self,
1265        root_id: &ProjectRootId,
1266        request_id: &str,
1267    ) -> Option<&'static str> {
1268        self.inner
1269            .state
1270            .try_lock()
1271            .map(|state| state.mutating_job_state_label(root_id, request_id))
1272    }
1273
1274    /// Snapshot RouteBind blockers without waiting on scheduler state. The subc
1275    /// health path uses this only for a delayed-bind breadcrumb, so contention
1276    /// is reported as scheduler busy rather than delaying the transport loop.
1277    pub fn try_bind_blocker_snapshot(
1278        &self,
1279        root_id: &ProjectRootId,
1280        request_id: &str,
1281    ) -> Option<BindBlockerSnapshot> {
1282        self.inner
1283            .state
1284            .try_lock()
1285            .map(|state| state.bind_blocker_snapshot(root_id, request_id))
1286    }
1287
1288    /// Return each over-age interactive occupant once. Marking happens under the
1289    /// scheduler lock so repeated transport-loop censuses cannot flood logs while
1290    /// the same job remains stuck.
1291    pub(crate) fn try_take_long_running_interactive_jobs(
1292        &self,
1293        minimum_age: Duration,
1294    ) -> Option<Vec<LongRunningInteractiveJobSnapshot>> {
1295        let now = Instant::now();
1296        let mut state = self.inner.state.try_lock()?;
1297        let mut snapshots = state
1298            .running_jobs
1299            .values_mut()
1300            .filter_map(|job| {
1301                let age = now.saturating_duration_since(job.started_at);
1302                if job.job_class != JobClass::Interactive
1303                    || job.occupancy_reported
1304                    || age < minimum_age
1305                {
1306                    return None;
1307                }
1308                job.occupancy_reported = true;
1309                Some(LongRunningInteractiveJobSnapshot {
1310                    root_id: job.root_id.clone(),
1311                    request_id: job.request_id.clone(),
1312                    command: job.command.clone(),
1313                    lane: job.lane,
1314                    age,
1315                    execution_started: job.execution_started.load(Ordering::Acquire),
1316                })
1317            })
1318            .collect::<Vec<_>>();
1319        snapshots.sort_by(|left, right| {
1320            left.root_id
1321                .as_path()
1322                .cmp(right.root_id.as_path())
1323                .then_with(|| left.request_id.cmp(&right.request_id))
1324        });
1325        Some(snapshots)
1326    }
1327
1328    pub fn nonrunnable_dispatch_count(&self) -> usize {
1329        self.inner.nonrunnable_dispatches.load(Ordering::Acquire)
1330    }
1331
1332    pub fn completion_counts(&self) -> (u64, u64) {
1333        (
1334            self.inner.completed_interactive.load(Ordering::Relaxed),
1335            self.inner.completed_maintenance.load(Ordering::Relaxed),
1336        )
1337    }
1338
1339    pub fn actor_is_fatal(&self, root_id: &ProjectRootId) -> bool {
1340        self.inner
1341            .state
1342            .lock()
1343            .actors
1344            .get(root_id)
1345            .map(|actor| actor.fatal)
1346            .unwrap_or(false)
1347    }
1348
1349    fn wake_scheduler(&self) {
1350        let _ = self.inner.event_tx.send(SchedulerEvent::Wake);
1351    }
1352}
1353
1354impl Default for Executor {
1355    fn default() -> Self {
1356        Self::new()
1357    }
1358}
1359
1360struct ExecutorInner {
1361    state: Arc<Mutex<SchedulerState>>,
1362    event_tx: Sender<SchedulerEvent>,
1363    scheduler_handle: Mutex<Option<JoinHandle<()>>>,
1364    worker_handles: Mutex<Vec<JoinHandle<()>>>,
1365    config: EffectiveConfig,
1366    nonrunnable_dispatches: Arc<AtomicUsize>,
1367    completed_interactive: Arc<AtomicU64>,
1368    completed_maintenance: Arc<AtomicU64>,
1369    dispatch_liveness: Arc<DispatchLivenessAtomics>,
1370}
1371
1372impl Drop for ExecutorInner {
1373    fn drop(&mut self) {
1374        let _ = self.event_tx.send(SchedulerEvent::Shutdown);
1375
1376        if let Some(handle) = self.scheduler_handle.lock().take() {
1377            let _ = handle.join();
1378        }
1379
1380        let mut workers = self.worker_handles.lock();
1381        for handle in workers.drain(..) {
1382            let _ = handle.join();
1383        }
1384    }
1385}
1386
1387struct SchedulerState {
1388    actors: HashMap<ProjectRootId, ActorState>,
1389    actor_order: Vec<ProjectRootId>,
1390    cursor: usize,
1391    idle_workers: usize,
1392    interactive_inflight: usize,
1393    maintenance_inflight: usize,
1394    config: EffectiveConfig,
1395    running_jobs: HashMap<u64, RunningJob>,
1396    next_job_id: u64,
1397}
1398
1399impl SchedulerState {
1400    fn new(config: EffectiveConfig) -> Self {
1401        Self {
1402            actors: HashMap::new(),
1403            actor_order: Vec::new(),
1404            cursor: 0,
1405            idle_workers: config.pool_size,
1406            interactive_inflight: 0,
1407            maintenance_inflight: 0,
1408            config,
1409            running_jobs: HashMap::new(),
1410            next_job_id: 1,
1411        }
1412    }
1413
1414    fn dispatch_liveness_snapshot(&self) -> DispatchLivenessSnapshot {
1415        self.assert_running_invariants();
1416        let now = Instant::now();
1417        let (completion_owned_interactive, completion_owned_maintenance) = self
1418            .running_jobs
1419            .values()
1420            .filter(|job| job.completion_ownership.strong_count() > 0)
1421            .fold((0, 0), |(interactive, maintenance), job| {
1422                match job.job_class {
1423                    JobClass::Interactive => (interactive + 1, maintenance),
1424                    JobClass::Maintenance => (interactive, maintenance + 1),
1425                }
1426            });
1427        let oldest_worker_owned_maintenance_age_ms = self
1428            .running_jobs
1429            .values()
1430            .filter(|job| {
1431                job.job_class == JobClass::Maintenance
1432                    && job.completion_ownership.strong_count() > 0
1433            })
1434            .map(|job| duration_millis_u64(now.saturating_duration_since(job.started_at)))
1435            .max();
1436        let mut interactive = QueueSnapshotAccumulator::default();
1437        let mut maintenance = QueueSnapshotAccumulator::default();
1438        for actor in self.actors.values() {
1439            interactive.add(actor.class_queues(JobClass::Interactive), now);
1440            maintenance.add(actor.class_queues(JobClass::Maintenance), now);
1441        }
1442
1443        DispatchLivenessSnapshot {
1444            interactive: interactive.finish(),
1445            maintenance: maintenance.finish(),
1446            running: DispatchRunningSnapshot {
1447                interactive: self.interactive_inflight,
1448                maintenance: self.maintenance_inflight,
1449                completion_owned_interactive,
1450                completion_owned_maintenance,
1451                oldest_worker_owned_maintenance_age_ms,
1452                phantom_interactive: self
1453                    .interactive_inflight
1454                    .saturating_sub(completion_owned_interactive),
1455                phantom_maintenance: self
1456                    .maintenance_inflight
1457                    .saturating_sub(completion_owned_maintenance),
1458            },
1459            phantom_running_maintenance: self
1460                .maintenance_inflight
1461                .saturating_sub(completion_owned_maintenance),
1462            interactive_reserve: self.config.interactive_reserve,
1463            maintenance_cap: self.config.maintenance_cap,
1464        }
1465    }
1466
1467    fn assert_running_invariants(&self) {
1468        let interactive = self
1469            .running_jobs
1470            .values()
1471            .filter(|job| job.job_class == JobClass::Interactive)
1472            .count();
1473        let maintenance = self
1474            .running_jobs
1475            .values()
1476            .filter(|job| job.job_class == JobClass::Maintenance)
1477            .count();
1478        debug_assert_eq!(self.interactive_inflight, interactive);
1479        debug_assert_eq!(self.maintenance_inflight, maintenance);
1480        debug_assert_eq!(
1481            self.idle_workers + self.running_jobs.len(),
1482            self.config.pool_size
1483        );
1484        debug_assert!(self
1485            .running_jobs
1486            .values()
1487            .all(|job| job.completion_ownership.strong_count() > 0));
1488    }
1489
1490    fn next_job_id(&mut self) -> u64 {
1491        let job_id = self.next_job_id;
1492        self.next_job_id = self.next_job_id.wrapping_add(1).max(1);
1493        job_id
1494    }
1495
1496    fn mutating_lane_snapshots(&self) -> Vec<MutatingLaneSnapshot> {
1497        let now = Instant::now();
1498        let mut snapshots: Vec<_> = self
1499            .actors
1500            .iter()
1501            .filter_map(|(root_id, actor)| {
1502                actor
1503                    .mutating_inflight
1504                    .as_ref()
1505                    .map(|job| MutatingLaneSnapshot {
1506                        root_id: root_id.clone(),
1507                        request_id: job.request_id.clone(),
1508                        command: job.command.clone(),
1509                        started_age_ms: duration_millis_u64(
1510                            now.saturating_duration_since(job.started_at),
1511                        ),
1512                    })
1513            })
1514            .collect();
1515        snapshots.sort_by(|left, right| left.root_id.as_path().cmp(right.root_id.as_path()));
1516        snapshots
1517    }
1518
1519    fn mutating_job_state_label(&self, root_id: &ProjectRootId, request_id: &str) -> &'static str {
1520        let Some(actor) = self.actors.get(root_id) else {
1521            return "actor_missing";
1522        };
1523        if actor
1524            .mutating_inflight
1525            .as_ref()
1526            .is_some_and(|job| job.request_id == request_id)
1527        {
1528            return "running";
1529        }
1530        if actor.has_queued_mutating_job(request_id) {
1531            return "queued";
1532        }
1533        if actor.writer_inflight {
1534            return "blocked_by_other_mutating";
1535        }
1536        "not_found"
1537    }
1538
1539    fn bind_blocker_snapshot(
1540        &self,
1541        root_id: &ProjectRootId,
1542        request_id: &str,
1543    ) -> BindBlockerSnapshot {
1544        let configure_state = self.mutating_job_state_label(root_id, request_id);
1545        let now = Instant::now();
1546        let actor = self.actors.get(root_id);
1547        let oldest_queued_writer_at = actor.and_then(ActorState::oldest_queued_writer_at);
1548        let oldest_queued_writer_age_ms = oldest_queued_writer_at
1549            .map(|queued_at| duration_millis_u64(now.saturating_duration_since(queued_at)));
1550        let mut in_flight_readers = self
1551            .running_jobs
1552            .values()
1553            .filter(|job| {
1554                job.root_id == *root_id
1555                    && matches!(job.lane, Lane::PureRead | Lane::SerialLspStatus)
1556            })
1557            .map(|job| BindBlockerReaderSnapshot {
1558                request_id: job.request_id.clone(),
1559                command: job.command.clone(),
1560                lane: job.lane,
1561                started_age_ms: duration_millis_u64(now.saturating_duration_since(job.started_at)),
1562                execution_started: job.execution_started.load(Ordering::Acquire),
1563                started_before_oldest_writer: oldest_queued_writer_at
1564                    .is_some_and(|queued_at| job.started_at <= queued_at),
1565            })
1566            .collect::<Vec<_>>();
1567        in_flight_readers.sort_by(|left, right| left.request_id.cmp(&right.request_id));
1568
1569        let mut blockers = Vec::new();
1570        if let Some(actor) = actor {
1571            if configure_state == "queued" {
1572                let configure_count = actor.pending_configure_count();
1573                if configure_count > 0 {
1574                    blockers.push(format!("queued_behind_configure({configure_count})"));
1575                }
1576                if actor.read_inflight > 0 || actor.lsp_inflight {
1577                    let zombie_readers = in_flight_readers
1578                        .iter()
1579                        .filter(|reader| !reader.execution_started)
1580                        .map(format_bind_blocker_reader)
1581                        .collect::<Vec<_>>();
1582                    let stuck_readers = in_flight_readers
1583                        .iter()
1584                        .filter(|reader| {
1585                            reader.execution_started
1586                                && reader.started_age_ms
1587                                    >= duration_millis_u64(READER_STUCK_CENSUS_AGE)
1588                        })
1589                        .map(format_bind_blocker_reader)
1590                        .collect::<Vec<_>>();
1591                    if !zombie_readers.is_empty() {
1592                        blockers.push(format!("zombie_reader({})", zombie_readers.join("; ")));
1593                    } else if !stuck_readers.is_empty() {
1594                        blockers.push(format!(
1595                            "waiting_on_readers_stuck({})",
1596                            stuck_readers.join("; ")
1597                        ));
1598                    } else {
1599                        blockers.push("waiting_on_readers".to_string());
1600                    }
1601                }
1602            }
1603        }
1604
1605        if configure_state == "queued" {
1606            let maintenance: Vec<_> = self
1607                .running_jobs
1608                .values()
1609                .filter(|job| job.job_class == JobClass::Maintenance)
1610                .collect();
1611            if !maintenance.is_empty() {
1612                blockers.push(format!(
1613                    "queued_behind_maintenance({})",
1614                    format_running_jobs(&maintenance)
1615                ));
1616            }
1617        }
1618
1619        if self.idle_workers == 0 {
1620            let running: Vec<_> = self.running_jobs.values().collect();
1621            blockers.push(format!(
1622                "idle_workers==0({})",
1623                format_running_jobs(&running)
1624            ));
1625        }
1626
1627        BindBlockerSnapshot {
1628            configure_state,
1629            configure_phase_timings: actor.map(|actor| actor.ctx.configure_ack_phase_snapshot()),
1630            blockers,
1631            oldest_queued_writer_age_ms,
1632            in_flight_readers,
1633            reader_admissions_while_promoted_writer_waited: actor
1634                .map(|actor| actor.reader_admissions_while_promoted_writer_waited)
1635                .unwrap_or(0),
1636        }
1637    }
1638}
1639
1640fn is_configure_request(request_id: &str) -> bool {
1641    request_id.starts_with("subc-bind-")
1642}
1643
1644fn format_bind_blocker_reader(reader: &BindBlockerReaderSnapshot) -> String {
1645    format!(
1646        "job={} command={} lane={:?} age_ms={} execution_started={} started_before_oldest_writer={}",
1647        reader.request_id,
1648        reader.command,
1649        reader.lane,
1650        reader.started_age_ms,
1651        reader.execution_started,
1652        reader.started_before_oldest_writer
1653    )
1654}
1655
1656fn format_running_jobs(jobs: &[&RunningJob]) -> String {
1657    let now = Instant::now();
1658    let mut labels: Vec<_> = jobs
1659        .iter()
1660        .map(|job| {
1661            format!(
1662                "job={} command={} lane={:?} root={} age_ms={}",
1663                job.request_id,
1664                job.command,
1665                job.lane,
1666                job.root_id.as_path().display(),
1667                duration_millis_u64(now.saturating_duration_since(job.started_at))
1668            )
1669        })
1670        .collect();
1671    labels.sort();
1672    labels.truncate(4);
1673    labels.join("; ")
1674}
1675
1676#[derive(Default)]
1677struct QueueSnapshotAccumulator {
1678    queued: usize,
1679    oldest_age_ms: Option<u64>,
1680}
1681
1682impl QueueSnapshotAccumulator {
1683    fn add(&mut self, queues: &ClassQueues, now: Instant) {
1684        self.queued += queues.queued_count();
1685        if let Some(queued_at) = queues.oldest_queued_at() {
1686            let age_ms = duration_millis_u64(now.saturating_duration_since(queued_at));
1687            self.oldest_age_ms = Some(
1688                self.oldest_age_ms
1689                    .map_or(age_ms, |oldest| oldest.max(age_ms)),
1690            );
1691        }
1692    }
1693
1694    fn finish(self) -> DispatchClassQueueSnapshot {
1695        DispatchClassQueueSnapshot {
1696            queued: self.queued,
1697            oldest_age_ms: self.oldest_age_ms,
1698        }
1699    }
1700}
1701
1702fn duration_millis_u64(duration: Duration) -> u64 {
1703    duration.as_millis().min(u128::from(u64::MAX)) as u64
1704}
1705
1706struct ActorState {
1707    ctx: Arc<AppContext>,
1708    epoch: Arc<RwLock<()>>,
1709    read_inflight: usize,
1710    lsp_inflight: bool,
1711    actor_total_inflight: usize,
1712    writer_inflight: bool,
1713    maintenance_commit_inflight: bool,
1714    mutating_inflight: Option<RunningMutatingJob>,
1715    reader_admissions_while_promoted_writer_waited: u64,
1716    deficit: isize,
1717    interactive: ClassQueues,
1718    maintenance: ClassQueues,
1719    fatal: bool,
1720}
1721
1722impl ActorState {
1723    fn new(ctx: Arc<AppContext>) -> Self {
1724        Self {
1725            ctx,
1726            epoch: Arc::new(RwLock::new(())),
1727            read_inflight: 0,
1728            lsp_inflight: false,
1729            actor_total_inflight: 0,
1730            writer_inflight: false,
1731            maintenance_commit_inflight: false,
1732            mutating_inflight: None,
1733            reader_admissions_while_promoted_writer_waited: 0,
1734            deficit: 0,
1735            interactive: ClassQueues::new(),
1736            maintenance: ClassQueues::new(),
1737            fatal: false,
1738        }
1739    }
1740
1741    fn push_job(&mut self, job_class: JobClass, lane: Lane, job: QueuedJob) {
1742        self.class_queues_mut(job_class).push_job(lane, job);
1743    }
1744
1745    fn has_queued_jobs(&self) -> bool {
1746        self.interactive.has_queued_jobs() || self.maintenance.has_queued_jobs()
1747    }
1748
1749    fn is_idle(&self) -> bool {
1750        self.actor_total_inflight == 0
1751            && !self.has_queued_jobs()
1752            && !view_publication::running_for_context(&self.ctx)
1753    }
1754
1755    fn has_queued_jobs_for(&self, job_class: JobClass) -> bool {
1756        self.class_queues(job_class).has_queued_jobs()
1757    }
1758
1759    fn front_lane(&self, job_class: JobClass) -> Option<Lane> {
1760        self.class_queues(job_class).front_lane()
1761    }
1762
1763    fn pop_front_job(&mut self, job_class: JobClass, lane: Lane) -> Option<QueuedJob> {
1764        self.class_queues_mut(job_class).pop_front_job(lane)
1765    }
1766
1767    fn higher_priority_writer_barrier_blocks(&self, job_class: JobClass) -> bool {
1768        // Maintenance must not start while interactive mutating work (tool
1769        // mutations, route binds) waits: a maintenance job that takes the
1770        // actor's writer slot would push the interactive writer behind it.
1771        matches!(job_class, JobClass::Maintenance)
1772            && !self.interactive.queue(Lane::Mutating).is_empty()
1773    }
1774
1775    fn class_queues(&self, job_class: JobClass) -> &ClassQueues {
1776        match job_class {
1777            JobClass::Interactive => &self.interactive,
1778            JobClass::Maintenance => &self.maintenance,
1779        }
1780    }
1781
1782    fn class_queues_mut(&mut self, job_class: JobClass) -> &mut ClassQueues {
1783        match job_class {
1784            JobClass::Interactive => &mut self.interactive,
1785            JobClass::Maintenance => &mut self.maintenance,
1786        }
1787    }
1788
1789    fn fail_queued_jobs(&mut self) {
1790        self.interactive.fail_queued_jobs();
1791        self.maintenance.fail_queued_jobs();
1792    }
1793
1794    fn has_queued_mutating_job(&self, request_id: &str) -> bool {
1795        self.interactive.has_queued_mutating_job(request_id)
1796            || self.maintenance.has_queued_mutating_job(request_id)
1797    }
1798
1799    fn remove_queued_cancellable(&mut self, token: &JobCancellation) -> Option<QueuedJob> {
1800        self.interactive
1801            .remove_cancellable(token)
1802            .or_else(|| self.maintenance.remove_cancellable(token))
1803    }
1804
1805    fn oldest_queued_writer_at(&self) -> Option<Instant> {
1806        [
1807            self.interactive.queue(Lane::Mutating).front(),
1808            self.maintenance.queue(Lane::Mutating).front(),
1809        ]
1810        .into_iter()
1811        .flatten()
1812        .map(|job| job.queued_at)
1813        .min()
1814    }
1815
1816    fn pending_configure_count(&self) -> usize {
1817        usize::from(
1818            self.mutating_inflight
1819                .as_ref()
1820                .is_some_and(|job| is_configure_request(&job.request_id)),
1821        ) + self.interactive.queued_configure_count()
1822            + self.maintenance.queued_configure_count()
1823    }
1824}
1825
1826struct ClassQueues {
1827    order: VecDeque<Lane>,
1828    pure_reads: VecDeque<QueuedJob>,
1829    lsp_status: VecDeque<QueuedJob>,
1830    heavy_init: VecDeque<QueuedJob>,
1831    mutating: VecDeque<QueuedJob>,
1832    maintenance_commit: VecDeque<QueuedJob>,
1833}
1834
1835impl ClassQueues {
1836    fn new() -> Self {
1837        Self {
1838            order: VecDeque::new(),
1839            pure_reads: VecDeque::new(),
1840            lsp_status: VecDeque::new(),
1841            heavy_init: VecDeque::new(),
1842            mutating: VecDeque::new(),
1843            maintenance_commit: VecDeque::new(),
1844        }
1845    }
1846
1847    fn push_job(&mut self, lane: Lane, job: QueuedJob) {
1848        self.order.push_back(lane);
1849        self.queue_mut(lane).push_back(job);
1850    }
1851
1852    fn has_queued_jobs(&self) -> bool {
1853        !self.order.is_empty()
1854    }
1855
1856    fn front_lane(&self) -> Option<Lane> {
1857        self.order.front().copied()
1858    }
1859
1860    /// Interactive admission order: a hard-starved configure (queued RouteBind
1861    /// older than the promotion age) preempts everything so its daemon deadline
1862    /// survives; otherwise pure reads go first (they overlap each other and
1863    /// never barrier the actor), then remaining lanes in arrival order.
1864    /// Maintenance keeps strict arrival order via `front_lane`.
1865    fn next_interactive_lane(&self, now: Instant) -> Option<Lane> {
1866        let starved_writer = self.mutating.iter().any(|job| {
1867            now.saturating_duration_since(job.queued_at) >= INTERACTIVE_WRITER_PROMOTION_AGE
1868        });
1869        if starved_writer {
1870            // Also stops NEW readers from being admitted on this actor while
1871            // the promoted writer waits for in-flight readers to drain.
1872            return Some(Lane::Mutating);
1873        }
1874        if !self.pure_reads.is_empty() {
1875            return Some(Lane::PureRead);
1876        }
1877        self.order
1878            .iter()
1879            .copied()
1880            .find(|lane| *lane != Lane::PureRead)
1881    }
1882
1883    fn pop_front_job(&mut self, lane: Lane) -> Option<QueuedJob> {
1884        // Keep `order` consistent with per-lane queues when admission picks a
1885        // lane other than the arrival-order head: remove the FIRST occurrence
1886        // of the chosen lane from `order`, not necessarily the front.
1887        let position = self.order.iter().position(|queued| *queued == lane)?;
1888        self.order.remove(position);
1889        self.queue_mut(lane).pop_front()
1890    }
1891
1892    fn queued_count(&self) -> usize {
1893        self.order.len()
1894    }
1895
1896    fn has_maintenance_coalesce_key(&self, key: MaintenanceCoalesceKey) -> bool {
1897        [
1898            Lane::PureRead,
1899            Lane::SerialLspStatus,
1900            Lane::HeavyInit,
1901            Lane::Mutating,
1902            Lane::MaintenanceCommit,
1903        ]
1904        .into_iter()
1905        .any(|lane| {
1906            self.queue(lane)
1907                .iter()
1908                .any(|job| job.maintenance_coalesce_key == Some(key))
1909        })
1910    }
1911
1912    /// Remove only redundant idempotent drains while preserving survivor order.
1913    fn remove_duplicate_maintenance_jobs(&mut self) -> Vec<QueuedJob> {
1914        let mut seen = HashSet::new();
1915        let mut lane_positions = [0usize; 5];
1916        let mut duplicates = Vec::new();
1917
1918        for (order_position, lane) in self.order.iter().copied().enumerate() {
1919            let lane_index = lane_index(lane);
1920            let queue_position = lane_positions[lane_index];
1921            lane_positions[lane_index] += 1;
1922            let Some(key) = self
1923                .queue(lane)
1924                .get(queue_position)
1925                .and_then(|job| job.maintenance_coalesce_key)
1926            else {
1927                continue;
1928            };
1929            if !seen.insert(key) {
1930                duplicates.push((order_position, lane, queue_position));
1931            }
1932        }
1933
1934        let mut removed = Vec::with_capacity(duplicates.len());
1935        for (order_position, lane, queue_position) in duplicates.into_iter().rev() {
1936            self.order.remove(order_position);
1937            if let Some(job) = self.queue_mut(lane).remove(queue_position) {
1938                removed.push(job);
1939            }
1940        }
1941        removed
1942    }
1943
1944    fn oldest_queued_at(&self) -> Option<Instant> {
1945        self.front_lane()
1946            .and_then(|lane| self.queue(lane).front().map(|job| job.queued_at))
1947    }
1948
1949    fn fail_queued_jobs(&mut self) {
1950        self.order.clear();
1951        fail_queued_job_queue(&mut self.pure_reads);
1952        fail_queued_job_queue(&mut self.lsp_status);
1953        fail_queued_job_queue(&mut self.heavy_init);
1954        fail_queued_job_queue(&mut self.mutating);
1955        fail_queued_job_queue(&mut self.maintenance_commit);
1956    }
1957
1958    fn cancel_queued_jobs(&mut self) -> usize {
1959        self.order.clear();
1960        cancel_queued_job_queue(&mut self.pure_reads)
1961            + cancel_queued_job_queue(&mut self.lsp_status)
1962            + cancel_queued_job_queue(&mut self.heavy_init)
1963            + cancel_queued_job_queue(&mut self.mutating)
1964            + cancel_queued_job_queue(&mut self.maintenance_commit)
1965    }
1966
1967    fn has_queued_mutating_job(&self, request_id: &str) -> bool {
1968        self.mutating.iter().any(|job| job.request_id == request_id)
1969    }
1970
1971    /// Remove the queued job carrying this exact cancellation token.
1972    ///
1973    /// `order` holds one lane entry per push, and per-lane entries pair FIFO
1974    /// with the lane queue: the k-th occurrence of a lane in `order`
1975    /// corresponds to the k-th element of that lane's queue. Removing the
1976    /// FIRST matching order entry for a job deeper in its lane queue would
1977    /// shift the pairing and reorder the survivors, so the occurrence at the
1978    /// job's own queue position is removed instead.
1979    fn remove_cancellable(&mut self, token: &JobCancellation) -> Option<QueuedJob> {
1980        for lane in [
1981            Lane::PureRead,
1982            Lane::SerialLspStatus,
1983            Lane::HeavyInit,
1984            Lane::Mutating,
1985            Lane::MaintenanceCommit,
1986        ] {
1987            let queue = self.queue_mut(lane);
1988            let position = queue.iter().position(|queued| {
1989                queued
1990                    .cancellation
1991                    .as_ref()
1992                    .is_some_and(|candidate| candidate.same_token(token))
1993            });
1994            if let Some(position) = position {
1995                let removed = queue.remove(position);
1996                let mut occurrence = 0usize;
1997                if let Some(order_position) = self.order.iter().position(|entry| {
1998                    if *entry != lane {
1999                        return false;
2000                    }
2001                    let matched = occurrence == position;
2002                    occurrence += 1;
2003                    matched
2004                }) {
2005                    self.order.remove(order_position);
2006                }
2007                return removed;
2008            }
2009        }
2010        None
2011    }
2012
2013    fn queued_configure_count(&self) -> usize {
2014        self.mutating
2015            .iter()
2016            .filter(|job| is_configure_request(&job.request_id))
2017            .count()
2018    }
2019
2020    fn queue(&self, lane: Lane) -> &VecDeque<QueuedJob> {
2021        match lane {
2022            Lane::PureRead => &self.pure_reads,
2023            Lane::SerialLspStatus => &self.lsp_status,
2024            Lane::HeavyInit => &self.heavy_init,
2025            Lane::Mutating => &self.mutating,
2026            Lane::MaintenanceCommit => &self.maintenance_commit,
2027        }
2028    }
2029
2030    fn queue_mut(&mut self, lane: Lane) -> &mut VecDeque<QueuedJob> {
2031        match lane {
2032            Lane::PureRead => &mut self.pure_reads,
2033            Lane::SerialLspStatus => &mut self.lsp_status,
2034            Lane::HeavyInit => &mut self.heavy_init,
2035            Lane::Mutating => &mut self.mutating,
2036            Lane::MaintenanceCommit => &mut self.maintenance_commit,
2037        }
2038    }
2039}
2040
2041struct QueuedJob {
2042    job: ExecutorJob,
2043    completion: CompletionSender,
2044    request_id: String,
2045    command: String,
2046    queued_at: Instant,
2047    cancellation: Option<JobCancellation>,
2048    maintenance_coalesce_key: Option<MaintenanceCoalesceKey>,
2049}
2050
2051fn lane_index(lane: Lane) -> usize {
2052    match lane {
2053        Lane::PureRead => 0,
2054        Lane::SerialLspStatus => 1,
2055        Lane::HeavyInit => 2,
2056        Lane::Mutating => 3,
2057        Lane::MaintenanceCommit => 4,
2058    }
2059}
2060
2061fn fail_queued_job_queue(queue: &mut VecDeque<QueuedJob>) {
2062    for queued in queue.drain(..) {
2063        queued
2064            .completion
2065            .send(actor_fatal_response(queued.request_id));
2066    }
2067}
2068
2069fn cancel_queued_job_queue(queue: &mut VecDeque<QueuedJob>) -> usize {
2070    let cancelled = queue.len();
2071    for queued in queue.drain(..) {
2072        queued.completion.send(Response::error(
2073            queued.request_id,
2074            "maintenance_cancelled",
2075            "maintenance cancelled because the actor has no bound routes",
2076        ));
2077    }
2078    cancelled
2079}
2080
2081fn job_command(job_class: JobClass, lane: Lane) -> String {
2082    format!("executor::{job_class:?}::{lane:?}")
2083}
2084
2085fn maintenance_cancelled_response(
2086    request_id: impl Into<String>,
2087    message: impl Into<String>,
2088) -> Response {
2089    Response::error(request_id, "maintenance_cancelled", message)
2090}
2091
2092fn maintenance_backpressure_response(request_id: impl Into<String>) -> Response {
2093    Response::error_with_data(
2094        request_id,
2095        "maintenance_backpressure",
2096        format!("maintenance queue reached its per-actor capacity of {MAINTENANCE_QUEUE_CAP} jobs"),
2097        serde_json::json!({
2098            "retryable": true,
2099            "queue_cap": MAINTENANCE_QUEUE_CAP,
2100        }),
2101    )
2102}
2103
2104fn actor_fatal_response(request_id: impl Into<String>) -> Response {
2105    Response::error(
2106        request_id,
2107        "actor_fatal",
2108        "executor actor is fatal after a mutating job panic",
2109    )
2110}
2111
2112fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> String {
2113    if let Some(message) = payload.downcast_ref::<&'static str>() {
2114        (*message).to_string()
2115    } else if let Some(message) = payload.downcast_ref::<String>() {
2116        message.clone()
2117    } else {
2118        "unknown panic payload".to_string()
2119    }
2120}
2121
2122fn panic_response(
2123    request_id: impl Into<String>,
2124    command: &str,
2125    payload: &(dyn std::any::Any + Send),
2126) -> Response {
2127    let panic_message = panic_payload_message(payload);
2128    Response::error(
2129        request_id,
2130        "actor_fatal",
2131        format!("command '{command}' panicked: {panic_message}"),
2132    )
2133}
2134
2135enum CompletionSender {
2136    Sync(Sender<Response>),
2137    Async(oneshot::Sender<Response>),
2138}
2139
2140impl CompletionSender {
2141    fn send(self, response: Response) {
2142        match self {
2143            Self::Sync(tx) => {
2144                let _ = tx.send(response);
2145            }
2146            Self::Async(tx) => {
2147                let _ = tx.send(response);
2148            }
2149        }
2150    }
2151}
2152
2153struct RunJob {
2154    root_id: ProjectRootId,
2155    job_class: JobClass,
2156    lane: Lane,
2157    ctx: Arc<AppContext>,
2158    epoch: Arc<RwLock<()>>,
2159    job: ExecutorJob,
2160    completion: Option<CompletionSender>,
2161    request_id: String,
2162    command: String,
2163    heavy_permit: Option<HeavyPermit>,
2164    cancellation: Option<JobCancellation>,
2165    execution_started: Arc<AtomicBool>,
2166    completion_guard: Option<JobCompletionGuard>,
2167}
2168
2169struct JobCompletionOwnership;
2170
2171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2172enum JobCompletionOutcome {
2173    Completed,
2174    Panicked,
2175    Abandoned,
2176}
2177
2178struct CompletionEvent {
2179    job_id: u64,
2180    heavy_permit: Option<HeavyPermit>,
2181    outcome: JobCompletionOutcome,
2182    ownership: Arc<JobCompletionOwnership>,
2183}
2184
2185struct JobCompletionGuard {
2186    event_tx: Sender<SchedulerEvent>,
2187    event: Option<CompletionEvent>,
2188}
2189
2190impl JobCompletionGuard {
2191    fn new(event_tx: Sender<SchedulerEvent>, event: CompletionEvent) -> Self {
2192        Self {
2193            event_tx,
2194            event: Some(event),
2195        }
2196    }
2197
2198    fn set_outcome(&mut self, outcome: JobCompletionOutcome) {
2199        if let Some(event) = self.event.as_mut() {
2200            event.outcome = outcome;
2201        }
2202    }
2203}
2204
2205impl Drop for JobCompletionGuard {
2206    fn drop(&mut self) {
2207        let Some(mut event) = self.event.take() else {
2208            return;
2209        };
2210        if std::thread::panicking() {
2211            event.outcome = JobCompletionOutcome::Panicked;
2212        }
2213        let _ = self.event_tx.send(SchedulerEvent::Completed(event));
2214    }
2215}
2216
2217enum SchedulerEvent {
2218    Wake,
2219    Completed(CompletionEvent),
2220    Shutdown,
2221}
2222
2223fn scheduler_loop(
2224    state: Arc<Mutex<SchedulerState>>,
2225    heavy: Arc<HeavySemaphore>,
2226    run_tx: Sender<RunJob>,
2227    event_tx: Sender<SchedulerEvent>,
2228    event_rx: Receiver<SchedulerEvent>,
2229    nonrunnable_dispatches: Arc<AtomicUsize>,
2230    completed_interactive: Arc<AtomicU64>,
2231    completed_maintenance: Arc<AtomicU64>,
2232    dispatch_liveness: Arc<DispatchLivenessAtomics>,
2233) {
2234    while let Ok(event) = event_rx.recv() {
2235        let shutdown;
2236        {
2237            let mut state = state.lock();
2238            shutdown = process_scheduler_event_batch(
2239                event,
2240                &event_rx,
2241                &mut state,
2242                &completed_interactive,
2243                &completed_maintenance,
2244            );
2245
2246            if !shutdown {
2247                dispatch_runnable(
2248                    &mut state,
2249                    &heavy,
2250                    &run_tx,
2251                    &event_tx,
2252                    &nonrunnable_dispatches,
2253                );
2254            }
2255            dispatch_liveness.record(&state.dispatch_liveness_snapshot());
2256        }
2257
2258        if shutdown {
2259            break;
2260        }
2261    }
2262}
2263
2264fn process_scheduler_event_batch(
2265    first: SchedulerEvent,
2266    event_rx: &Receiver<SchedulerEvent>,
2267    state: &mut SchedulerState,
2268    completed_interactive: &AtomicU64,
2269    completed_maintenance: &AtomicU64,
2270) -> bool {
2271    let mut event = Some(first);
2272    for _ in 0..SCHEDULER_EVENT_BATCH_CAP {
2273        let Some(current) = event.take().or_else(|| event_rx.try_recv().ok()) else {
2274            break;
2275        };
2276        if process_scheduler_event(current, state, completed_interactive, completed_maintenance) {
2277            return true;
2278        }
2279    }
2280    false
2281}
2282
2283fn note_completed_class(
2284    job_class: JobClass,
2285    completed_interactive: &AtomicU64,
2286    completed_maintenance: &AtomicU64,
2287) {
2288    match job_class {
2289        JobClass::Interactive => {
2290            completed_interactive.fetch_add(1, Ordering::Relaxed);
2291        }
2292        JobClass::Maintenance => {
2293            completed_maintenance.fetch_add(1, Ordering::Relaxed);
2294        }
2295    }
2296}
2297
2298fn process_scheduler_event(
2299    event: SchedulerEvent,
2300    state: &mut SchedulerState,
2301    completed_interactive: &AtomicU64,
2302    completed_maintenance: &AtomicU64,
2303) -> bool {
2304    match event {
2305        SchedulerEvent::Wake => false,
2306        SchedulerEvent::Completed(event) => {
2307            if let Some(job_class) = complete_job(state, event) {
2308                note_completed_class(job_class, completed_interactive, completed_maintenance);
2309            }
2310            false
2311        }
2312        SchedulerEvent::Shutdown => true,
2313    }
2314}
2315
2316fn complete_job(state: &mut SchedulerState, event: CompletionEvent) -> Option<JobClass> {
2317    let CompletionEvent {
2318        job_id,
2319        heavy_permit,
2320        outcome,
2321        ownership,
2322    } = event;
2323    let Some(running) = state.running_jobs.remove(&job_id) else {
2324        log::error!("executor completion: unknown job_id={job_id} outcome={outcome:?}");
2325        return None;
2326    };
2327    debug_assert!(running
2328        .completion_ownership
2329        .upgrade()
2330        .is_some_and(|current| Arc::ptr_eq(&current, &ownership)));
2331
2332    let RunningJob {
2333        root_id,
2334        request_id,
2335        job_class,
2336        lane,
2337        ..
2338    } = running;
2339    log::debug!(
2340        "executor completion: job_id={job_id} class={job_class:?} lane={lane:?} outcome={outcome:?} request_id={request_id} root={}",
2341        root_id.as_path().display()
2342    );
2343
2344    match job_class {
2345        JobClass::Interactive => {
2346            debug_assert!(state.interactive_inflight > 0);
2347            state.interactive_inflight = state.interactive_inflight.saturating_sub(1);
2348        }
2349        JobClass::Maintenance => {
2350            debug_assert!(state.maintenance_inflight > 0);
2351            state.maintenance_inflight = state.maintenance_inflight.saturating_sub(1);
2352        }
2353    }
2354
2355    if let Some(actor) = state.actors.get_mut(&root_id) {
2356        actor.actor_total_inflight = actor.actor_total_inflight.saturating_sub(1);
2357        match lane {
2358            Lane::PureRead => {
2359                actor.read_inflight = actor.read_inflight.saturating_sub(1);
2360            }
2361            Lane::SerialLspStatus => {
2362                actor.lsp_inflight = false;
2363            }
2364            Lane::HeavyInit => {}
2365            Lane::Mutating => {
2366                actor.writer_inflight = false;
2367                actor.mutating_inflight = None;
2368            }
2369            Lane::MaintenanceCommit => {
2370                actor.maintenance_commit_inflight = false;
2371            }
2372        }
2373
2374        if outcome == JobCompletionOutcome::Panicked && lane == Lane::Mutating {
2375            actor.fatal = true;
2376            actor.fail_queued_jobs();
2377        }
2378    }
2379
2380    drop(heavy_permit);
2381    drop(ownership);
2382    state.idle_workers += 1;
2383    state.assert_running_invariants();
2384    Some(job_class)
2385}
2386
2387fn dispatch_runnable(
2388    state: &mut SchedulerState,
2389    heavy: &Arc<HeavySemaphore>,
2390    run_tx: &Sender<RunJob>,
2391    event_tx: &Sender<SchedulerEvent>,
2392    nonrunnable_dispatches: &AtomicUsize,
2393) {
2394    while state.idle_workers > 0 && !state.actor_order.is_empty() {
2395        let mut made_progress = false;
2396        let mut dispatch_failed = false;
2397
2398        made_progress |= dispatch_runnable_class(
2399            state,
2400            JobClass::Interactive,
2401            heavy,
2402            run_tx,
2403            event_tx,
2404            nonrunnable_dispatches,
2405            &mut dispatch_failed,
2406        );
2407        if dispatch_failed || state.idle_workers == 0 {
2408            return;
2409        }
2410
2411        if can_dispatch_class(state, JobClass::Maintenance) {
2412            made_progress |= dispatch_runnable_class(
2413                state,
2414                JobClass::Maintenance,
2415                heavy,
2416                run_tx,
2417                event_tx,
2418                nonrunnable_dispatches,
2419                &mut dispatch_failed,
2420            );
2421            if dispatch_failed {
2422                return;
2423            }
2424        }
2425
2426        if !made_progress {
2427            break;
2428        }
2429    }
2430}
2431
2432fn dispatch_runnable_class(
2433    state: &mut SchedulerState,
2434    job_class: JobClass,
2435    heavy: &Arc<HeavySemaphore>,
2436    run_tx: &Sender<RunJob>,
2437    event_tx: &Sender<SchedulerEvent>,
2438    nonrunnable_dispatches: &AtomicUsize,
2439    dispatch_failed: &mut bool,
2440) -> bool {
2441    if !can_dispatch_class(state, job_class) || state.actor_order.is_empty() {
2442        return false;
2443    }
2444
2445    let actor_count = state.actor_order.len();
2446    let mut made_progress = false;
2447
2448    for _ in 0..actor_count {
2449        if !can_dispatch_class(state, job_class) || state.actor_order.is_empty() {
2450            break;
2451        }
2452
2453        if state.cursor >= state.actor_order.len() {
2454            state.cursor = 0;
2455        }
2456        let root_id = state.actor_order[state.cursor].clone();
2457        state.cursor = (state.cursor + 1) % state.actor_order.len();
2458
2459        let run_job = {
2460            let Some(actor) = state.actors.get_mut(&root_id) else {
2461                continue;
2462            };
2463
2464            if actor.fatal {
2465                actor.fail_queued_jobs();
2466                actor.deficit = 0;
2467                continue;
2468            }
2469
2470            if !actor.has_queued_jobs() {
2471                actor.deficit = 0;
2472                continue;
2473            }
2474
2475            if !actor.has_queued_jobs_for(job_class) {
2476                continue;
2477            }
2478
2479            actor.deficit =
2480                (actor.deficit + state.config.drr_quantum).min(state.config.deficit_cap);
2481            if actor.deficit < JOB_COST {
2482                continue;
2483            }
2484
2485            try_admit_actor(&root_id, actor, job_class, &state.config, heavy)
2486        };
2487
2488        if let Some(mut run_job) = run_job {
2489            let job_id = state.next_job_id();
2490            let ownership = Arc::new(JobCompletionOwnership);
2491            run_job.completion_guard = Some(JobCompletionGuard::new(
2492                event_tx.clone(),
2493                CompletionEvent {
2494                    job_id,
2495                    heavy_permit: run_job.heavy_permit.take(),
2496                    outcome: JobCompletionOutcome::Abandoned,
2497                    ownership: Arc::clone(&ownership),
2498                },
2499            ));
2500            let replaced = state.running_jobs.insert(
2501                job_id,
2502                RunningJob {
2503                    root_id: run_job.root_id.clone(),
2504                    request_id: run_job.request_id.clone(),
2505                    command: run_job.command.clone(),
2506                    job_class: run_job.job_class,
2507                    lane: run_job.lane,
2508                    started_at: Instant::now(),
2509                    execution_started: Arc::clone(&run_job.execution_started),
2510                    completion_ownership: Arc::downgrade(&ownership),
2511                    occupancy_reported: false,
2512                },
2513            );
2514            debug_assert!(replaced.is_none());
2515            state.idle_workers -= 1;
2516            match job_class {
2517                JobClass::Interactive => state.interactive_inflight += 1,
2518                JobClass::Maintenance => state.maintenance_inflight += 1,
2519            }
2520            state.assert_running_invariants();
2521            log::debug!(
2522                "executor dispatch: job_id={job_id} class={:?} lane={:?} request_id={} root={}",
2523                run_job.job_class,
2524                run_job.lane,
2525                run_job.request_id,
2526                run_job.root_id.as_path().display()
2527            );
2528            made_progress = true;
2529            if run_tx.send(run_job).is_err() {
2530                nonrunnable_dispatches.fetch_add(1, Ordering::AcqRel);
2531                *dispatch_failed = true;
2532                return made_progress;
2533            }
2534        }
2535    }
2536
2537    made_progress
2538}
2539
2540fn can_dispatch_class(state: &SchedulerState, job_class: JobClass) -> bool {
2541    if state.idle_workers == 0 {
2542        return false;
2543    }
2544    match job_class {
2545        JobClass::Interactive => true,
2546        JobClass::Maintenance => {
2547            state.maintenance_inflight < state.config.maintenance_cap
2548                && state.idle_workers > state.config.interactive_reserve
2549        }
2550    }
2551}
2552
2553fn try_admit_actor(
2554    root_id: &ProjectRootId,
2555    actor: &mut ActorState,
2556    job_class: JobClass,
2557    config: &EffectiveConfig,
2558    heavy: &Arc<HeavySemaphore>,
2559) -> Option<RunJob> {
2560    let lane = match job_class {
2561        JobClass::Interactive => actor
2562            .class_queues(JobClass::Interactive)
2563            .next_interactive_lane(Instant::now())?,
2564        JobClass::Maintenance => actor.front_lane(job_class)?,
2565    };
2566    let mut heavy_permit = None;
2567
2568    if actor.writer_inflight || actor.higher_priority_writer_barrier_blocks(job_class) {
2569        return None;
2570    }
2571
2572    let has_epoch_reader =
2573        actor.read_inflight > 0 || actor.lsp_inflight || actor.maintenance_commit_inflight;
2574    // MaintenanceCommit uses the executor's global maintenance capacity and
2575    // must not consume the per-actor slots reserved for interactive work. This
2576    // matters when a two-worker executor has actor_cap=1: the global reserve
2577    // leaves one worker idle, and this accounting makes that worker reachable
2578    // by an interactive request for the same actor.
2579    let actor_interactive_inflight = actor
2580        .actor_total_inflight
2581        .saturating_sub(usize::from(actor.maintenance_commit_inflight));
2582    let actor_has_interactive_capacity = actor_interactive_inflight < config.actor_cap;
2583    let runnable = match lane {
2584        Lane::PureRead => actor.read_inflight < config.read_cap && actor_has_interactive_capacity,
2585        Lane::SerialLspStatus => !actor.lsp_inflight && actor_has_interactive_capacity,
2586        Lane::HeavyInit => {
2587            if !actor_has_interactive_capacity {
2588                false
2589            } else if let Some(permit) = heavy.try_acquire() {
2590                heavy_permit = Some(permit);
2591                true
2592            } else {
2593                false
2594            }
2595        }
2596        Lane::Mutating => !has_epoch_reader && actor_has_interactive_capacity,
2597        // This lane has separate global and per-actor bounds: maintenance_cap
2598        // reserves workers globally, and the boolean prevents same-actor
2599        // maintenance from stacking without consuming an interactive slot.
2600        Lane::MaintenanceCommit => !actor.maintenance_commit_inflight,
2601    };
2602
2603    if !runnable {
2604        return None;
2605    }
2606
2607    let promoted_writer_waiting = actor.oldest_queued_writer_at().is_some_and(|queued_at| {
2608        Instant::now().saturating_duration_since(queued_at) >= INTERACTIVE_WRITER_PROMOTION_AGE
2609    });
2610    if promoted_writer_waiting && matches!(lane, Lane::PureRead | Lane::SerialLspStatus) {
2611        actor.reader_admissions_while_promoted_writer_waited = actor
2612            .reader_admissions_while_promoted_writer_waited
2613            .saturating_add(1);
2614    }
2615
2616    let queued = actor.pop_front_job(job_class, lane)?;
2617    actor.deficit -= JOB_COST;
2618    if lane == Lane::Mutating {
2619        actor.mutating_inflight = Some(RunningMutatingJob {
2620            request_id: queued.request_id.clone(),
2621            command: queued.command.clone(),
2622            started_at: Instant::now(),
2623        });
2624    }
2625    match lane {
2626        Lane::PureRead => {
2627            actor.read_inflight += 1;
2628            actor.actor_total_inflight += 1;
2629        }
2630        Lane::SerialLspStatus => {
2631            actor.lsp_inflight = true;
2632            actor.actor_total_inflight += 1;
2633        }
2634        Lane::HeavyInit => {
2635            actor.actor_total_inflight += 1;
2636        }
2637        Lane::Mutating => {
2638            actor.writer_inflight = true;
2639            actor.actor_total_inflight += 1;
2640        }
2641        Lane::MaintenanceCommit => {
2642            actor.maintenance_commit_inflight = true;
2643            actor.actor_total_inflight += 1;
2644        }
2645    }
2646
2647    Some(RunJob {
2648        root_id: root_id.clone(),
2649        job_class,
2650        lane,
2651        ctx: Arc::clone(&actor.ctx),
2652        epoch: Arc::clone(&actor.epoch),
2653        job: queued.job,
2654        completion: Some(queued.completion),
2655        request_id: queued.request_id,
2656        command: queued.command,
2657        heavy_permit,
2658        cancellation: queued.cancellation,
2659        execution_started: Arc::new(AtomicBool::new(false)),
2660        completion_guard: None,
2661    })
2662}
2663
2664fn worker_loop(run_rx: Receiver<RunJob>) {
2665    while let Ok(mut run_job) = run_rx.recv() {
2666        let response =
2667            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run_lane_job(&mut run_job)));
2668        let panicked = response.is_err();
2669        let response = match response {
2670            Ok(response) => response,
2671            Err(payload) => panic_response(
2672                run_job.request_id.clone(),
2673                &run_job.command,
2674                payload.as_ref(),
2675            ),
2676        };
2677
2678        if let Some(completion) = run_job.completion.take() {
2679            completion.send(response);
2680        }
2681        run_job
2682            .completion_guard
2683            .as_mut()
2684            .expect("dispatched executor job is missing its completion guard")
2685            .set_outcome(if panicked {
2686                JobCompletionOutcome::Panicked
2687            } else {
2688                JobCompletionOutcome::Completed
2689            });
2690    }
2691}
2692
2693fn run_lane_job(run_job: &mut RunJob) -> Response {
2694    let _actor_scope =
2695        view_publication::ActorScope::install(Arc::clone(&run_job.ctx), Arc::clone(&run_job.epoch));
2696    let _cancellation_ctx = JobCancellationContextGuard::install(run_job.cancellation.clone());
2697    let missing_request_id = run_job.request_id.clone();
2698    let job = std::mem::replace(
2699        &mut run_job.job,
2700        Box::new(move |_| {
2701            Response::error(
2702                missing_request_id,
2703                "job_missing",
2704                "executor job already taken",
2705            )
2706        }),
2707    );
2708
2709    let run = |job: ExecutorJob| {
2710        let can_start = run_job
2711            .cancellation
2712            .as_ref()
2713            .is_none_or(JobCancellation::mark_running);
2714        if !can_start {
2715            return Response::error(
2716                run_job.request_id.clone(),
2717                "request_cancelled",
2718                "request cancelled before execution",
2719            );
2720        }
2721        run_job.execution_started.store(true, Ordering::Release);
2722        job(&run_job.ctx)
2723    };
2724
2725    match run_job.lane {
2726        Lane::PureRead | Lane::SerialLspStatus => {
2727            if let Some(cancellation) = run_job.cancellation.as_ref() {
2728                loop {
2729                    if let Some(_epoch) = run_job.epoch.try_read_for(Duration::from_millis(25)) {
2730                        return run(job);
2731                    }
2732                    if cancellation.cancel_requested_before_commit() {
2733                        return Response::error(
2734                            run_job.request_id.clone(),
2735                            "request_cancelled",
2736                            "request cancelled before execution",
2737                        );
2738                    }
2739                }
2740            }
2741            let _epoch = run_job.epoch.read();
2742            run(job)
2743        }
2744        // Cache and index builders reject stale generations and serialize
2745        // publication internally. Taking the actor write gate after the command
2746        // constructs its response would delay completion behind maintenance and
2747        // park later readers behind an otherwise unnecessary writer.
2748        Lane::HeavyInit => run(job),
2749        Lane::Mutating => {
2750            let _epoch = run_job.epoch.write();
2751            run(job)
2752        }
2753        Lane::MaintenanceCommit => {
2754            // Same gate as reads: the job's mutations are protected by the
2755            // touched subsystems' own locks, and holding only the read gate
2756            // lets interactive PureReads overlap freely.
2757            let _epoch = run_job.epoch.read();
2758            run(job)
2759        }
2760    }
2761}
2762
2763#[derive(Debug)]
2764struct HeavySemaphore {
2765    available: AtomicUsize,
2766    max: usize,
2767}
2768
2769impl HeavySemaphore {
2770    fn new(permits: usize) -> Self {
2771        Self {
2772            available: AtomicUsize::new(permits),
2773            max: permits,
2774        }
2775    }
2776
2777    fn try_acquire(self: &Arc<Self>) -> Option<HeavyPermit> {
2778        loop {
2779            let available = self.available.load(Ordering::Acquire);
2780            if available == 0 {
2781                return None;
2782            }
2783            if self
2784                .available
2785                .compare_exchange(
2786                    available,
2787                    available - 1,
2788                    Ordering::AcqRel,
2789                    Ordering::Acquire,
2790                )
2791                .is_ok()
2792            {
2793                return Some(HeavyPermit {
2794                    semaphore: Arc::clone(self),
2795                });
2796            }
2797        }
2798    }
2799}
2800
2801struct HeavyPermit {
2802    semaphore: Arc<HeavySemaphore>,
2803}
2804
2805impl Drop for HeavyPermit {
2806    fn drop(&mut self) {
2807        let previous = self.semaphore.available.fetch_add(1, Ordering::Release);
2808        debug_assert!(previous < self.semaphore.max);
2809    }
2810}