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