1use std::sync::Arc;
9use std::sync::atomic::{AtomicUsize, Ordering};
10
11use tokio::sync::{OwnedSemaphorePermit, Semaphore};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct PostProcLimits {
16 pub pipelines: usize,
18 pub repair: usize,
20 pub extract: usize,
22}
23
24impl Default for PostProcLimits {
25 fn default() -> Self {
26 Self {
27 pipelines: 2,
28 repair: 1,
29 extract: 1,
30 }
31 }
32}
33
34impl PostProcLimits {
35 fn normalized(self) -> Self {
36 Self {
37 pipelines: self.pipelines.max(1),
38 repair: self.repair.max(1),
39 extract: self.extract.max(1),
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
46pub struct PostProcResourceSnapshot {
47 pub active_pipelines: usize,
48 pub active_repairs: usize,
49 pub active_extractions: usize,
50 pub peak_pipelines: usize,
51 pub peak_repairs: usize,
52 pub peak_extractions: usize,
53}
54
55#[derive(Debug, Default)]
56struct Usage {
57 active: AtomicUsize,
58 peak: AtomicUsize,
59}
60
61impl Usage {
62 fn acquired(&self) {
63 let active = self.active.fetch_add(1, Ordering::AcqRel) + 1;
64 self.peak.fetch_max(active, Ordering::Relaxed);
65 }
66
67 fn released(&self) {
68 self.active.fetch_sub(1, Ordering::AcqRel);
69 }
70}
71
72#[derive(Debug)]
74pub struct PostProcResourcePool {
75 limits: PostProcLimits,
76 pipelines: Arc<Semaphore>,
77 repairs: Arc<Semaphore>,
78 extractions: Arc<Semaphore>,
79 pipeline_usage: Arc<Usage>,
80 repair_usage: Arc<Usage>,
81 extraction_usage: Arc<Usage>,
82}
83
84impl PostProcResourcePool {
85 pub fn new(limits: PostProcLimits) -> Arc<Self> {
86 let limits = limits.normalized();
87 Arc::new(Self {
88 limits,
89 pipelines: Arc::new(Semaphore::new(limits.pipelines)),
90 repairs: Arc::new(Semaphore::new(limits.repair)),
91 extractions: Arc::new(Semaphore::new(limits.extract)),
92 pipeline_usage: Arc::new(Usage::default()),
93 repair_usage: Arc::new(Usage::default()),
94 extraction_usage: Arc::new(Usage::default()),
95 })
96 }
97
98 pub fn limits(&self) -> PostProcLimits {
99 self.limits
100 }
101
102 pub async fn acquire_pipeline(self: &Arc<Self>) -> PostProcPermit {
103 Self::acquire(&self.pipelines, &self.pipeline_usage).await
104 }
105
106 pub(crate) async fn acquire_repair(self: &Arc<Self>) -> PostProcPermit {
107 Self::acquire(&self.repairs, &self.repair_usage).await
108 }
109
110 pub(crate) async fn acquire_extract(self: &Arc<Self>) -> PostProcPermit {
111 Self::acquire(&self.extractions, &self.extraction_usage).await
112 }
113
114 async fn acquire(semaphore: &Arc<Semaphore>, usage: &Arc<Usage>) -> PostProcPermit {
115 let permit = Arc::clone(semaphore)
116 .acquire_owned()
117 .await
118 .expect("post-processing resource semaphore must remain open");
119 usage.acquired();
120 PostProcPermit {
121 _permit: permit,
122 usage: Arc::clone(usage),
123 }
124 }
125
126 pub fn snapshot(&self) -> PostProcResourceSnapshot {
127 PostProcResourceSnapshot {
128 active_pipelines: self.pipeline_usage.active.load(Ordering::Relaxed),
129 active_repairs: self.repair_usage.active.load(Ordering::Relaxed),
130 active_extractions: self.extraction_usage.active.load(Ordering::Relaxed),
131 peak_pipelines: self.pipeline_usage.peak.load(Ordering::Relaxed),
132 peak_repairs: self.repair_usage.peak.load(Ordering::Relaxed),
133 peak_extractions: self.extraction_usage.peak.load(Ordering::Relaxed),
134 }
135 }
136}
137
138#[derive(Debug)]
140pub struct PostProcPermit {
141 _permit: OwnedSemaphorePermit,
142 usage: Arc<Usage>,
143}
144
145impl Drop for PostProcPermit {
146 fn drop(&mut self) {
147 self.usage.released();
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use std::time::Duration;
154
155 use super::*;
156
157 #[tokio::test]
158 async fn different_stages_can_overlap_but_same_stage_respects_its_limit() {
159 let pool = PostProcResourcePool::new(PostProcLimits {
160 pipelines: 2,
161 repair: 1,
162 extract: 1,
163 });
164 let _pipeline_a = pool.acquire_pipeline().await;
165 let _pipeline_b = pool.acquire_pipeline().await;
166 let repair_a = pool.acquire_repair().await;
167 let _extract_b = pool.acquire_extract().await;
168
169 let overlap = pool.snapshot();
170 assert_eq!(overlap.active_pipelines, 2);
171 assert_eq!(overlap.active_repairs, 1);
172 assert_eq!(overlap.active_extractions, 1);
173
174 let waiting_pool = Arc::clone(&pool);
175 let mut waiting_repair = tokio::spawn(async move { waiting_pool.acquire_repair().await });
176 assert!(
177 tokio::time::timeout(Duration::from_millis(25), &mut waiting_repair)
178 .await
179 .is_err(),
180 "a second repair must wait for the configured repair worker"
181 );
182
183 drop(repair_a);
184 let repair_b = tokio::time::timeout(Duration::from_secs(1), waiting_repair)
185 .await
186 .expect("waiting repair should be admitted")
187 .expect("waiting repair task should not panic");
188 drop(repair_b);
189
190 let snapshot = pool.snapshot();
191 assert_eq!(snapshot.peak_pipelines, 2);
192 assert_eq!(snapshot.peak_repairs, 1);
193 assert_eq!(snapshot.peak_extractions, 1);
194 }
195
196 #[tokio::test]
197 async fn zero_limits_are_safely_normalized_to_one_worker() {
198 let pool = PostProcResourcePool::new(PostProcLimits {
199 pipelines: 0,
200 repair: 0,
201 extract: 0,
202 });
203 assert_eq!(
204 pool.limits(),
205 PostProcLimits {
206 pipelines: 1,
207 repair: 1,
208 extract: 1,
209 }
210 );
211 }
212
213 #[tokio::test]
214 async fn stress_many_jobs_never_exceeds_stage_worker_limits() {
215 let pool = PostProcResourcePool::new(PostProcLimits {
216 pipelines: 3,
217 repair: 2,
218 extract: 1,
219 });
220 let mut jobs = Vec::new();
221 for index in 0..24 {
222 let resources = Arc::clone(&pool);
223 jobs.push(tokio::spawn(async move {
224 let _pipeline = resources.acquire_pipeline().await;
225 if index % 2 == 0 {
226 let _repair = resources.acquire_repair().await;
227 tokio::task::yield_now().await;
228 }
229 let _extract = resources.acquire_extract().await;
230 tokio::task::yield_now().await;
231 }));
232 }
233 for job in jobs {
234 job.await.expect("stress job should not panic");
235 }
236
237 let snapshot = pool.snapshot();
238 assert!(snapshot.peak_pipelines <= 3, "{snapshot:?}");
239 assert!(snapshot.peak_repairs <= 2, "{snapshot:?}");
240 assert!(snapshot.peak_extractions <= 1, "{snapshot:?}");
241 assert_eq!(snapshot.active_pipelines, 0);
242 assert_eq!(snapshot.active_repairs, 0);
243 assert_eq!(snapshot.active_extractions, 0);
244 }
245}