aft/
cold_build_limiter.rs1use std::sync::atomic::{AtomicUsize, Ordering};
2use std::sync::{Arc, LazyLock};
3use std::time::{Duration, Instant};
4
5#[cfg(not(test))]
6const DEFAULT_COLD_BUILD_LIMIT: usize = 2;
7#[cfg(test)]
8const DEFAULT_COLD_BUILD_LIMIT: usize = 1024;
9
10static GLOBAL_COLD_BUILD_LIMITER: LazyLock<Arc<ColdBuildLimiter>> =
11 LazyLock::new(|| Arc::new(ColdBuildLimiter::new(DEFAULT_COLD_BUILD_LIMIT)));
12
13pub fn try_acquire() -> Option<ColdBuildPermit> {
14 GLOBAL_COLD_BUILD_LIMITER.try_acquire()
15}
16
17pub fn acquire_blocking(kind: &str) -> ColdBuildPermit {
26 acquire_blocking_while(kind, || true).expect("unconditional cold-build admission")
27}
28
29pub fn acquire_blocking_while(kind: &str, admitted: impl Fn() -> bool) -> Option<ColdBuildPermit> {
33 acquire_blocking_while_with_limiter(&GLOBAL_COLD_BUILD_LIMITER, kind, admitted)
34}
35
36fn acquire_blocking_while_with_limiter(
37 limiter: &Arc<ColdBuildLimiter>,
38 kind: &str,
39 admitted: impl Fn() -> bool,
40) -> Option<ColdBuildPermit> {
41 let started = Instant::now();
42 let mut logged = false;
43 loop {
44 if !admitted() {
45 return None;
46 }
47 if let Some(permit) = limiter.try_acquire() {
48 if !admitted() {
52 drop(permit);
53 return None;
54 }
55 if logged {
56 crate::slog_info!(
57 "maintenance build slot acquired after {}ms wait: {}",
58 started.elapsed().as_millis(),
59 kind
60 );
61 }
62 return Some(permit);
63 }
64 if !logged {
65 crate::slog_info!(
66 "maintenance build queued behind concurrency cap ({}): {}",
67 limiter.limit(),
68 kind
69 );
70 logged = true;
71 }
72 std::thread::sleep(Duration::from_millis(100));
73 }
74}
75
76pub fn limit() -> usize {
77 GLOBAL_COLD_BUILD_LIMITER.limit()
78}
79
80#[cfg(test)]
81pub(crate) fn test_limiter(limit: usize) -> Arc<ColdBuildLimiter> {
82 Arc::new(ColdBuildLimiter::new(limit))
83}
84
85#[cfg(test)]
86pub(crate) fn acquire_blocking_while_with_test_limiter(
87 limiter: &Arc<ColdBuildLimiter>,
88 kind: &str,
89 admitted: impl Fn() -> bool,
90) -> Option<ColdBuildPermit> {
91 acquire_blocking_while_with_limiter(limiter, kind, admitted)
92}
93
94#[derive(Debug)]
95pub(crate) struct ColdBuildLimiter {
96 available: AtomicUsize,
97 limit: usize,
98}
99
100impl ColdBuildLimiter {
101 fn new(limit: usize) -> Self {
102 let limit = limit.max(1);
103 Self {
104 available: AtomicUsize::new(limit),
105 limit,
106 }
107 }
108
109 fn limit(&self) -> usize {
110 self.limit
111 }
112
113 fn try_acquire(self: &Arc<Self>) -> Option<ColdBuildPermit> {
114 loop {
115 let available = self.available.load(Ordering::Acquire);
116 if available == 0 {
117 return None;
118 }
119 if self
120 .available
121 .compare_exchange(
122 available,
123 available - 1,
124 Ordering::AcqRel,
125 Ordering::Acquire,
126 )
127 .is_ok()
128 {
129 return Some(ColdBuildPermit {
130 limiter: Arc::clone(self),
131 });
132 }
133 }
134 }
135}
136
137#[derive(Debug)]
138pub struct ColdBuildPermit {
139 limiter: Arc<ColdBuildLimiter>,
140}
141
142impl Drop for ColdBuildPermit {
143 fn drop(&mut self) {
144 let previous = self.limiter.available.fetch_add(1, Ordering::Release);
145 debug_assert!(previous < self.limiter.limit);
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 fn serial() -> std::sync::MutexGuard<'static, ()> {
155 static M: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
156 M.get_or_init(|| std::sync::Mutex::new(()))
157 .lock()
158 .unwrap_or_else(std::sync::PoisonError::into_inner)
159 }
160
161 #[test]
162 fn permits_release_on_drop() {
163 let _serial = serial();
164 let before = GLOBAL_COLD_BUILD_LIMITER.available.load(Ordering::Acquire);
165 {
166 let _a = acquire_blocking("test-a");
167 let _b = acquire_blocking("test-b");
168 assert_eq!(
169 GLOBAL_COLD_BUILD_LIMITER.available.load(Ordering::Acquire),
170 before - 2
171 );
172 }
173 assert_eq!(
174 GLOBAL_COLD_BUILD_LIMITER.available.load(Ordering::Acquire),
175 before
176 );
177 }
178
179 #[test]
180 fn acquire_blocking_waits_until_release() {
181 let _serial = serial();
182 let mut held: Vec<ColdBuildPermit> = Vec::new();
184 while let Some(permit) = try_acquire() {
185 held.push(permit);
186 }
187 let waiter = std::thread::spawn(|| {
188 let _p = acquire_blocking("waiter");
189 });
190 std::thread::sleep(std::time::Duration::from_millis(250));
191 assert!(!waiter.is_finished(), "waiter must block while cap is full");
192 drop(held.pop());
193 waiter.join().expect("waiter finishes after release");
194 drop(held);
195 }
196
197 #[test]
198 fn admission_revoked_between_check_and_permit_drops_the_slot() {
199 let _serial = serial();
200 let before = GLOBAL_COLD_BUILD_LIMITER.available.load(Ordering::Acquire);
201 let checks = AtomicUsize::new(0);
202
203 let permit = acquire_blocking_while("revoked-after-cas", || {
204 checks.fetch_add(1, Ordering::SeqCst) == 0
205 });
206
207 assert!(permit.is_none());
208 assert_eq!(checks.load(Ordering::SeqCst), 2);
209 assert_eq!(
210 GLOBAL_COLD_BUILD_LIMITER.available.load(Ordering::Acquire),
211 before,
212 "revoked admission must return the just-acquired slot"
213 );
214 }
215
216 #[test]
217 fn conditional_waiter_cancels_without_consuming_a_released_slot() {
218 let _serial = serial();
219 let mut held = Vec::new();
220 while let Some(permit) = try_acquire() {
221 held.push(permit);
222 }
223 let admitted = Arc::new(std::sync::atomic::AtomicBool::new(true));
224 let waiter_admitted = Arc::clone(&admitted);
225 let waiter = std::thread::spawn(move || {
226 acquire_blocking_while("conditional waiter", || {
227 waiter_admitted.load(Ordering::SeqCst)
228 })
229 });
230 std::thread::sleep(Duration::from_millis(150));
231 admitted.store(false, Ordering::SeqCst);
232 assert!(
233 waiter.join().expect("conditional waiter joins").is_none(),
234 "revoked work must leave the cold-build queue without taking a permit"
235 );
236 drop(held);
237 }
238}