1use crate::error::{AgError, AgResult};
16use std::future::Future;
17use std::sync::{Arc, OnceLock};
18use std::time::Duration;
19use tokio::sync::{watch, OwnedSemaphorePermit, Semaphore};
20use tokio::time::Instant;
21
22pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
24pub const DEFAULT_MAX_IN_FLIGHT_REQUESTS: usize = 64;
26
27tokio::task_local! {
28 static CURRENT_REQUEST: RequestContext;
31}
32
33#[derive(Clone, Debug)]
39pub struct RequestLimiter(Arc<Semaphore>);
40
41impl RequestLimiter {
42 pub fn new(max_in_flight: usize) -> Self {
45 Self(Arc::new(Semaphore::new(
46 max_in_flight.clamp(1, Semaphore::MAX_PERMITS),
47 )))
48 }
49
50 pub fn available_permits(&self) -> usize {
52 self.0.available_permits()
53 }
54
55 pub(crate) async fn run<T, F, Fut>(
56 &self,
57 timeout: Duration,
58 options: &RequestOptions,
59 work: F,
60 ) -> AgResult<T>
61 where
62 F: FnOnce(RequestContext) -> Fut,
63 Fut: Future<Output = AgResult<T>>,
64 {
65 if options.cancellation.is_cancelled() {
66 return Err(AgError::Cancelled);
67 }
68 let permit = self
69 .0
70 .clone()
71 .try_acquire_owned()
72 .map_err(|_| AgError::Overloaded)?;
73 let deadline = Instant::now()
74 .checked_add(options.timeout.unwrap_or(timeout))
75 .ok_or_else(|| AgError::query("timeout", "duration exceeds clock range"))?;
76 let context = RequestContext {
77 _admission: Arc::new(permit),
78 cancellation: RequestCancellation::default(),
79 deadline,
80 work: Vec::new(),
81 work_admission: None,
82 };
83 let _guard = CancelOnDrop(context.cancellation.clone());
84 context.check()?;
85 tokio::select! {
86 biased;
87 _ = options.cancellation.cancelled() => Err(AgError::Cancelled),
88 result = tokio::time::timeout_at(
89 deadline,
90 CURRENT_REQUEST.scope(context.clone(), work(context.clone())),
91 ) => {
92 result.map_err(|_| AgError::DeadlineExceeded)?
93 }
94 }
95 }
96}
97
98impl Default for RequestLimiter {
99 fn default() -> Self {
100 static SHARED: OnceLock<RequestLimiter> = OnceLock::new();
101 SHARED
102 .get_or_init(|| Self::new(DEFAULT_MAX_IN_FLIGHT_REQUESTS))
103 .clone()
104 }
105}
106
107#[derive(Clone, Debug)]
113pub struct RequestCancellation(watch::Sender<bool>);
114
115impl Default for RequestCancellation {
116 fn default() -> Self {
117 Self(watch::channel(false).0)
118 }
119}
120
121impl RequestCancellation {
122 pub fn cancel(&self) {
124 self.0.send_replace(true);
125 }
126 pub fn is_cancelled(&self) -> bool {
128 *self.0.borrow()
129 }
130 async fn cancelled(&self) {
131 let mut receiver = self.0.subscribe();
132 let _ = receiver.wait_for(|cancelled| *cancelled).await;
133 }
134}
135
136#[derive(Clone, Debug, Default)]
138pub struct RequestOptions {
139 pub timeout: Option<Duration>,
143 pub cancellation: RequestCancellation,
146}
147
148#[derive(Clone)]
149pub(crate) struct RequestContext {
150 _admission: Arc<OwnedSemaphorePermit>,
151 cancellation: RequestCancellation,
152 deadline: Instant,
153 work: Vec<crate::resource::WorkReservation>,
154 work_admission: Option<Arc<crate::resource::WorkAdmission>>,
155}
156
157impl RequestContext {
158 pub(crate) fn current() -> Option<Self> {
159 CURRENT_REQUEST.try_with(Clone::clone).ok()
160 }
161
162 pub(crate) fn run_blocking<T>(
166 request: Option<Self>,
167 work: impl FnOnce() -> AgResult<T>,
168 ) -> AgResult<T> {
169 if let Some(request) = &request {
170 request.check()?;
171 }
172 let result = work();
173 if let Some(request) = &request {
174 request.check()?;
175 }
176 result
177 }
178
179 pub(crate) fn with_work_admission(&self, admission: crate::resource::WorkAdmission) -> Self {
180 let mut next = self.clone();
181 next.work_admission = Some(Arc::new(admission));
182 next
183 }
184
185 pub(crate) async fn scope<F: Future>(&self, work: F) -> F::Output {
187 CURRENT_REQUEST.scope(self.clone(), work).await
188 }
189
190 pub(crate) fn with_work(&self, reservation: crate::resource::WorkReservation) -> Self {
191 let mut next = self.clone();
192 next.work.push(reservation);
193 next
194 }
195
196 pub(crate) fn check(&self) -> AgResult<()> {
197 if self.cancellation.is_cancelled() {
198 return Err(AgError::Cancelled);
199 }
200 if Instant::now() >= self.deadline {
201 return Err(AgError::DeadlineExceeded);
202 }
203 Ok(())
204 }
205}
206
207struct CancelOnDrop(RequestCancellation);
208impl Drop for CancelOnDrop {
209 fn drop(&mut self) {
210 self.0.cancel();
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use std::time::Duration;
218
219 #[tokio::test]
220 async fn cancelled_queued_job_does_not_run_its_work() {
221 let limiter = RequestLimiter::new(1);
222 let worker_limiter = limiter.clone();
223 let (captured_tx, captured_rx) = tokio::sync::oneshot::channel();
224 let task = tokio::spawn(async move {
225 worker_limiter
226 .run(
227 Duration::from_secs(30),
228 &RequestOptions::default(),
229 |_| async {
230 captured_tx.send(RequestContext::current()).ok().unwrap();
231 std::future::pending::<AgResult<()>>().await
232 },
233 )
234 .await
235 });
236 let request = captured_rx.await.unwrap();
237 assert!(request.is_some());
238 task.abort();
239 let _ = task.await;
240 assert_eq!(limiter.available_permits(), 0);
241 let result: AgResult<()> = RequestContext::run_blocking(request, || {
242 panic!("work queued by a cancelled request must not start")
243 });
244 assert!(matches!(result, Err(AgError::Cancelled)));
245 assert_eq!(limiter.available_permits(), 1);
246 }
247
248 #[tokio::test(start_paused = true)]
249 async fn expired_decode_owner_does_not_fail_a_healthy_waiter() {
250 use crate::{
251 io::{CogReader, ReaderConfig},
252 CpuLimiter,
253 };
254 use object_store::{memory::InMemory, path::Path, ObjectStoreExt};
255
256 let store = Arc::new(InMemory::new());
257 store
258 .put(
259 &Path::from("fixture"),
260 bytes::Bytes::from_static(include_bytes!("../tests/fixtures/rgb_nodata.tif"))
261 .into(),
262 )
263 .await
264 .unwrap();
265 let cpu = CpuLimiter::new(1);
266 let config = ReaderConfig {
267 cpu_limiter: Some(cpu.clone()),
268 ..ReaderConfig::default()
269 };
270 let cache = config.block_cache.clone();
271 let reader = Arc::new(
272 CogReader::builder()
273 .reader_config(config)
274 .open_object_store(store, "fixture")
275 .await
276 .unwrap(),
277 );
278 let limiter = RequestLimiter::new(1);
279 let request = RequestContext {
280 _admission: Arc::new(limiter.0.clone().try_acquire_owned().unwrap()),
281 cancellation: RequestCancellation::default(),
282 deadline: Instant::now() + Duration::from_secs(1),
283 work: Vec::new(),
284 work_admission: None,
285 };
286 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
287 let (release_tx, release_rx) = std::sync::mpsc::channel();
288 let permit = cpu.acquire().await.unwrap();
292 let blocker = tokio::task::spawn_blocking(move || {
293 let _permit = permit;
294 started_tx.send(()).unwrap();
295 let _ = release_rx.recv_timeout(Duration::from_secs(5));
296 });
297 started_rx.await.unwrap();
298 let owner_reader = reader.clone();
299 let owner = tokio::spawn(CURRENT_REQUEST.scope(request, async move {
300 owner_reader.read_window(0, 0, 0, 1, 1).await
301 }));
302 while cache.stats().fetched == 0 {
303 tokio::task::yield_now().await;
304 }
305 let waiter = tokio::spawn(async move { reader.read_window(0, 0, 0, 1, 1).await });
306 while cache.stats().misses < 2 {
307 tokio::task::yield_now().await;
308 }
309 tokio::time::advance(Duration::from_secs(2)).await;
310 release_tx.send(()).unwrap();
311 blocker.await.unwrap();
312 assert!(matches!(
313 owner.await.unwrap(),
314 Err(AgError::DeadlineExceeded)
315 ));
316 let image = waiter
317 .await
318 .unwrap()
319 .expect("healthy waiter must retry the expired owner's claim");
320 assert_eq!(image.pixels.to_f64()[[0, 0, 0]], 1.0);
321 assert_eq!(limiter.available_permits(), 1);
322 }
323
324 #[tokio::test]
325 async fn cancelled_http_initialization_retains_request_admission() {
326 let limiter = RequestLimiter::new(1);
327 let worker_limiter = limiter.clone();
328 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
329 let (release_tx, release_rx) = std::sync::mpsc::channel();
330 let task = tokio::spawn(async move {
331 worker_limiter
332 .run(Duration::from_secs(30), &RequestOptions::default(), |_| {
333 crate::io::cog::build_http_transport(move || {
334 started_tx.send(()).unwrap();
335 let _ = release_rx.recv_timeout(Duration::from_secs(5));
336 Ok(())
337 })
338 })
339 .await
340 });
341 started_rx.await.unwrap();
342 task.abort();
343 let _ = task.await;
344 let available_while_running = limiter.available_permits();
345 release_tx.send(()).unwrap();
346 tokio::time::timeout(Duration::from_secs(2), async {
347 while limiter.available_permits() != 1 {
348 tokio::task::yield_now().await;
349 }
350 })
351 .await
352 .unwrap();
353 assert_eq!(available_while_running, 0);
354 }
355
356 #[tokio::test(start_paused = true)]
357 async fn stalled_read_is_bounded_by_request_deadline() {
358 let limiter = RequestLimiter::new(1);
359 let result: AgResult<()> = limiter
360 .run(Duration::from_secs(30), &RequestOptions::default(), |_| {
361 std::future::pending()
362 })
363 .await;
364 assert!(matches!(result, Err(AgError::DeadlineExceeded)));
365 assert_eq!(limiter.available_permits(), 1);
366 }
367
368 #[tokio::test]
369 async fn admission_rejects_excess_without_starting_work() {
370 let limiter = RequestLimiter::new(1);
371 let (entered_tx, entered_rx) = tokio::sync::oneshot::channel();
372 let first_limiter = limiter.clone();
373 let first = tokio::spawn(async move {
374 first_limiter
375 .run(
376 Duration::from_secs(30),
377 &RequestOptions::default(),
378 |_| async {
379 entered_tx.send(()).unwrap();
380 std::future::pending::<AgResult<()>>().await
381 },
382 )
383 .await
384 });
385 entered_rx.await.unwrap();
386 let result: AgResult<()> = limiter
387 .run(
388 Duration::from_secs(30),
389 &RequestOptions::default(),
390 |_| async { panic!("overloaded request must not start") },
391 )
392 .await;
393 assert!(matches!(result, Err(AgError::Overloaded)));
394 first.abort();
395 let _ = first.await;
396 assert_eq!(limiter.available_permits(), 1);
397 }
398
399 #[tokio::test]
400 async fn cancellation_keeps_admission_until_blocking_job_stops() {
401 let limiter = RequestLimiter::new(1);
402 let first_limiter = limiter.clone();
403 let budget = crate::resource::WorkBudget::new(100);
404 let task_budget = budget.clone();
405 let (entered_tx, entered_rx) = tokio::sync::oneshot::channel();
406 let (release_tx, release_rx) = std::sync::mpsc::channel();
407 let first = tokio::spawn(async move {
408 first_limiter
409 .run(
410 Duration::from_secs(30),
411 &RequestOptions::default(),
412 |context| async move {
413 let context = context.with_work(task_budget.try_reserve(70).unwrap());
414 tokio::task::spawn_blocking(move || {
415 entered_tx.send(()).unwrap();
416 release_rx.recv().unwrap();
417 assert!(matches!(context.check(), Err(AgError::Cancelled)));
418 })
419 .await
420 .unwrap();
421 Ok(())
422 },
423 )
424 .await
425 });
426 entered_rx.await.unwrap();
427 first.abort();
428 let _ = first.await;
429 assert_eq!(limiter.available_permits(), 0);
430 assert_eq!(budget.used_bytes(), 70);
431 release_tx.send(()).unwrap();
432 tokio::time::timeout(Duration::from_secs(2), async {
433 while limiter.available_permits() != 1 || budget.used_bytes() != 0 {
434 tokio::task::yield_now().await;
435 }
436 })
437 .await
438 .expect("blocking admission and bytes must be released");
439 }
440
441 #[tokio::test]
442 async fn explicit_cancellation_stops_stalled_request() {
443 let limiter = RequestLimiter::new(1);
444 let options = RequestOptions::default();
445 options.cancellation.cancel();
446 let result: AgResult<()> = limiter
447 .run(Duration::from_secs(30), &options, |_| {
448 std::future::pending()
449 })
450 .await;
451 assert!(matches!(result, Err(AgError::Cancelled)));
452 }
453}