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