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
11const 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
31pub fn acquire_blocking(kind: &str) -> ColdBuildPermit {
40 acquire_blocking_while(kind, || true).expect("unconditional cold-build admission")
41}
42
43pub 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#[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#[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#[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
136pub(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
145pub(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#[derive(Debug)]
167pub(crate) struct StandingColdBuildPermit {
168 _permit: ColdBuildPermit,
169 pub(crate) admission_epoch: u64,
170}
171
172pub(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 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 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 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 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 #[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 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 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 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}