1use std::collections::VecDeque;
2use std::sync::atomic::{AtomicUsize, Ordering};
3use std::sync::{Arc, LazyLock, Mutex};
4use std::time::{Duration, Instant};
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}
70
71impl ColdBuildAdmissionClass {
72 const fn index(self) -> usize {
73 match self {
74 Self::InspectTriggered => 0,
75 Self::Maintenance => 1,
76 }
77 }
78
79 const fn other_index(self) -> usize {
80 match self {
81 Self::InspectTriggered => Self::Maintenance.index(),
82 Self::Maintenance => Self::InspectTriggered.index(),
83 }
84 }
85
86 const fn label(self) -> &'static str {
87 match self {
88 Self::InspectTriggered => "inspect-triggered",
89 Self::Maintenance => "maintenance",
90 }
91 }
92}
93
94#[derive(Clone, Debug, Eq, PartialEq)]
96pub(crate) struct ColdBuildAdmissionRequest {
97 request_id: String,
98 class: ColdBuildAdmissionClass,
99}
100
101impl ColdBuildAdmissionRequest {
102 pub(crate) fn new(request_id: impl Into<String>, class: ColdBuildAdmissionClass) -> Self {
103 Self {
104 request_id: request_id.into(),
105 class,
106 }
107 }
108}
109
110#[derive(Clone, Debug, Eq, PartialEq)]
116pub(crate) struct ColdBuildAdmissionEvent {
117 pub(crate) request_id: String,
118 pub(crate) class: ColdBuildAdmissionClass,
119 pub(crate) admission_order: u64,
120}
121
122pub(crate) fn try_acquire_classified_with_limiter(
125 limiter: &Arc<ColdBuildLimiter>,
126 request: &ColdBuildAdmissionRequest,
127) -> Option<ColdBuildPermit> {
128 limiter.try_acquire_classified(request, || true)
129}
130
131pub(crate) fn acquire_blocking_while_cancellable_with_limiter(
139 limiter: &Arc<ColdBuildLimiter>,
140 kind: &str,
141 request: ColdBuildAdmissionRequest,
142 admitted: impl Fn() -> bool,
143 cancelled: impl Fn() -> bool,
144) -> Option<ColdBuildPermit> {
145 acquire_blocking_while_inner(limiter, kind, Some(&request), admitted, cancelled)
146}
147
148fn acquire_blocking_while_inner(
149 limiter: &Arc<ColdBuildLimiter>,
150 kind: &str,
151 request: Option<&ColdBuildAdmissionRequest>,
152 admitted: impl Fn() -> bool,
153 cancelled: impl Fn() -> bool,
154) -> Option<ColdBuildPermit> {
155 let _waiter = request.map(|request| AdmissionWaiter::register(limiter, request.class));
156 let started = Instant::now();
157 let mut logged = false;
158 loop {
159 if !admitted() || cancelled() {
160 return None;
161 }
162 let revoked_after_acquire = std::cell::Cell::new(false);
163 let permit = match request {
164 Some(request) => limiter.try_acquire_classified(request, || {
165 let still_admitted = admitted() && !cancelled();
166 revoked_after_acquire.set(!still_admitted);
167 still_admitted
168 }),
169 None => limiter.try_acquire().and_then(|permit| {
170 if admitted() && !cancelled() {
174 Some(permit)
175 } else {
176 revoked_after_acquire.set(true);
177 drop(permit);
178 None
179 }
180 }),
181 };
182 if revoked_after_acquire.get() {
183 return None;
184 }
185 if let Some(permit) = permit {
186 if logged {
187 match request {
188 Some(request) => crate::slog_info!(
189 "{} cold-build slot acquired after {}ms wait: request={} kind={}",
190 request.class.label(),
191 started.elapsed().as_millis(),
192 request.request_id,
193 kind
194 ),
195 None => crate::slog_info!(
196 "maintenance build slot acquired after {}ms wait: {}",
197 started.elapsed().as_millis(),
198 kind
199 ),
200 }
201 }
202 return Some(permit);
203 }
204 if !logged {
205 match request {
206 Some(request) => crate::slog_info!(
207 "{} cold-build request queued behind concurrency cap ({}): request={} kind={}",
208 request.class.label(),
209 limiter.limit(),
210 request.request_id,
211 kind
212 ),
213 None => crate::slog_info!(
214 "maintenance build queued behind concurrency cap ({}): {}",
215 limiter.limit(),
216 kind
217 ),
218 }
219 logged = true;
220 }
221 std::thread::sleep(Duration::from_millis(100));
222 }
223}
224
225pub fn limit() -> usize {
226 GLOBAL_COLD_BUILD_LIMITER.limit()
227}
228
229#[cfg(test)]
230pub(crate) fn test_limiter(limit: usize) -> Arc<ColdBuildLimiter> {
231 Arc::new(ColdBuildLimiter::new(limit))
232}
233
234#[cfg(test)]
235pub(crate) fn acquire_blocking_while_with_test_limiter(
236 limiter: &Arc<ColdBuildLimiter>,
237 kind: &str,
238 admitted: impl Fn() -> bool,
239) -> Option<ColdBuildPermit> {
240 acquire_blocking_while_with_limiter(limiter, kind, admitted)
241}
242
243#[derive(Debug)]
244pub(crate) struct ColdBuildLimiter {
245 available: AtomicUsize,
246 limit: usize,
247 admission_state: Mutex<AdmissionState>,
248}
249
250#[derive(Debug)]
251struct AdmissionState {
252 waiting_by_class: [usize; 2],
253 last_admitted_class: Option<ColdBuildAdmissionClass>,
254 next_admission_order: u64,
255 events: VecDeque<ColdBuildAdmissionEvent>,
256}
257
258impl ColdBuildLimiter {
259 fn new(limit: usize) -> Self {
260 let limit = limit.max(1);
261 Self {
262 available: AtomicUsize::new(limit),
263 limit,
264 admission_state: Mutex::new(AdmissionState {
265 waiting_by_class: [0; 2],
266 last_admitted_class: None,
267 next_admission_order: 1,
268 events: VecDeque::with_capacity(ADMISSION_EVENT_RETENTION),
269 }),
270 }
271 }
272
273 pub(crate) fn limit(&self) -> usize {
274 self.limit
275 }
276
277 pub(crate) fn try_acquire(self: &Arc<Self>) -> Option<ColdBuildPermit> {
278 loop {
279 let available = self.available.load(Ordering::Acquire);
280 if available == 0 {
281 return None;
282 }
283 if self
284 .available
285 .compare_exchange(
286 available,
287 available - 1,
288 Ordering::AcqRel,
289 Ordering::Acquire,
290 )
291 .is_ok()
292 {
293 return Some(ColdBuildPermit {
294 limiter: Arc::clone(self),
295 });
296 }
297 }
298 }
299
300 fn try_acquire_classified(
301 self: &Arc<Self>,
302 request: &ColdBuildAdmissionRequest,
303 admitted_after_acquire: impl FnOnce() -> bool,
304 ) -> Option<ColdBuildPermit> {
305 let mut state = self
306 .admission_state
307 .lock()
308 .unwrap_or_else(std::sync::PoisonError::into_inner);
309 let available = self.available.load(Ordering::Acquire);
313 if available <= 1
314 && state.waiting_by_class[request.class.other_index()] > 0
315 && state.last_admitted_class == Some(request.class)
316 {
317 return None;
318 }
319 let permit = self.try_acquire()?;
320 if !admitted_after_acquire() {
321 drop(permit);
322 return None;
323 }
324 Self::record_admission_locked(&mut state, request);
325 Some(permit)
326 }
327
328 fn record_admission_locked(state: &mut AdmissionState, request: &ColdBuildAdmissionRequest) {
329 let event = ColdBuildAdmissionEvent {
330 request_id: request.request_id.clone(),
331 class: request.class,
332 admission_order: state.next_admission_order,
333 };
334 state.next_admission_order += 1;
335 state.last_admitted_class = Some(request.class);
336 if state.events.len() == ADMISSION_EVENT_RETENTION {
337 state.events.pop_front();
338 }
339 state.events.push_back(event);
340 }
341
342 #[cfg_attr(not(test), allow(dead_code))]
345 pub(crate) fn admission_events(&self) -> Vec<ColdBuildAdmissionEvent> {
346 self.admission_state
347 .lock()
348 .unwrap_or_else(std::sync::PoisonError::into_inner)
349 .events
350 .iter()
351 .cloned()
352 .collect()
353 }
354
355 #[cfg(test)]
356 fn waiting_by_class_for_test(&self) -> [usize; 2] {
357 self.admission_state
358 .lock()
359 .unwrap_or_else(std::sync::PoisonError::into_inner)
360 .waiting_by_class
361 }
362}
363
364struct AdmissionWaiter {
365 limiter: Arc<ColdBuildLimiter>,
366 class: ColdBuildAdmissionClass,
367}
368
369impl AdmissionWaiter {
370 fn register(limiter: &Arc<ColdBuildLimiter>, class: ColdBuildAdmissionClass) -> Self {
371 let mut state = limiter
372 .admission_state
373 .lock()
374 .unwrap_or_else(std::sync::PoisonError::into_inner);
375 state.waiting_by_class[class.index()] += 1;
376 drop(state);
377 Self {
378 limiter: Arc::clone(limiter),
379 class,
380 }
381 }
382}
383
384impl Drop for AdmissionWaiter {
385 fn drop(&mut self) {
386 let mut state = self
387 .limiter
388 .admission_state
389 .lock()
390 .unwrap_or_else(std::sync::PoisonError::into_inner);
391 let waiting = &mut state.waiting_by_class[self.class.index()];
392 debug_assert!(*waiting > 0);
393 *waiting = waiting.saturating_sub(1);
394 }
395}
396
397#[derive(Debug)]
398pub struct ColdBuildPermit {
399 limiter: Arc<ColdBuildLimiter>,
400}
401
402impl Drop for ColdBuildPermit {
403 fn drop(&mut self) {
404 let previous = self.limiter.available.fetch_add(1, Ordering::Release);
405 debug_assert!(previous < self.limiter.limit);
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 fn serial() -> std::sync::MutexGuard<'static, ()> {
415 static M: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
416 M.get_or_init(|| std::sync::Mutex::new(()))
417 .lock()
418 .unwrap_or_else(std::sync::PoisonError::into_inner)
419 }
420
421 fn wait_for_waiters(limiter: &ColdBuildLimiter) {
422 let deadline = Instant::now() + Duration::from_secs(3);
423 loop {
424 let waiting = limiter.waiting_by_class_for_test();
425 if waiting[ColdBuildAdmissionClass::InspectTriggered.index()] > 0
426 && waiting[ColdBuildAdmissionClass::Maintenance.index()] > 0
427 {
428 return;
429 }
430 assert!(
431 Instant::now() < deadline,
432 "both admission classes must remain queued; waiting={waiting:?}"
433 );
434 std::thread::yield_now();
435 }
436 }
437
438 #[test]
439 fn permits_release_on_drop() {
440 let _serial = serial();
441 let before = GLOBAL_COLD_BUILD_LIMITER.available.load(Ordering::Acquire);
442 {
443 let _a = acquire_blocking("test-a");
444 let _b = acquire_blocking("test-b");
445 assert_eq!(
446 GLOBAL_COLD_BUILD_LIMITER.available.load(Ordering::Acquire),
447 before - 2
448 );
449 }
450 assert_eq!(
451 GLOBAL_COLD_BUILD_LIMITER.available.load(Ordering::Acquire),
452 before
453 );
454 }
455
456 #[test]
457 fn acquire_blocking_waits_until_release() {
458 let _serial = serial();
459 let mut held: Vec<ColdBuildPermit> = Vec::new();
461 while let Some(permit) = try_acquire() {
462 held.push(permit);
463 }
464 let waiter = std::thread::spawn(|| {
465 let _p = acquire_blocking("waiter");
466 });
467 std::thread::sleep(std::time::Duration::from_millis(250));
468 assert!(!waiter.is_finished(), "waiter must block while cap is full");
469 drop(held.pop());
470 waiter.join().expect("waiter finishes after release");
471 drop(held);
472 }
473
474 #[test]
475 fn admission_revoked_between_check_and_permit_drops_the_slot() {
476 let _serial = serial();
477 let before = GLOBAL_COLD_BUILD_LIMITER.available.load(Ordering::Acquire);
478 let checks = AtomicUsize::new(0);
479
480 let permit = acquire_blocking_while("revoked-after-cas", || {
481 checks.fetch_add(1, Ordering::SeqCst) == 0
482 });
483
484 assert!(permit.is_none());
485 assert_eq!(checks.load(Ordering::SeqCst), 2);
486 assert_eq!(
487 GLOBAL_COLD_BUILD_LIMITER.available.load(Ordering::Acquire),
488 before,
489 "revoked admission must return the just-acquired slot"
490 );
491 }
492
493 #[test]
494 fn conditional_waiter_cancels_without_consuming_a_released_slot() {
495 let _serial = serial();
496 let mut held = Vec::new();
497 while let Some(permit) = try_acquire() {
498 held.push(permit);
499 }
500 let admitted = Arc::new(std::sync::atomic::AtomicBool::new(true));
501 let waiter_admitted = Arc::clone(&admitted);
502 let waiter = std::thread::spawn(move || {
503 acquire_blocking_while("conditional waiter", || {
504 waiter_admitted.load(Ordering::SeqCst)
505 })
506 });
507 std::thread::sleep(Duration::from_millis(150));
508 admitted.store(false, Ordering::SeqCst);
509 assert!(
510 waiter.join().expect("conditional waiter joins").is_none(),
511 "revoked work must leave the cold-build queue without taking a permit"
512 );
513 drop(held);
514 }
515
516 #[test]
517 fn cancellation_after_acquisition_returns_the_permit_without_an_event() {
518 let limiter = test_limiter(1);
519 let cancellation_checks = AtomicUsize::new(0);
520
521 let permit = acquire_blocking_while_cancellable_with_limiter(
522 &limiter,
523 "cancel-after-acquire",
524 ColdBuildAdmissionRequest::new(
525 "inspect-cancelled",
526 ColdBuildAdmissionClass::InspectTriggered,
527 ),
528 || true,
529 || cancellation_checks.fetch_add(1, Ordering::SeqCst) > 0,
530 );
531
532 assert!(permit.is_none());
533 assert_eq!(cancellation_checks.load(Ordering::SeqCst), 2);
534 assert_eq!(
535 limiter.available.load(Ordering::Acquire),
536 1,
537 "post-acquisition cancellation must return the permit"
538 );
539 assert!(
540 limiter.admission_events().is_empty(),
541 "cancelled work must not emit a successful admission"
542 );
543 }
544
545 #[test]
546 fn inspect_waiter_takes_next_release_ahead_of_queued_maintenance() {
547 let limiter = test_limiter(1);
548 let active_request = ColdBuildAdmissionRequest::new(
549 "active-semantic-seed",
550 ColdBuildAdmissionClass::Maintenance,
551 );
552 let active = try_acquire_classified_with_limiter(&limiter, &active_request)
553 .expect("active maintenance build holds the slot");
554 let (admitted_tx, admitted_rx) = std::sync::mpsc::channel();
555 let mut waiters = Vec::new();
556
557 for (request_id, class) in [
558 ("queued-refresh", ColdBuildAdmissionClass::Maintenance),
559 (
560 "blocking-inspect",
561 ColdBuildAdmissionClass::InspectTriggered,
562 ),
563 ] {
564 let limiter = Arc::clone(&limiter);
565 let admitted_tx = admitted_tx.clone();
566 waiters.push(std::thread::spawn(move || {
567 let permit = acquire_blocking_while_cancellable_with_limiter(
568 &limiter,
569 request_id,
570 ColdBuildAdmissionRequest::new(request_id, class),
571 || true,
572 || false,
573 )
574 .expect("queued build is admitted");
575 admitted_tx
576 .send((class, permit))
577 .expect("test receives admitted permit");
578 }));
579 }
580 drop(admitted_tx);
581 wait_for_waiters(&limiter);
582 drop(active);
583
584 let (first_class, first_permit) = admitted_rx
585 .recv_timeout(Duration::from_secs(3))
586 .expect("released slot admits interactive inspect");
587 assert_eq!(first_class, ColdBuildAdmissionClass::InspectTriggered);
588 assert!(
589 admitted_rx.try_recv().is_err(),
590 "maintenance remains deferred"
591 );
592 drop(first_permit);
593
594 let (second_class, second_permit) = admitted_rx
595 .recv_timeout(Duration::from_secs(3))
596 .expect("maintenance resumes after inspect releases its slot");
597 assert_eq!(second_class, ColdBuildAdmissionClass::Maintenance);
598 drop(second_permit);
599 for waiter in waiters {
600 waiter.join().expect("admission waiter joins");
601 }
602 }
603
604 #[test]
605 fn admission_events_cover_both_classes_across_the_fixed_32_release_schedule() {
606 const RELEASE_COUNT: usize = 32;
607
608 let limiter = test_limiter(1);
609 let cancelled = Arc::new(std::sync::atomic::AtomicBool::new(false));
610 let (permit_tx, permit_rx) = std::sync::mpsc::channel();
611 let initial_permit = limiter.try_acquire().expect("hold the only slot");
612 let mut waiters = Vec::new();
613
614 for (request_id, class) in [
615 ("inspect-request", ColdBuildAdmissionClass::InspectTriggered),
616 ("maintenance-request", ColdBuildAdmissionClass::Maintenance),
617 ] {
618 let limiter = Arc::clone(&limiter);
619 let cancelled = Arc::clone(&cancelled);
620 let permit_tx = permit_tx.clone();
621 waiters.push(std::thread::spawn(move || {
622 while !cancelled.load(Ordering::SeqCst) {
623 let permit = acquire_blocking_while_cancellable_with_limiter(
624 &limiter,
625 "fixed-release-test",
626 ColdBuildAdmissionRequest::new(request_id, class),
627 || true,
628 || cancelled.load(Ordering::SeqCst),
629 );
630 let Some(permit) = permit else {
631 return;
632 };
633 if permit_tx.send(permit).is_err() {
634 return;
635 }
636 }
637 }));
638 }
639 drop(permit_tx);
640
641 wait_for_waiters(&limiter);
642 let mut released_permit = Some(initial_permit);
643 for release in 1..RELEASE_COUNT {
644 wait_for_waiters(&limiter);
645 drop(released_permit.take());
646 released_permit = Some(
647 permit_rx
648 .recv_timeout(Duration::from_secs(3))
649 .unwrap_or_else(|error| {
650 panic!("release {release} must admit a waiter: {error}")
651 }),
652 );
653 }
654 wait_for_waiters(&limiter);
655 drop(released_permit);
656 let consumed_by_build = permit_rx
657 .recv_timeout(Duration::from_secs(3))
658 .unwrap_or_else(|error| panic!("release {RELEASE_COUNT} must admit a waiter: {error}"));
659
660 cancelled.store(true, Ordering::SeqCst);
661 for waiter in waiters {
662 waiter.join().expect("cancelled waiter joins");
663 }
664
665 let events = limiter.admission_events();
666 assert_eq!(events.len(), RELEASE_COUNT);
667 assert!(events
668 .iter()
669 .any(|event| event.class == ColdBuildAdmissionClass::InspectTriggered));
670 assert!(events
671 .iter()
672 .any(|event| event.class == ColdBuildAdmissionClass::Maintenance));
673 assert!(events.iter().all(|event| matches!(
674 event.request_id.as_str(),
675 "inspect-request" | "maintenance-request"
676 )));
677 assert!(events
678 .iter()
679 .enumerate()
680 .all(|(index, event)| event.admission_order == index as u64 + 1));
681
682 assert_eq!(
683 limiter.available.load(Ordering::Acquire),
684 0,
685 "the final acquired permit must remain accounted for by the consumed build"
686 );
687 drop(consumed_by_build);
688 assert_eq!(
689 limiter.available.load(Ordering::Acquire),
690 1,
691 "releasing the consumed build permit must restore the limiter slot"
692 );
693 }
694}