1use std::future::Future;
13use std::sync::Arc;
14use std::time::Duration;
15
16use futures_util::FutureExt;
17use sentry::{Hub, SentryFutureExt, TransactionContext};
18use tokio::sync::{AcquireError, Notify, OwnedSemaphorePermit, Semaphore};
19
20use crate::error::{Error, ErrorKind, Panic, Result};
21
22const EMITTER_INTERVAL: Duration = Duration::from_secs(1);
24
25#[non_exhaustive]
30#[derive(Clone, Copy, Debug)]
31pub struct Stats {
32 pub in_use: u32,
34 pub queued: u32,
36 pub bulk_in_use: u32,
38}
39
40#[derive(Clone, Debug)]
52pub struct ConcurrencyLimiter {
53 tasks: Arc<Semaphore>,
54 queue: Arc<Semaphore>,
55 bulk: Arc<Semaphore>,
56 tasks_total: u32,
57 queue_total: u32,
58 bulk_total: u32,
59 timeout: Duration,
60 released: Arc<Notify>,
61}
62
63impl ConcurrencyLimiter {
64 pub fn new(max: u32) -> Self {
72 Self {
73 tasks: Arc::new(Semaphore::new(max as usize)),
74 queue: Arc::new(Semaphore::new(0)),
75 bulk: Arc::new(Semaphore::new(max as usize)),
76 tasks_total: max,
77 queue_total: 0,
78 bulk_total: max,
79 timeout: Duration::from_secs(1),
80 released: Arc::new(Notify::new()),
81 }
82 }
83
84 pub fn with_queue(mut self, size: u32) -> Self {
89 self.queue_total = size;
90 self.queue = Arc::new(Semaphore::new(size as usize));
91 self
92 }
93
94 pub fn with_timeout(mut self, timeout: Duration) -> Self {
100 self.timeout = timeout;
101 self
102 }
103
104 pub fn with_bulk(mut self, percent: u32) -> Self {
113 let clamped = percent.min(100);
114 self.bulk_total = (self.tasks_total * clamped).div_ceil(100).max(1);
115 self.bulk = Arc::new(Semaphore::new(self.bulk_total as usize));
116 self
117 }
118
119 pub async fn acquire(&self) -> Result<ConcurrencyPermit> {
126 if self.tasks_total == 0 {
127 return Err(ErrorKind::AtCapacity.into());
128 }
129
130 if let Ok(task_permit) = self.tasks.clone().try_acquire_owned() {
132 return Ok(ConcurrencyPermit {
133 task_permit: Some(task_permit),
134 bulk_permit: None,
135 released: Arc::clone(&self.released),
136 });
137 }
138
139 let _ticket = self
142 .queue
143 .clone()
144 .try_acquire_owned()
145 .map_err(|_| ErrorKind::AtCapacity)?;
146
147 let acquire = self.tasks.clone().acquire_owned();
148 let task_permit = tokio::time::timeout(self.timeout, acquire)
149 .await
150 .map_err(|_| ErrorKind::AtCapacity)?
151 .map_err(|_| ErrorKind::AtCapacity)?;
152
153 Ok(ConcurrencyPermit {
154 task_permit: Some(task_permit),
155 bulk_permit: None,
156 released: Arc::clone(&self.released),
157 })
158 }
159
160 pub fn try_acquire(&self) -> Result<ConcurrencyPermit> {
164 let task_permit = self
165 .tasks
166 .clone()
167 .try_acquire_owned()
168 .map_err(|_| ErrorKind::AtCapacity)?;
169
170 Ok(ConcurrencyPermit {
171 task_permit: Some(task_permit),
172 bulk_permit: None,
173 released: Arc::clone(&self.released),
174 })
175 }
176
177 pub async fn acquire_bulk(&self) -> Result<ConcurrencyPermit> {
187 if self.tasks_total == 0 {
188 return Err(ErrorKind::AtCapacity.into());
189 }
190
191 let bulk_sem = self.bulk.clone();
192 let tasks_sem = self.tasks.clone();
193
194 let acquire = async move {
195 let bulk_permit = bulk_sem.acquire_owned().await?;
196 let task_permit = tasks_sem.acquire_owned().await?;
197 Ok((task_permit, bulk_permit))
198 };
199
200 let (task_permit, bulk_permit) = tokio::time::timeout(self.timeout, acquire)
201 .await
202 .map_err(|_| ErrorKind::AtCapacity)?
203 .map_err(|_: AcquireError| ErrorKind::AtCapacity)?;
204
205 Ok(ConcurrencyPermit {
206 task_permit: Some(task_permit),
207 bulk_permit: Some(bulk_permit),
208 released: Arc::clone(&self.released),
209 })
210 }
211
212 pub fn available_permits(&self) -> u32 {
214 u32::try_from(self.tasks.available_permits()).unwrap_or(self.tasks_total)
215 }
216
217 pub fn used_permits(&self) -> u32 {
219 self.tasks_total - self.available_permits()
220 }
221
222 pub fn total_permits(&self) -> u32 {
224 self.tasks_total
225 }
226
227 pub fn queued_permits(&self) -> u32 {
229 let available = u32::try_from(self.queue.available_permits()).unwrap_or(self.queue_total);
230 self.queue_total - available
231 }
232
233 pub fn total_queue(&self) -> u32 {
235 self.queue_total
236 }
237
238 pub fn used_bulk_permits(&self) -> u32 {
240 let available = u32::try_from(self.bulk.available_permits()).unwrap_or(self.bulk_total);
241 self.bulk_total - available
242 }
243
244 pub fn total_bulk(&self) -> u32 {
246 self.bulk_total
247 }
248
249 #[allow(dead_code)]
251 pub async fn wait_all(&self) {
252 loop {
253 let notified = self.released.notified();
254 if self.used_permits() == 0 {
255 return;
256 }
257 notified.await;
258 }
259 }
260
261 pub fn stats(&self) -> Stats {
263 Stats {
264 in_use: self.used_permits(),
265 queued: self.queued_permits(),
266 bulk_in_use: self.used_bulk_permits(),
267 }
268 }
269
270 pub async fn run_emitter<F, Fut>(&self, mut emit: F)
275 where
276 F: FnMut(Stats) -> Fut,
277 Fut: Future<Output = ()>,
278 {
279 let mut ticker = tokio::time::interval(EMITTER_INTERVAL);
280 loop {
281 ticker.tick().await;
282 emit(self.stats()).await;
283 }
284 }
285}
286
287pub struct ConcurrencyPermit {
295 task_permit: Option<OwnedSemaphorePermit>,
296 bulk_permit: Option<OwnedSemaphorePermit>,
297 released: Arc<Notify>,
298}
299
300impl std::fmt::Debug for ConcurrencyPermit {
301 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302 f.debug_struct("ConcurrencyPermit").finish_non_exhaustive()
303 }
304}
305
306impl Drop for ConcurrencyPermit {
307 fn drop(&mut self) {
308 drop(self.task_permit.take());
309 drop(self.bulk_permit.take());
310 self.released.notify_waiters();
311 }
312}
313
314pub async fn run_metered<T, G, F>(operation: &'static str, guard: G, f: F) -> Result<T>
325where
326 T: Send + 'static,
327 G: Send + 'static,
328 F: Future<Output = Result<T>> + Send + 'static,
329{
330 let receiver = spawn_metered_inner(operation, guard, f);
331 receiver.await.map_err(|_| {
332 let error = Error::new(ErrorKind::Internal, "service task dropped");
333 objectstore_log::error!(!!&error, operation, "Task failed");
334 error
335 })?
336}
337
338pub fn spawn_metered<T, G, F>(operation: &'static str, guard: G, f: F)
343where
344 T: Send + 'static,
345 G: Send + 'static,
346 F: Future<Output = Result<T>> + Send + 'static,
347{
348 drop(spawn_metered_inner(operation, guard, f));
349}
350
351fn spawn_metered_inner<T, G, F>(
352 operation: &'static str,
353 guard: G,
354 f: F,
355) -> tokio::sync::oneshot::Receiver<Result<T>>
356where
357 T: Send + 'static,
358 G: Send + 'static,
359 F: Future<Output = Result<T>> + Send + 'static,
360{
361 objectstore_metrics::count!("service.task.start", operation = operation);
362
363 let hub = Hub::current();
364 let span = hub.configure_scope(|scope| scope.get_span());
365
366 let new_hub = Hub::new_from_top(hub);
367 let transaction = new_hub.start_transaction(TransactionContext::continue_from_span(
368 operation,
369 "tokio.task",
370 span,
371 ));
372
373 let scope_guard = new_hub.push_scope();
374 new_hub.configure_scope(|scope| scope.set_span(Some(transaction.clone().into())));
375
376 let (tx, rx) = tokio::sync::oneshot::channel();
377 tokio::spawn(
378 async move {
379 let start = tokio::time::Instant::now();
380 let result = std::panic::AssertUnwindSafe(f)
381 .catch_unwind()
382 .await
383 .unwrap_or_else(|payload| Err(Panic::new(payload).into()));
384
385 if let Err(ref e) = result {
386 let error = e as &dyn std::error::Error;
387 objectstore_log::event_dyn!(e.level(), error, operation, "Task failed");
388 }
389
390 objectstore_metrics::record!(
391 "service.task.duration" = start.elapsed(),
392 operation = operation,
393 outcome = if result.is_ok() { "success" } else { "error" },
394 );
395
396 let _ = tx.send(result);
397 drop(guard);
398 transaction.finish();
399 drop(scope_guard);
400 }
401 .bind_hub(new_hub),
402 );
403
404 rx
405}
406
407#[cfg(test)]
408mod tests {
409 use std::sync::atomic::{AtomicU32, Ordering};
410
411 use super::*;
412
413 #[test]
414 fn available_permits_tracks_held() {
415 let limiter = ConcurrencyLimiter::new(5);
416 assert_eq!(limiter.available_permits(), 5);
417
418 let p1 = limiter.try_acquire().unwrap();
419 assert_eq!(limiter.available_permits(), 4);
420
421 let p2 = limiter.try_acquire().unwrap();
422 assert_eq!(limiter.available_permits(), 3);
423
424 drop(p1);
425 assert_eq!(limiter.available_permits(), 4);
426
427 drop(p2);
428 assert_eq!(limiter.available_permits(), 5);
429 }
430
431 #[test]
432 fn total_permits_returns_configured_max() {
433 let limiter = ConcurrencyLimiter::new(42);
434 assert_eq!(limiter.total_permits(), 42);
435 }
436
437 #[test]
438 fn acquire_and_release() {
439 let limiter = ConcurrencyLimiter::new(2);
440 assert_eq!(limiter.used_permits(), 0);
441
442 let p1 = limiter.try_acquire().unwrap();
443 assert_eq!(limiter.used_permits(), 1);
444
445 let p2 = limiter.try_acquire().unwrap();
446 assert_eq!(limiter.used_permits(), 2);
447
448 drop(p1);
449 assert_eq!(limiter.used_permits(), 1);
450
451 drop(p2);
452 assert_eq!(limiter.used_permits(), 0);
453 }
454
455 #[test]
456 fn at_capacity_rejects() {
457 let limiter = ConcurrencyLimiter::new(1);
458 let _permit = limiter.try_acquire().unwrap();
459
460 let result = limiter.try_acquire();
461 assert!(result.is_err_and(|error| error.kind() == ErrorKind::AtCapacity));
462 }
463
464 #[test]
465 fn permit_recovery_after_drop() {
466 let limiter = ConcurrencyLimiter::new(1);
467
468 let permit = limiter.try_acquire().unwrap();
469 assert!(limiter.try_acquire().is_err());
470
471 drop(permit);
472 assert!(limiter.try_acquire().is_ok());
473 }
474
475 #[tokio::test(start_paused = true)]
476 async fn emitter_calls_callback() {
477 let limiter = ConcurrencyLimiter::new(5);
478 let _permit = limiter.try_acquire().unwrap();
479
480 let emitted_in_use = Arc::new(AtomicU32::new(0));
481 let emitted_queued = Arc::new(AtomicU32::new(0));
482 let in_use_clone = Arc::clone(&emitted_in_use);
483 let queued_clone = Arc::clone(&emitted_queued);
484
485 let emitter = limiter.run_emitter(move |stats| {
486 let in_use_ref = Arc::clone(&in_use_clone);
487 let queued_ref = Arc::clone(&queued_clone);
488 async move {
489 in_use_ref.store(stats.in_use, Ordering::Relaxed);
490 queued_ref.store(stats.queued, Ordering::Relaxed);
491 }
492 });
493
494 tokio::select! {
495 _ = emitter => unreachable!("emitter runs forever"),
496 _ = tokio::time::sleep(EMITTER_INTERVAL) => {}
497 }
498
499 assert_eq!(emitted_in_use.load(Ordering::Relaxed), 1);
500 assert_eq!(emitted_queued.load(Ordering::Relaxed), 0);
501 }
502
503 #[tokio::test]
504 async fn wait_all_resolves_when_permits_returned() {
505 let limiter = ConcurrencyLimiter::new(2);
506 let p1 = limiter.try_acquire().unwrap();
507 let p2 = limiter.try_acquire().unwrap();
508
509 let mut wait = Box::pin(limiter.wait_all());
510
511 drop(p1);
513 assert!(futures::poll!(&mut wait).is_pending());
514
515 drop(p2);
517 assert!(futures::poll!(&mut wait).is_ready());
518 }
519
520 #[tokio::test]
521 async fn wait_all_returns_immediately_when_empty() {
522 let limiter = ConcurrencyLimiter::new(5);
523 let wait = Box::pin(limiter.wait_all());
524 assert!(futures::poll!(wait).is_ready());
525 }
526
527 #[tokio::test(start_paused = true)]
530 async fn queue_zero_rejects_immediately() {
531 let limiter = ConcurrencyLimiter::new(2);
532 assert_eq!(limiter.total_queue(), 0);
533
534 let p1 = limiter.try_acquire().unwrap();
535 let p2 = limiter.try_acquire().unwrap();
536 assert!(
537 limiter
538 .try_acquire()
539 .is_err_and(|error| error.kind() == ErrorKind::AtCapacity)
540 );
541
542 drop(p1);
543 assert!(limiter.try_acquire().is_ok());
544 drop(p2);
545
546 let mut bulk_permits = Vec::new();
548 for _ in 0..2 {
549 let permit = limiter.acquire_bulk().await.unwrap();
550 bulk_permits.push(permit);
551 }
552
553 let start = tokio::time::Instant::now();
554 let result = limiter.acquire().await;
555 assert!(result.is_err_and(|error| error.kind() == ErrorKind::AtCapacity));
556 assert_eq!(start.elapsed(), Duration::ZERO);
557 drop(bulk_permits);
558 }
559
560 #[tokio::test(start_paused = true)]
561 async fn acquire_succeeds_immediately_when_available() {
562 let limiter = ConcurrencyLimiter::new(2).with_queue(3);
563
564 let permit = limiter.acquire().await.unwrap();
565 assert_eq!(limiter.used_permits(), 1);
566 assert_eq!(limiter.queued_permits(), 0);
567 drop(permit);
568 }
569
570 #[tokio::test(start_paused = true)]
571 async fn acquire_waits_and_succeeds_after_release() {
572 let limiter = ConcurrencyLimiter::new(1).with_queue(2);
573
574 let held = limiter.acquire().await.unwrap();
575 assert_eq!(limiter.used_permits(), 1);
576
577 let limiter2 = limiter.clone();
578 let waiter = tokio::spawn(async move { limiter2.acquire().await });
579
580 tokio::task::yield_now().await;
581 assert_eq!(limiter.queued_permits(), 1);
582
583 drop(held);
584
585 let permit = waiter.await.unwrap().unwrap();
586 assert_eq!(limiter.used_permits(), 1);
587 assert_eq!(limiter.queued_permits(), 0);
588 drop(permit);
589 }
590
591 #[tokio::test(start_paused = true)]
592 async fn acquire_times_out() {
593 let limiter = ConcurrencyLimiter::new(1).with_queue(2);
594
595 let _held = limiter.acquire().await.unwrap();
596
597 let limiter2 = limiter.clone();
598 let waiter = tokio::spawn(async move { limiter2.acquire().await });
599
600 tokio::time::sleep(Duration::from_secs(2)).await;
601
602 let result = waiter.await.unwrap();
603 assert!(result.is_err_and(|error| error.kind() == ErrorKind::AtCapacity));
604 assert_eq!(limiter.queued_permits(), 0);
605 }
606
607 #[tokio::test(start_paused = true)]
608 async fn acquire_rejects_over_max_plus_queue() {
609 let limiter = ConcurrencyLimiter::new(1).with_queue(1);
610
611 let _held = limiter.acquire().await.unwrap();
612
613 let limiter2 = limiter.clone();
614 let _waiter = tokio::spawn(async move { limiter2.acquire().await });
615 tokio::task::yield_now().await;
616
617 assert_eq!(limiter.queued_permits(), 1);
618 let result = limiter.acquire().await;
619 assert!(result.is_err_and(|error| error.kind() == ErrorKind::AtCapacity));
620 }
621
622 #[tokio::test(start_paused = true)]
623 async fn dropping_parked_acquire_releases_queue_slot() {
624 let limiter = ConcurrencyLimiter::new(1).with_queue(1);
625
626 let _held = limiter.acquire().await.unwrap();
627
628 let limiter2 = limiter.clone();
629 let waiter = tokio::spawn(async move { limiter2.acquire().await });
630 tokio::task::yield_now().await;
631 assert_eq!(limiter.queued_permits(), 1);
632
633 waiter.abort();
634 let _ = waiter.await;
635 tokio::task::yield_now().await;
636
637 assert_eq!(limiter.queued_permits(), 0);
638
639 let limiter3 = limiter.clone();
640 let replacement = tokio::spawn(async move { limiter3.acquire().await });
641 tokio::task::yield_now().await;
642 assert_eq!(limiter.queued_permits(), 1);
643 drop(replacement);
644 }
645
646 #[tokio::test(start_paused = true)]
647 async fn queued_permits_reflects_state() {
648 let limiter = ConcurrencyLimiter::new(2).with_queue(3);
649
650 assert_eq!(limiter.queued_permits(), 0);
651 let _p1 = limiter.try_acquire().unwrap();
652 assert_eq!(limiter.queued_permits(), 0);
653 let _p2 = limiter.try_acquire().unwrap();
654 assert_eq!(limiter.queued_permits(), 0);
655 drop(_p1);
656 drop(_p2);
657
658 let _bulk = limiter.acquire_bulk().await.unwrap();
660 let _bulk2 = limiter.acquire_bulk().await.unwrap();
661 assert_eq!(limiter.queued_permits(), 0);
662
663 let limiter2 = limiter.clone();
664 let _waiter = tokio::spawn(async move { limiter2.acquire().await });
665 tokio::task::yield_now().await;
666 assert_eq!(limiter.queued_permits(), 1);
667 }
668
669 #[tokio::test(start_paused = true)]
670 async fn acquire_rejects_immediately_when_max_is_zero() {
671 let limiter = ConcurrencyLimiter::new(0).with_queue(5);
672
673 let start = tokio::time::Instant::now();
674 let result = limiter.acquire().await;
675 assert!(result.is_err_and(|error| error.kind() == ErrorKind::AtCapacity));
676 assert_eq!(start.elapsed(), Duration::ZERO);
677 }
678
679 #[tokio::test(start_paused = true)]
680 async fn emitter_reports_queued_count() {
681 let limiter = ConcurrencyLimiter::new(1).with_queue(2);
682 let _held = limiter.acquire().await.unwrap();
683
684 let limiter2 = limiter.clone();
685 let _waiter = tokio::spawn(async move { limiter2.acquire().await });
686 tokio::task::yield_now().await;
687
688 let emitted_in_use = Arc::new(AtomicU32::new(0));
689 let emitted_queued = Arc::new(AtomicU32::new(0));
690 let in_use_clone = Arc::clone(&emitted_in_use);
691 let queued_clone = Arc::clone(&emitted_queued);
692
693 let emitter = limiter.run_emitter(move |stats| {
694 let in_use_ref = Arc::clone(&in_use_clone);
695 let queued_ref = Arc::clone(&queued_clone);
696 async move {
697 in_use_ref.store(stats.in_use, Ordering::Relaxed);
698 queued_ref.store(stats.queued, Ordering::Relaxed);
699 }
700 });
701
702 tokio::select! {
703 _ = emitter => unreachable!("emitter runs forever"),
704 _ = tokio::time::sleep(EMITTER_INTERVAL) => {}
705 }
706
707 assert_eq!(emitted_in_use.load(Ordering::Relaxed), 1);
708 assert_eq!(emitted_queued.load(Ordering::Relaxed), 1);
709 }
710
711 #[test]
714 fn bulk_defaults_to_full_capacity() {
715 let limiter = ConcurrencyLimiter::new(100);
716 assert_eq!(limiter.total_bulk(), 100);
717 }
718
719 #[test]
720 fn bulk_percent_computes_correctly() {
721 let limiter = ConcurrencyLimiter::new(100).with_bulk(60);
722 assert_eq!(limiter.total_bulk(), 60);
723
724 let limiter = ConcurrencyLimiter::new(10).with_bulk(90);
725 assert_eq!(limiter.total_bulk(), 9);
726
727 let limiter = ConcurrencyLimiter::new(100).with_bulk(150);
728 assert_eq!(limiter.total_bulk(), 100);
729
730 let limiter = ConcurrencyLimiter::new(1).with_bulk(60);
732 assert_eq!(limiter.total_bulk(), 1);
733 }
734
735 #[tokio::test(start_paused = true)]
736 async fn bulk_caps_at_budget() {
737 let limiter = ConcurrencyLimiter::new(10).with_queue(5).with_bulk(90);
738 let bulk_budget = limiter.total_bulk();
739 assert_eq!(bulk_budget, 9);
740
741 let mut permits = Vec::new();
742 for _ in 0..bulk_budget {
743 let permit = limiter.acquire_bulk().await.unwrap();
744 permits.push(permit);
745 }
746
747 assert_eq!(limiter.used_bulk_permits(), bulk_budget);
748 assert_eq!(limiter.used_permits(), bulk_budget);
749
750 let normal = limiter.acquire().await.unwrap();
752 assert_eq!(limiter.used_permits(), 10);
753 drop(normal);
754 drop(permits);
755 }
756
757 #[tokio::test(start_paused = true)]
758 async fn normal_uses_all_permits_when_bulk_idle() {
759 let limiter = ConcurrencyLimiter::new(10).with_queue(5);
760
761 let mut permits = Vec::new();
762 for _ in 0..10 {
763 let permit = limiter.acquire().await.unwrap();
764 permits.push(permit);
765 }
766
767 assert_eq!(limiter.used_permits(), 10);
768 assert_eq!(limiter.used_bulk_permits(), 0);
769 drop(permits);
770 }
771
772 #[tokio::test(start_paused = true)]
773 async fn bulk_waits_for_inner_permit() {
774 let limiter = ConcurrencyLimiter::new(1).with_queue(2);
775
776 let held = limiter.acquire().await.unwrap();
777 assert_eq!(limiter.used_permits(), 1);
778
779 let limiter2 = limiter.clone();
780 let waiter = tokio::spawn(async move { limiter2.acquire_bulk().await });
781 tokio::task::yield_now().await;
782
783 drop(held);
784
785 let permit = waiter.await.unwrap().unwrap();
786 assert_eq!(limiter.used_permits(), 1);
787 assert_eq!(limiter.used_bulk_permits(), 1);
788 drop(permit);
789 }
790
791 #[tokio::test(start_paused = true)]
792 async fn bulk_timeout_spans_both_waits() {
793 let limiter = ConcurrencyLimiter::new(1);
794
795 let _held = limiter.acquire().await.unwrap();
796
797 let limiter2 = limiter.clone();
798 let waiter = tokio::spawn(async move { limiter2.acquire_bulk().await });
799
800 tokio::time::sleep(Duration::from_secs(2)).await;
801
802 let result = waiter.await.unwrap();
803 assert!(result.is_err_and(|error| error.kind() == ErrorKind::AtCapacity));
804 assert_eq!(limiter.used_bulk_permits(), 0);
805 }
806
807 #[tokio::test(start_paused = true)]
808 async fn bulk_cancellation_leaks_nothing() {
809 let limiter = ConcurrencyLimiter::new(2).with_queue(2);
810
811 let _held1 = limiter.acquire().await.unwrap();
812 let _held2 = limiter.acquire().await.unwrap();
813
814 let limiter2 = limiter.clone();
815 let waiter = tokio::spawn(async move { limiter2.acquire_bulk().await });
816 tokio::task::yield_now().await;
817
818 waiter.abort();
819 let _ = waiter.await;
820 tokio::task::yield_now().await;
821
822 assert_eq!(limiter.used_bulk_permits(), 0);
823 assert_eq!(limiter.used_permits(), 2);
824 }
825
826 #[tokio::test(start_paused = true)]
827 async fn bulk_rejects_immediately_when_max_is_zero() {
828 let limiter = ConcurrencyLimiter::new(0).with_queue(5);
829
830 let start = tokio::time::Instant::now();
831 let result = limiter.acquire_bulk().await;
832 assert!(result.is_err_and(|error| error.kind() == ErrorKind::AtCapacity));
833 assert_eq!(start.elapsed(), Duration::ZERO);
834 }
835
836 #[tokio::test(start_paused = true)]
837 async fn bulk_with_zero_percent_allows_one() {
838 let limiter = ConcurrencyLimiter::new(10).with_bulk(0);
839
840 assert_eq!(limiter.total_bulk(), 1);
841
842 let permit = limiter.acquire_bulk().await.unwrap();
843 assert_eq!(limiter.used_bulk_permits(), 1);
844
845 let limiter2 = limiter.clone();
846 let waiter = tokio::spawn(async move { limiter2.acquire_bulk().await });
847 tokio::time::sleep(Duration::from_secs(2)).await;
848
849 let result = waiter.await.unwrap();
850 assert!(result.is_err_and(|error| error.kind() == ErrorKind::AtCapacity));
851 drop(permit);
852 }
853
854 #[tokio::test(start_paused = true)]
855 async fn queue_bounded_under_bulk_load() {
856 let limiter = ConcurrencyLimiter::new(3).with_queue(2);
857
858 let mut bulk_permits = Vec::new();
859 for _ in 0..3 {
860 let permit = limiter.acquire_bulk().await.unwrap();
861 bulk_permits.push(permit);
862 }
863
864 let limiter2 = limiter.clone();
865 let _w1 = tokio::spawn(async move { limiter2.acquire().await });
866 tokio::task::yield_now().await;
867 assert_eq!(limiter.queued_permits(), 1);
868
869 let limiter3 = limiter.clone();
870 let _w2 = tokio::spawn(async move { limiter3.acquire().await });
871 tokio::task::yield_now().await;
872 assert_eq!(limiter.queued_permits(), 2);
873
874 let start = tokio::time::Instant::now();
876 let result = limiter.acquire().await;
877 assert!(result.is_err_and(|error| error.kind() == ErrorKind::AtCapacity));
878 assert_eq!(start.elapsed(), Duration::ZERO);
879
880 drop(bulk_permits);
881 }
882}