Skip to main content

objectstore_service/
concurrency.rs

1//! Concurrency limiter for backend operations.
2//!
3//! [`ConcurrencyLimiter`] caps the number of in-flight backend operations
4//! using a tokio semaphore. Each acquired [`ConcurrencyPermit`] notifies
5//! waiters on drop, allowing [`ConcurrencyLimiter::wait_all`] to resolve once
6//! all permits have been returned.
7//!
8//! [`run_metered`] runs an isolated task and waits for its result, while
9//! [`spawn_metered`] starts the same task without waiting. Both provide panic
10//! recovery and `service.task.*` metric emission.
11
12use 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
22/// Interval for the periodic metrics emitter.
23const EMITTER_INTERVAL: Duration = Duration::from_secs(1);
24
25/// Snapshot of concurrency limiter state.
26///
27/// Passed to the callback registered via
28/// [`ConcurrencyLimiter::run_emitter`].
29#[non_exhaustive]
30#[derive(Clone, Copy, Debug)]
31pub struct Stats {
32    /// Number of execution permits currently held.
33    pub in_use: u32,
34    /// Number of callers waiting in the queue for a permit.
35    pub queued: u32,
36    /// Number of bulk operations currently in flight.
37    pub bulk_in_use: u32,
38}
39
40/// Limits concurrent backend operations and tracks in-flight count.
41///
42/// Permits are acquired with [`acquire`](Self::acquire) or
43/// [`acquire_bulk`](Self::acquire_bulk) and automatically returned when
44/// the [`ConcurrencyPermit`] is dropped.
45///
46/// Bulk operations use a separate budget semaphore that limits how many
47/// execution slots they may occupy. This is intended as a safe operating
48/// point — below this level there should be little-to-no performance
49/// degradation, leaving room for more tasks to be admitted via the queue
50/// before rejection is necessary.
51#[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    /// Creates a new limiter with the given maximum number of permits.
65    ///
66    /// By default the queue depth is zero, preserving the original try-or-reject behavior. Use
67    /// [`with_queue`](Self::with_queue) to enable bounded waiting.
68    ///
69    /// The bulk budget defaults to 100% of `max` (no restriction); use
70    /// [`with_bulk`](Self::with_bulk) to set a safe operating point for bulk traffic.
71    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    /// Enables bounded waiting when all execution permits are held.
85    ///
86    /// Up to `size` additional callers may park in [`acquire`](Self::acquire)
87    /// waiting for a permit. Callers beyond that are rejected immediately.
88    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    /// Sets the maximum time a caller may wait for a permit.
95    ///
96    /// Applies to both [`acquire`](Self::acquire) (when parked in the
97    /// queue) and [`acquire_bulk`](Self::acquire_bulk) (waiting for the
98    /// bulk and execution semaphores). Defaults to 1 second.
99    pub fn with_timeout(mut self, timeout: Duration) -> Self {
100        self.timeout = timeout;
101        self
102    }
103
104    /// Sets the bulk concurrency budget as a percentage of `max`.
105    ///
106    /// `percent` is clamped to `1..=100`. At `100` (the default), bulk
107    /// operations can use all execution slots. Lower values set a safe
108    /// operating point below which there is little-to-no performance
109    /// degradation, allowing more tasks to queue before rejection is
110    /// necessary — e.g. `60` means bulk operations can hold at most 60%
111    /// of permits.
112    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    /// Acquires a single concurrency permit, waiting if necessary.
120    ///
121    /// If a permit is free, returns immediately without touching the
122    /// queue. Otherwise, acquires a queue ticket (bounded by the queue
123    /// depth) and waits up to the configured timeout. Returns
124    /// [`ErrorKind::AtCapacity`] if the queue is full or on timeout.
125    pub async fn acquire(&self) -> Result<ConcurrencyPermit> {
126        if self.tasks_total == 0 {
127            return Err(ErrorKind::AtCapacity.into());
128        }
129
130        // Fast path: Instantly grab a free permit without parking.
131        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        // Slow path: acquire a temporary queue ticket to bound concurrent
140        // waiters. Released in this scope when the permit is constructed.
141        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    /// Tries to acquire a single permit without waiting.
161    ///
162    /// Returns [`ErrorKind::AtCapacity`] when no permits are available.
163    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    /// Acquires a single permit for a bulk operation, waiting if necessary.
178    ///
179    /// Bulk operations are bounded by the bulk budget — a safe operating
180    /// point below which there is little-to-no performance degradation.
181    /// Both the bulk semaphore and the inner execution semaphore are
182    /// acquired under a single timeout deadline configured via
183    /// [`with_timeout`](Self::with_timeout).
184    ///
185    /// Returns [`ErrorKind::AtCapacity`] on timeout or when `max` is zero.
186    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    /// Returns the number of permits currently available.
213    pub fn available_permits(&self) -> u32 {
214        u32::try_from(self.tasks.available_permits()).unwrap_or(self.tasks_total)
215    }
216
217    /// Returns the number of permits currently held.
218    pub fn used_permits(&self) -> u32 {
219        self.tasks_total - self.available_permits()
220    }
221
222    /// Returns the total number of permits.
223    pub fn total_permits(&self) -> u32 {
224        self.tasks_total
225    }
226
227    /// Returns the number of callers currently waiting in the queue.
228    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    /// Returns the configured queue capacity.
234    pub fn total_queue(&self) -> u32 {
235        self.queue_total
236    }
237
238    /// Returns the number of bulk permits currently held.
239    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    /// Returns the bulk concurrency budget.
245    pub fn total_bulk(&self) -> u32 {
246        self.bulk_total
247    }
248
249    /// Waits until all permits have been returned.
250    #[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    /// Returns a snapshot of the current counts.
262    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    /// Periodically calls `emit` with the current in-use and queued counts.
271    ///
272    /// This future runs forever and is intended to be spawned as a background
273    /// task alongside the service.
274    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
287/// RAII guard for a concurrency permit.
288///
289/// Dropping this permit releases it back to the [`ConcurrencyLimiter`] and
290/// notifies any task waiting in [`ConcurrencyLimiter::wait_all`].
291///
292/// The execution permit drops before the bulk permit so that a waiter
293/// blocked on the task semaphore sees the freed slot immediately.
294pub 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
314/// Runs a future on a dedicated task with panic isolation and timing metrics.
315///
316/// The `guard` is moved into the spawned task and dropped after the future
317/// completes, ensuring any resource it represents (e.g. a concurrency permit)
318/// outlives the operation.
319///
320/// Emits `service.task.start` (counter) before spawning and
321/// `service.task.duration` (distribution) when the task completes, both tagged
322/// with the given `operation` name. The duration tag includes an `outcome` of
323/// `"success"` or `"error"`.
324pub 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
338/// Spawns a metered future without waiting for its result.
339///
340/// The task has the same panic isolation, metrics, and guard lifetime as
341/// [`run_metered`], but its result is discarded.
342pub 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        // Dropping one permit is not enough.
512        drop(p1);
513        assert!(futures::poll!(&mut wait).is_pending());
514
515        // Dropping the last permit should resolve it.
516        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    // --- Queue tests ---
528
529    #[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        // Bulk holding all permits also rejects instantly (no timeout wait).
547        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        // Exact count when bulk holds permits.
659        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    // --- Bulk tests ---
712
713    #[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        // Allow at least one bulk permit even when the percentage is zero.
731        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        // Normal request can still acquire the remaining permit.
751        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        // Third waiter exceeds queue depth — rejected instantly.
875        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}