Skip to main content

aft/
cold_build_limiter.rs

1use std::collections::{BTreeMap, VecDeque};
2use std::sync::atomic::{AtomicUsize, Ordering};
3use std::sync::{Arc, LazyLock, Mutex};
4use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
5
6#[cfg(not(test))]
7const DEFAULT_COLD_BUILD_LIMIT: usize = 2;
8#[cfg(test)]
9const DEFAULT_COLD_BUILD_LIMIT: usize = 1024;
10
11// This is an internal harness record, not a user-facing limiter setting. The
12// test harness performs 32 release/admission cycles, so retain enough events to
13// cover that exercise while bounding memory use in a long-lived daemon.
14const ADMISSION_EVENT_RETENTION: usize = 64;
15
16static GLOBAL_COLD_BUILD_LIMITER: LazyLock<Arc<ColdBuildLimiter>> =
17    LazyLock::new(|| Arc::new(ColdBuildLimiter::new(DEFAULT_COLD_BUILD_LIMIT)));
18
19pub(crate) fn global_limiter() -> Arc<ColdBuildLimiter> {
20    Arc::clone(&GLOBAL_COLD_BUILD_LIMITER)
21}
22
23pub(crate) fn isolated_limiter(limit: usize) -> Arc<ColdBuildLimiter> {
24    Arc::new(ColdBuildLimiter::new(limit))
25}
26
27pub fn try_acquire() -> Option<ColdBuildPermit> {
28    GLOBAL_COLD_BUILD_LIMITER.try_acquire()
29}
30
31/// Block until a build slot is free, then take it.
32///
33/// For build sites with no reschedule path (search-index builds spawn once per
34/// configure): skipping would strand the index, so past-cap work waits instead.
35/// Production captures showed concurrent per-root builds starving dispatch
36/// while CPU sat idle; waiting serializes that pressure at the source. Only
37/// call from dedicated background threads, never the dispatch thread or an
38/// executor worker.
39pub fn acquire_blocking(kind: &str) -> ColdBuildPermit {
40    acquire_blocking_while(kind, || true).expect("unconditional cold-build admission")
41}
42
43/// Wait for a build slot while `admitted` remains true. The predicate is checked
44/// before every attempt, so a root that becomes unbound does not consume a slot
45/// after spending time queued behind the process-wide cap.
46pub fn acquire_blocking_while(kind: &str, admitted: impl Fn() -> bool) -> Option<ColdBuildPermit> {
47    acquire_blocking_while_with_limiter(&GLOBAL_COLD_BUILD_LIMITER, kind, admitted)
48}
49
50pub(crate) fn acquire_blocking_while_with_limiter(
51    limiter: &Arc<ColdBuildLimiter>,
52    kind: &str,
53    admitted: impl Fn() -> bool,
54) -> Option<ColdBuildPermit> {
55    let request = ColdBuildAdmissionRequest::new(kind, ColdBuildAdmissionClass::Maintenance);
56    acquire_blocking_while_inner(limiter, kind, Some(&request), admitted, || false)
57}
58
59/// Identify the source of a cold-build request without exposing a limiter knob.
60///
61/// The classes deliberately have no absolute priority ordering. When a class
62/// was admitted most recently and another class is waiting, the limiter defers
63/// that repeat admission. Standing adds a yielding class to this existing
64/// rotation; it never installs a priority retry path.
65#[derive(Clone, Copy, Debug, Eq, PartialEq)]
66pub(crate) enum ColdBuildAdmissionClass {
67    InspectTriggered,
68    Maintenance,
69    Standing,
70}
71
72const ADMISSION_CLASS_COUNT: usize = 3;
73
74impl ColdBuildAdmissionClass {
75    const fn index(self) -> usize {
76        match self {
77            Self::InspectTriggered => 0,
78            Self::Maintenance => 1,
79            Self::Standing => 2,
80        }
81    }
82
83    const fn label(self) -> &'static str {
84        match self {
85            Self::InspectTriggered => "inspect-triggered",
86            Self::Maintenance => "maintenance",
87            Self::Standing => "standing",
88        }
89    }
90}
91
92/// Internal request metadata attached to an admission attempt.
93#[derive(Clone, Debug, Eq, PartialEq)]
94pub(crate) struct ColdBuildAdmissionRequest {
95    request_id: String,
96    class: ColdBuildAdmissionClass,
97}
98
99impl ColdBuildAdmissionRequest {
100    pub(crate) fn new(request_id: impl Into<String>, class: ColdBuildAdmissionClass) -> Self {
101        Self {
102            request_id: request_id.into(),
103            class,
104        }
105    }
106}
107
108/// A structured record of a successful cold-build admission.
109///
110/// `admission_order` records the order in which permits were admitted, not the
111/// order in which waiters arrived. Arrival-order and overtake checks require a
112/// separate ticketed-ordering design.
113#[derive(Clone, Debug, Eq, PartialEq)]
114pub(crate) struct ColdBuildAdmissionEvent {
115    pub(crate) request_id: String,
116    pub(crate) class: ColdBuildAdmissionClass,
117    pub(crate) admission_order: u64,
118}
119
120#[derive(Clone, Debug, Eq, PartialEq)]
121pub(crate) struct ColdBuildCensusEntry {
122    pub(crate) domain: &'static str,
123    pub(crate) root: String,
124    pub(crate) kind: String,
125    pub(crate) acquired_at_ms: u64,
126    pub(crate) age_ms: u64,
127}
128
129#[derive(Clone, Debug, Eq, PartialEq)]
130pub(crate) struct ColdBuildLimiterCensus {
131    pub(crate) cap: usize,
132    pub(crate) holders: Vec<ColdBuildCensusEntry>,
133    pub(crate) queued: Vec<ColdBuildCensusEntry>,
134}
135
136/// Attempt immediate admission without bypassing queued requests from the other
137/// class. Background schedulers use this when they can defer rejected work.
138pub(crate) fn try_acquire_classified_with_limiter(
139    limiter: &Arc<ColdBuildLimiter>,
140    request: &ColdBuildAdmissionRequest,
141) -> Option<ColdBuildPermit> {
142    limiter.try_acquire_classified(request, &request.request_id, || true)
143}
144
145/// Acquire a limiter permit for a classified request while it remains admitted
146/// and uncancelled.
147///
148/// Cancellation is sampled before every acquisition attempt and again after a
149/// permit has been acquired. The second check closes the gap before a build can
150/// start: a newly cancelled request returns the permit without emitting an
151/// admission event.
152pub(crate) fn acquire_blocking_while_cancellable_with_limiter(
153    limiter: &Arc<ColdBuildLimiter>,
154    kind: &str,
155    request: ColdBuildAdmissionRequest,
156    admitted: impl Fn() -> bool,
157    cancelled: impl Fn() -> bool,
158) -> Option<ColdBuildPermit> {
159    acquire_blocking_while_inner(limiter, kind, Some(&request), admitted, cancelled)
160}
161
162/// A Standing permit preserves the lifecycle admission epoch captured before
163/// limiter acquisition. Checkpoint code drops it before yielding and carries the
164/// same epoch into the next attempt, so an obsolete build cannot become current
165/// merely by waiting for a slot.
166#[derive(Debug)]
167pub(crate) struct StandingColdBuildPermit {
168    _permit: ColdBuildPermit,
169    pub(crate) admission_epoch: u64,
170}
171
172/// Standing performs the same waiter inspection before initial acquisition and
173/// checkpoint reacquisition because both call this one function. It declines
174/// immediately when an interactive or normal-maintenance waiter is visible.
175pub(crate) fn acquire_standing_while_cancellable_with_limiter(
176    limiter: &Arc<ColdBuildLimiter>,
177    kind: &str,
178    request_id: impl Into<String>,
179    admission_epoch: u64,
180    admitted: impl Fn() -> bool,
181    cancelled: impl Fn() -> bool,
182) -> Option<StandingColdBuildPermit> {
183    let request = ColdBuildAdmissionRequest::new(request_id, ColdBuildAdmissionClass::Standing);
184    acquire_blocking_while_inner(limiter, kind, Some(&request), admitted, cancelled).map(|permit| {
185        StandingColdBuildPermit {
186            _permit: permit,
187            admission_epoch,
188        }
189    })
190}
191
192fn acquire_blocking_while_inner(
193    limiter: &Arc<ColdBuildLimiter>,
194    kind: &str,
195    request: Option<&ColdBuildAdmissionRequest>,
196    admitted: impl Fn() -> bool,
197    cancelled: impl Fn() -> bool,
198) -> Option<ColdBuildPermit> {
199    let _waiter = request.map(|request| AdmissionWaiter::register(limiter, request, kind));
200    let started = Instant::now();
201    let mut logged = false;
202    loop {
203        if !admitted() || cancelled() {
204            return None;
205        }
206        // Standing yields to any already-queued interactive or ordinary
207        // maintenance contender. Returning None keeps it resumable; callers
208        // use the same path again at the next checkpoint without priority tags.
209        if request.is_some_and(|request| request.class == ColdBuildAdmissionClass::Standing)
210            && limiter.has_non_standing_waiters()
211        {
212            return None;
213        }
214        let revoked_after_acquire = std::cell::Cell::new(false);
215        let permit = match request {
216            Some(request) => limiter.try_acquire_classified(request, kind, || {
217                let still_admitted = admitted()
218                    && !cancelled()
219                    && (request.class != ColdBuildAdmissionClass::Standing
220                        || !limiter.has_non_standing_waiters());
221                revoked_after_acquire.set(!still_admitted);
222                still_admitted
223            }),
224            None => limiter.try_acquire().and_then(|permit| {
225                // A request can become unbound or cancelled after the pre-attempt
226                // check but before the permit is acquired. Recheck while owning
227                // the slot; dropping the permit returns it before any build starts.
228                if admitted() && !cancelled() {
229                    Some(permit)
230                } else {
231                    revoked_after_acquire.set(true);
232                    drop(permit);
233                    None
234                }
235            }),
236        };
237        if revoked_after_acquire.get() {
238            return None;
239        }
240        if let Some(permit) = permit {
241            let wait_ms = started.elapsed().as_millis().min(u64::MAX as u128) as u64;
242            if wait_ms > 0 {
243                crate::logging::note_tool_call_wait(
244                    crate::run_tool_call::WaitingOn::Limiter,
245                    None,
246                    wait_ms,
247                );
248            }
249            if logged {
250                match request {
251                    Some(request) => crate::slog_info!(
252                        "{} cold-build slot acquired after {}ms wait: request={} kind={}",
253                        request.class.label(),
254                        wait_ms,
255                        request.request_id,
256                        kind
257                    ),
258                    None => crate::slog_info!(
259                        "maintenance build slot acquired after {}ms wait: {}",
260                        wait_ms,
261                        kind
262                    ),
263                }
264            }
265            return Some(permit);
266        }
267        if !logged {
268            match request {
269                Some(request) => crate::slog_info!(
270                    "{} cold-build request queued behind concurrency cap ({}): request={} kind={}",
271                    request.class.label(),
272                    limiter.limit(),
273                    request.request_id,
274                    kind
275                ),
276                None => crate::slog_info!(
277                    "maintenance build queued behind concurrency cap ({}): {}",
278                    limiter.limit(),
279                    kind
280                ),
281            }
282            logged = true;
283        }
284        std::thread::sleep(Duration::from_millis(100));
285    }
286}
287
288pub fn limit() -> usize {
289    GLOBAL_COLD_BUILD_LIMITER.limit()
290}
291
292#[cfg(test)]
293pub(crate) fn test_limiter(limit: usize) -> Arc<ColdBuildLimiter> {
294    Arc::new(ColdBuildLimiter::new(limit))
295}
296
297#[cfg(test)]
298pub(crate) fn acquire_blocking_while_with_test_limiter(
299    limiter: &Arc<ColdBuildLimiter>,
300    kind: &str,
301    admitted: impl Fn() -> bool,
302) -> Option<ColdBuildPermit> {
303    acquire_blocking_while_with_limiter(limiter, kind, admitted)
304}
305
306#[derive(Debug)]
307pub(crate) struct ColdBuildLimiter {
308    available: AtomicUsize,
309    limit: usize,
310    /// Waiter counts are atomics so a Standing contender can yield without a
311    /// second hot-path lock. Rotation still uses `admission_state` below.
312    waiting_by_class: [AtomicUsize; ADMISSION_CLASS_COUNT],
313    admission_state: Mutex<AdmissionState>,
314}
315
316#[derive(Debug)]
317struct AdmissionState {
318    last_admitted_class: Option<ColdBuildAdmissionClass>,
319    next_admission_order: u64,
320    next_census_id: u64,
321    events: VecDeque<ColdBuildAdmissionEvent>,
322    holders: BTreeMap<u64, CensusRecord>,
323    queued: BTreeMap<u64, CensusRecord>,
324}
325
326#[derive(Clone, Debug)]
327struct CensusRecord {
328    domain: &'static str,
329    root: String,
330    kind: String,
331    started_at_ms: u64,
332}
333
334fn unix_millis_now() -> u64 {
335    SystemTime::now()
336        .duration_since(UNIX_EPOCH)
337        .unwrap_or_default()
338        .as_millis()
339        .min(u128::from(u64::MAX)) as u64
340}
341
342fn request_root(request_id: &str) -> String {
343    request_id
344        .strip_prefix("inspect:")
345        .and_then(|value| value.rsplit_once(':').map(|(root, _)| root))
346        .unwrap_or("unknown")
347        .to_string()
348}
349
350impl ColdBuildLimiter {
351    fn new(limit: usize) -> Self {
352        let limit = limit.max(1);
353        Self {
354            available: AtomicUsize::new(limit),
355            limit,
356            waiting_by_class: std::array::from_fn(|_| AtomicUsize::new(0)),
357            admission_state: Mutex::new(AdmissionState {
358                last_admitted_class: None,
359                next_admission_order: 1,
360                next_census_id: 1,
361                events: VecDeque::with_capacity(ADMISSION_EVENT_RETENTION),
362                holders: BTreeMap::new(),
363                queued: BTreeMap::new(),
364            }),
365        }
366    }
367
368    pub(crate) fn limit(&self) -> usize {
369        self.limit
370    }
371
372    fn try_take_slot(&self) -> bool {
373        loop {
374            let available = self.available.load(Ordering::Acquire);
375            if available == 0 {
376                return false;
377            }
378            if self
379                .available
380                .compare_exchange(
381                    available,
382                    available - 1,
383                    Ordering::AcqRel,
384                    Ordering::Acquire,
385                )
386                .is_ok()
387            {
388                return true;
389            }
390        }
391    }
392
393    pub(crate) fn try_acquire(self: &Arc<Self>) -> Option<ColdBuildPermit> {
394        if !self.try_take_slot() {
395            return None;
396        }
397        let mut state = self
398            .admission_state
399            .lock()
400            .unwrap_or_else(std::sync::PoisonError::into_inner);
401        let census_id = state.next_census_id;
402        state.next_census_id = state.next_census_id.saturating_add(1);
403        state.holders.insert(
404            census_id,
405            CensusRecord {
406                domain: "unclassified",
407                root: "unknown".to_string(),
408                kind: "unclassified".to_string(),
409                started_at_ms: unix_millis_now(),
410            },
411        );
412        Some(ColdBuildPermit {
413            limiter: Arc::clone(self),
414            census_id,
415        })
416    }
417
418    fn try_acquire_classified(
419        self: &Arc<Self>,
420        request: &ColdBuildAdmissionRequest,
421        kind: &str,
422        admitted_after_acquire: impl FnOnce() -> bool,
423    ) -> Option<ColdBuildPermit> {
424        let mut state = self
425            .admission_state
426            .lock()
427            .unwrap_or_else(std::sync::PoisonError::into_inner);
428        // Class alternation arbitrates the final released slot. When several
429        // slots are free, admitting both classes is not starvation and avoids
430        // stranding independent roots behind an artificial one-at-a-time turn.
431        let available = self.available.load(Ordering::Acquire);
432        if available <= 1
433            && self.has_waiter_from_another_class(request.class)
434            && state.last_admitted_class == Some(request.class)
435        {
436            return None;
437        }
438        if !self.try_take_slot() {
439            return None;
440        }
441        if !admitted_after_acquire() {
442            self.available.fetch_add(1, Ordering::Release);
443            return None;
444        }
445        Self::record_admission_locked(&mut state, request);
446        let census_id = state.next_census_id;
447        state.next_census_id = state.next_census_id.saturating_add(1);
448        state.holders.insert(
449            census_id,
450            CensusRecord {
451                domain: request.class.label(),
452                root: request_root(&request.request_id),
453                kind: kind.to_string(),
454                started_at_ms: unix_millis_now(),
455            },
456        );
457        Some(ColdBuildPermit {
458            limiter: Arc::clone(self),
459            census_id,
460        })
461    }
462
463    fn record_admission_locked(state: &mut AdmissionState, request: &ColdBuildAdmissionRequest) {
464        let event = ColdBuildAdmissionEvent {
465            request_id: request.request_id.clone(),
466            class: request.class,
467            admission_order: state.next_admission_order,
468        };
469        state.next_admission_order += 1;
470        state.last_admitted_class = Some(request.class);
471        if state.events.len() == ADMISSION_EVENT_RETENTION {
472            state.events.pop_front();
473        }
474        state.events.push_back(event);
475    }
476
477    /// Expose recorded admissions to internal tests and harness code so they
478    /// can verify limiter behavior without parsing log output.
479    #[cfg_attr(not(test), allow(dead_code))]
480    pub(crate) fn admission_events(&self) -> Vec<ColdBuildAdmissionEvent> {
481        self.admission_state
482            .lock()
483            .unwrap_or_else(std::sync::PoisonError::into_inner)
484            .events
485            .iter()
486            .cloned()
487            .collect()
488    }
489
490    /// O(1) waiter-set inspection used by Standing before its initial permit
491    /// and every checkpoint reacquisition. The counters are maintained by RAII
492    /// waiters and therefore need no scheduler-state lock on this hot path.
493    pub(crate) fn census(&self) -> ColdBuildLimiterCensus {
494        let now_ms = unix_millis_now();
495        let state = self
496            .admission_state
497            .lock()
498            .unwrap_or_else(std::sync::PoisonError::into_inner);
499        let render = |entry: &CensusRecord| ColdBuildCensusEntry {
500            domain: entry.domain,
501            root: entry.root.clone(),
502            kind: entry.kind.clone(),
503            acquired_at_ms: entry.started_at_ms,
504            age_ms: now_ms.saturating_sub(entry.started_at_ms),
505        };
506        ColdBuildLimiterCensus {
507            cap: self.limit,
508            holders: state.holders.values().map(render).collect(),
509            queued: state.queued.values().map(render).collect(),
510        }
511    }
512
513    pub(crate) fn has_non_standing_waiters(&self) -> bool {
514        self.waiting_by_class[ColdBuildAdmissionClass::InspectTriggered.index()]
515            .load(Ordering::Acquire)
516            > 0
517            || self.waiting_by_class[ColdBuildAdmissionClass::Maintenance.index()]
518                .load(Ordering::Acquire)
519                > 0
520    }
521
522    fn has_waiter_from_another_class(&self, class: ColdBuildAdmissionClass) -> bool {
523        self.waiting_by_class
524            .iter()
525            .enumerate()
526            .any(|(index, waiters)| index != class.index() && waiters.load(Ordering::Acquire) > 0)
527    }
528
529    #[cfg(test)]
530    fn waiting_by_class_for_test(&self) -> [usize; ADMISSION_CLASS_COUNT] {
531        std::array::from_fn(|index| self.waiting_by_class[index].load(Ordering::Acquire))
532    }
533}
534
535struct AdmissionWaiter {
536    limiter: Arc<ColdBuildLimiter>,
537    class: ColdBuildAdmissionClass,
538    census_id: u64,
539}
540
541impl AdmissionWaiter {
542    fn register(
543        limiter: &Arc<ColdBuildLimiter>,
544        request: &ColdBuildAdmissionRequest,
545        kind: &str,
546    ) -> Self {
547        limiter.waiting_by_class[request.class.index()].fetch_add(1, Ordering::AcqRel);
548        let mut state = limiter
549            .admission_state
550            .lock()
551            .unwrap_or_else(std::sync::PoisonError::into_inner);
552        let census_id = state.next_census_id;
553        state.next_census_id = state.next_census_id.saturating_add(1);
554        state.queued.insert(
555            census_id,
556            CensusRecord {
557                domain: request.class.label(),
558                root: request_root(&request.request_id),
559                kind: kind.to_string(),
560                started_at_ms: unix_millis_now(),
561            },
562        );
563        drop(state);
564        Self {
565            limiter: Arc::clone(limiter),
566            class: request.class,
567            census_id,
568        }
569    }
570}
571
572impl Drop for AdmissionWaiter {
573    fn drop(&mut self) {
574        let previous =
575            self.limiter.waiting_by_class[self.class.index()].fetch_sub(1, Ordering::AcqRel);
576        debug_assert!(previous > 0);
577        self.limiter
578            .admission_state
579            .lock()
580            .unwrap_or_else(std::sync::PoisonError::into_inner)
581            .queued
582            .remove(&self.census_id);
583    }
584}
585
586#[derive(Debug)]
587pub struct ColdBuildPermit {
588    limiter: Arc<ColdBuildLimiter>,
589    census_id: u64,
590}
591
592impl Drop for ColdBuildPermit {
593    fn drop(&mut self) {
594        self.limiter
595            .admission_state
596            .lock()
597            .unwrap_or_else(std::sync::PoisonError::into_inner)
598            .holders
599            .remove(&self.census_id);
600        let previous = self.limiter.available.fetch_add(1, Ordering::Release);
601        debug_assert!(previous < self.limiter.limit);
602    }
603}
604
605#[cfg(test)]
606mod tests {
607    use super::*;
608
609    // These tests mutate the process-global limiter; run them one at a time.
610    fn serial() -> std::sync::MutexGuard<'static, ()> {
611        static M: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
612        M.get_or_init(|| std::sync::Mutex::new(()))
613            .lock()
614            .unwrap_or_else(std::sync::PoisonError::into_inner)
615    }
616
617    fn wait_for_waiters(limiter: &ColdBuildLimiter) {
618        let deadline = Instant::now() + Duration::from_secs(3);
619        loop {
620            let waiting = limiter.waiting_by_class_for_test();
621            if waiting[ColdBuildAdmissionClass::InspectTriggered.index()] > 0
622                && waiting[ColdBuildAdmissionClass::Maintenance.index()] > 0
623            {
624                return;
625            }
626            assert!(
627                Instant::now() < deadline,
628                "both admission classes must remain queued; waiting={waiting:?}"
629            );
630            std::thread::yield_now();
631        }
632    }
633
634    #[test]
635    fn permits_release_on_drop() {
636        let _serial = serial();
637        let before = GLOBAL_COLD_BUILD_LIMITER.available.load(Ordering::Acquire);
638        {
639            let _a = acquire_blocking("test-a");
640            let _b = acquire_blocking("test-b");
641            assert_eq!(
642                GLOBAL_COLD_BUILD_LIMITER.available.load(Ordering::Acquire),
643                before - 2
644            );
645        }
646        assert_eq!(
647            GLOBAL_COLD_BUILD_LIMITER.available.load(Ordering::Acquire),
648            before
649        );
650    }
651
652    #[test]
653    fn acquire_blocking_waits_until_release() {
654        let _serial = serial();
655        // Drain every slot, then prove a waiter blocks until one holder drops.
656        let mut held: Vec<ColdBuildPermit> = Vec::new();
657        while let Some(permit) = try_acquire() {
658            held.push(permit);
659        }
660        let waiter = std::thread::spawn(|| {
661            let _p = acquire_blocking("waiter");
662        });
663        std::thread::sleep(std::time::Duration::from_millis(250));
664        assert!(!waiter.is_finished(), "waiter must block while cap is full");
665        drop(held.pop());
666        waiter.join().expect("waiter finishes after release");
667        drop(held);
668    }
669
670    #[test]
671    fn admission_revoked_between_check_and_permit_drops_the_slot() {
672        let _serial = serial();
673        let before = GLOBAL_COLD_BUILD_LIMITER.available.load(Ordering::Acquire);
674        let checks = AtomicUsize::new(0);
675
676        let permit = acquire_blocking_while("revoked-after-cas", || {
677            checks.fetch_add(1, Ordering::SeqCst) == 0
678        });
679
680        assert!(permit.is_none());
681        assert_eq!(checks.load(Ordering::SeqCst), 2);
682        assert_eq!(
683            GLOBAL_COLD_BUILD_LIMITER.available.load(Ordering::Acquire),
684            before,
685            "revoked admission must return the just-acquired slot"
686        );
687    }
688
689    #[test]
690    fn conditional_waiter_cancels_without_consuming_a_released_slot() {
691        let _serial = serial();
692        let mut held = Vec::new();
693        while let Some(permit) = try_acquire() {
694            held.push(permit);
695        }
696        let admitted = Arc::new(std::sync::atomic::AtomicBool::new(true));
697        let waiter_admitted = Arc::clone(&admitted);
698        let waiter = std::thread::spawn(move || {
699            acquire_blocking_while("conditional waiter", || {
700                waiter_admitted.load(Ordering::SeqCst)
701            })
702        });
703        std::thread::sleep(Duration::from_millis(150));
704        admitted.store(false, Ordering::SeqCst);
705        assert!(
706            waiter.join().expect("conditional waiter joins").is_none(),
707            "revoked work must leave the cold-build queue without taking a permit"
708        );
709        drop(held);
710    }
711
712    #[test]
713    fn cancellation_after_acquisition_returns_the_permit_without_an_event() {
714        let limiter = test_limiter(1);
715        let cancellation_checks = AtomicUsize::new(0);
716
717        let permit = acquire_blocking_while_cancellable_with_limiter(
718            &limiter,
719            "cancel-after-acquire",
720            ColdBuildAdmissionRequest::new(
721                "inspect-cancelled",
722                ColdBuildAdmissionClass::InspectTriggered,
723            ),
724            || true,
725            || cancellation_checks.fetch_add(1, Ordering::SeqCst) > 0,
726        );
727
728        assert!(permit.is_none());
729        assert_eq!(cancellation_checks.load(Ordering::SeqCst), 2);
730        assert_eq!(
731            limiter.available.load(Ordering::Acquire),
732            1,
733            "post-acquisition cancellation must return the permit"
734        );
735        assert!(
736            limiter.admission_events().is_empty(),
737            "cancelled work must not emit a successful admission"
738        );
739    }
740
741    #[test]
742    fn standing_yields_before_initial_and_checkpoint_reacquisition_when_non_standing_waits() {
743        let limiter = test_limiter(1);
744        let maintenance = ColdBuildAdmissionRequest::new(
745            "maintenance-waiter",
746            ColdBuildAdmissionClass::Maintenance,
747        );
748        let non_standing_waiter =
749            AdmissionWaiter::register(&limiter, &maintenance, "maintenance waiter");
750
751        assert!(acquire_standing_while_cancellable_with_limiter(
752            &limiter,
753            "standing-initial",
754            "standing-initial",
755            41,
756            || true,
757            || false,
758        )
759        .is_none());
760
761        drop(non_standing_waiter);
762        let first = acquire_standing_while_cancellable_with_limiter(
763            &limiter,
764            "standing-checkpoint",
765            "standing-checkpoint",
766            41,
767            || true,
768            || false,
769        )
770        .expect("standing may acquire once ordinary waiters clear");
771        assert_eq!(first.admission_epoch, 41);
772        drop(first);
773
774        let inspect = ColdBuildAdmissionRequest::new(
775            "inspect-waiter",
776            ColdBuildAdmissionClass::InspectTriggered,
777        );
778        let non_standing_waiter = AdmissionWaiter::register(&limiter, &inspect, "inspect waiter");
779        assert!(acquire_standing_while_cancellable_with_limiter(
780            &limiter,
781            "standing-reacquire",
782            "standing-reacquire",
783            41,
784            || true,
785            || false,
786        )
787        .is_none());
788        drop(non_standing_waiter);
789    }
790
791    #[test]
792    fn inspect_waiter_takes_next_release_ahead_of_queued_maintenance() {
793        let limiter = test_limiter(1);
794        let active_request = ColdBuildAdmissionRequest::new(
795            "active-semantic-seed",
796            ColdBuildAdmissionClass::Maintenance,
797        );
798        let active = try_acquire_classified_with_limiter(&limiter, &active_request)
799            .expect("active maintenance build holds the slot");
800        let (admitted_tx, admitted_rx) = std::sync::mpsc::channel();
801        let mut waiters = Vec::new();
802
803        for (request_id, class) in [
804            ("queued-refresh", ColdBuildAdmissionClass::Maintenance),
805            (
806                "blocking-inspect",
807                ColdBuildAdmissionClass::InspectTriggered,
808            ),
809        ] {
810            let limiter = Arc::clone(&limiter);
811            let admitted_tx = admitted_tx.clone();
812            waiters.push(std::thread::spawn(move || {
813                let permit = acquire_blocking_while_cancellable_with_limiter(
814                    &limiter,
815                    request_id,
816                    ColdBuildAdmissionRequest::new(request_id, class),
817                    || true,
818                    || false,
819                )
820                .expect("queued build is admitted");
821                admitted_tx
822                    .send((class, permit))
823                    .expect("test receives admitted permit");
824            }));
825        }
826        drop(admitted_tx);
827        wait_for_waiters(&limiter);
828        drop(active);
829
830        let (first_class, first_permit) = admitted_rx
831            .recv_timeout(Duration::from_secs(3))
832            .expect("released slot admits interactive inspect");
833        assert_eq!(first_class, ColdBuildAdmissionClass::InspectTriggered);
834        assert!(
835            admitted_rx.try_recv().is_err(),
836            "maintenance remains deferred"
837        );
838        drop(first_permit);
839
840        let (second_class, second_permit) = admitted_rx
841            .recv_timeout(Duration::from_secs(3))
842            .expect("maintenance resumes after inspect releases its slot");
843        assert_eq!(second_class, ColdBuildAdmissionClass::Maintenance);
844        drop(second_permit);
845        for waiter in waiters {
846            waiter.join().expect("admission waiter joins");
847        }
848    }
849
850    #[test]
851    fn admission_events_cover_both_classes_across_the_fixed_32_release_schedule() {
852        const RELEASE_COUNT: usize = 32;
853
854        let limiter = test_limiter(1);
855        let cancelled = Arc::new(std::sync::atomic::AtomicBool::new(false));
856        let (permit_tx, permit_rx) = std::sync::mpsc::channel();
857        let initial_permit = limiter.try_acquire().expect("hold the only slot");
858        let mut waiters = Vec::new();
859
860        for (request_id, class) in [
861            ("inspect-request", ColdBuildAdmissionClass::InspectTriggered),
862            ("maintenance-request", ColdBuildAdmissionClass::Maintenance),
863        ] {
864            let limiter = Arc::clone(&limiter);
865            let cancelled = Arc::clone(&cancelled);
866            let permit_tx = permit_tx.clone();
867            waiters.push(std::thread::spawn(move || {
868                while !cancelled.load(Ordering::SeqCst) {
869                    let permit = acquire_blocking_while_cancellable_with_limiter(
870                        &limiter,
871                        "fixed-release-test",
872                        ColdBuildAdmissionRequest::new(request_id, class),
873                        || true,
874                        || cancelled.load(Ordering::SeqCst),
875                    );
876                    let Some(permit) = permit else {
877                        return;
878                    };
879                    if permit_tx.send(permit).is_err() {
880                        return;
881                    }
882                }
883            }));
884        }
885        drop(permit_tx);
886
887        wait_for_waiters(&limiter);
888        let mut released_permit = Some(initial_permit);
889        for release in 1..RELEASE_COUNT {
890            wait_for_waiters(&limiter);
891            drop(released_permit.take());
892            released_permit = Some(
893                permit_rx
894                    .recv_timeout(Duration::from_secs(3))
895                    .unwrap_or_else(|error| {
896                        panic!("release {release} must admit a waiter: {error}")
897                    }),
898            );
899        }
900        wait_for_waiters(&limiter);
901        drop(released_permit);
902        let consumed_by_build = permit_rx
903            .recv_timeout(Duration::from_secs(3))
904            .unwrap_or_else(|error| panic!("release {RELEASE_COUNT} must admit a waiter: {error}"));
905
906        cancelled.store(true, Ordering::SeqCst);
907        for waiter in waiters {
908            waiter.join().expect("cancelled waiter joins");
909        }
910
911        let events = limiter.admission_events();
912        assert_eq!(events.len(), RELEASE_COUNT);
913        assert!(events
914            .iter()
915            .any(|event| event.class == ColdBuildAdmissionClass::InspectTriggered));
916        assert!(events
917            .iter()
918            .any(|event| event.class == ColdBuildAdmissionClass::Maintenance));
919        assert!(events.iter().all(|event| matches!(
920            event.request_id.as_str(),
921            "inspect-request" | "maintenance-request"
922        )));
923        assert!(events
924            .iter()
925            .enumerate()
926            .all(|(index, event)| event.admission_order == index as u64 + 1));
927
928        assert_eq!(
929            limiter.available.load(Ordering::Acquire),
930            0,
931            "the final acquired permit must remain accounted for by the consumed build"
932        );
933        drop(consumed_by_build);
934        assert_eq!(
935            limiter.available.load(Ordering::Acquire),
936            1,
937            "releasing the consumed build permit must restore the limiter slot"
938        );
939    }
940}
941
942#[cfg(test)]
943mod census_tests {
944    use super::*;
945
946    #[test]
947    fn census_names_holders_and_queued_requests_without_holding_work_locks() {
948        let limiter = isolated_limiter(1);
949        let permit =
950            acquire_blocking_while_with_limiter(&limiter, "inspect:/tmp/project:1", || true)
951                .expect("first permit");
952        let holder = limiter.census().holders.pop().expect("holder census");
953        assert_eq!(holder.domain, "maintenance");
954        assert_eq!(holder.root, "/tmp/project");
955        assert!(holder.kind.contains("inspect:/tmp/project:1"));
956
957        let waiter_limiter = Arc::clone(&limiter);
958        let waiter = std::thread::spawn(move || {
959            acquire_blocking_while_with_limiter(&waiter_limiter, "inspect:/tmp/queued:2", || true)
960        });
961        let deadline = Instant::now() + Duration::from_secs(2);
962        while limiter.census().queued.is_empty() && Instant::now() < deadline {
963            std::thread::sleep(Duration::from_millis(5));
964        }
965        let queued = limiter.census().queued.pop().expect("queued census");
966        assert_eq!(queued.root, "/tmp/queued");
967        drop(permit);
968        drop(
969            waiter
970                .join()
971                .expect("waiter thread")
972                .expect("queued permit"),
973        );
974        assert!(limiter.census().holders.is_empty());
975        assert!(limiter.census().queued.is_empty());
976    }
977}