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