Skip to main content

celers_core/
tenant_rate_limit.rs

1//! Per-tenant (per-user) rate limiting.
2//!
3//! This module composes the existing rate-limiting primitives in
4//! [`crate::rate_limit`] (the token-bucket / sliding-window [`RateLimiter`]s and
5//! [`RateLimitConfig`]) into a [`TenantRateLimiter`] keyed by *tenant id* (which
6//! may equally represent a user, an organization, or any other isolation unit).
7//!
8//! Each tenant gets its own independent rate limiter, so one noisy tenant cannot
9//! starve the others. A registry-wide default [`RateLimitConfig`] is applied to
10//! tenants without a specific override, and individual tenants may override both
11//! the rate configuration and an optional cumulative *quota* — a hard ceiling on
12//! the total number of permits a tenant may ever consume (useful for billing
13//! plans or trial limits).
14//!
15//! # Example
16//!
17//! ```
18//! use celers_core::rate_limit::RateLimitConfig;
19//! use celers_core::tenant_rate_limit::TenantRateLimiter;
20//!
21//! // Default: 100/s. Tenant "free" is overridden to a 2-permit burst.
22//! let limiter = TenantRateLimiter::with_default(RateLimitConfig::new(100.0));
23//! limiter.set_tenant_config("free", RateLimitConfig::new(1000.0).with_burst(2));
24//!
25//! // The "free" tenant exhausts its small burst...
26//! assert!(limiter.try_acquire("free"));
27//! assert!(limiter.try_acquire("free"));
28//! assert!(!limiter.try_acquire("free"));
29//!
30//! // ...while another tenant on the generous default is unaffected.
31//! assert!(limiter.try_acquire("enterprise"));
32//! ```
33
34use std::collections::HashMap;
35use std::sync::{Arc, RwLock};
36use std::time::Duration;
37
38use serde::{Deserialize, Serialize};
39
40use crate::rate_limit::{create_rate_limiter, RateLimitConfig, RateLimiter};
41
42/// The full policy applied to a single tenant: its rate-limit configuration plus
43/// an optional cumulative quota.
44///
45/// A `quota` of `None` means unlimited total permits (only the per-second rate
46/// applies). When set, once a tenant has consumed `quota` permits in total,
47/// [`TenantRateLimiter::try_acquire`] denies further requests regardless of the
48/// rate limiter's available tokens, until the tenant is reset.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct TenantRateLimit {
51    /// The rate-limit configuration (token bucket or sliding window).
52    pub config: RateLimitConfig,
53    /// Optional cumulative permit ceiling for the tenant.
54    pub quota: Option<u64>,
55}
56
57impl TenantRateLimit {
58    /// Create a tenant policy from a rate-limit configuration, with no quota.
59    #[must_use]
60    pub fn new(config: RateLimitConfig) -> Self {
61        Self {
62            config,
63            quota: None,
64        }
65    }
66
67    /// Attach a cumulative quota (maximum total permits) to this policy.
68    #[must_use]
69    pub fn with_quota(mut self, quota: u64) -> Self {
70        self.quota = Some(quota);
71        self
72    }
73}
74
75impl From<RateLimitConfig> for TenantRateLimit {
76    fn from(config: RateLimitConfig) -> Self {
77        Self::new(config)
78    }
79}
80
81impl Default for TenantRateLimit {
82    fn default() -> Self {
83        Self::new(RateLimitConfig::default())
84    }
85}
86
87/// Live state for one tenant: its limiter, resolved policy, and usage counters.
88struct TenantState {
89    /// The concrete rate limiter (token bucket or sliding window).
90    limiter: Box<dyn RateLimiter>,
91    /// Optional cumulative quota ceiling.
92    quota: Option<u64>,
93    /// Total permits granted to this tenant so far.
94    granted: u64,
95    /// Total requests denied for this tenant so far (rate-limited or over quota).
96    denied: u64,
97}
98
99impl TenantState {
100    fn from_policy(policy: &TenantRateLimit) -> Self {
101        Self {
102            limiter: create_rate_limiter(policy.config.clone()),
103            quota: policy.quota,
104            granted: 0,
105            denied: 0,
106        }
107    }
108
109    /// Returns `true` if this tenant has already reached its cumulative quota.
110    fn quota_exhausted(&self) -> bool {
111        matches!(self.quota, Some(limit) if self.granted >= limit)
112    }
113
114    fn try_acquire(&mut self) -> bool {
115        if self.quota_exhausted() {
116            self.denied += 1;
117            return false;
118        }
119        if self.limiter.try_acquire() {
120            self.granted += 1;
121            true
122        } else {
123            self.denied += 1;
124            false
125        }
126    }
127}
128
129/// A point-in-time, serializable snapshot of a tenant's usage.
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
131pub struct TenantUsage {
132    /// Total permits granted so far.
133    pub granted: u64,
134    /// Total requests denied so far.
135    pub denied: u64,
136    /// The configured cumulative quota, if any.
137    pub quota: Option<u64>,
138    /// Permits remaining before the quota is hit (`None` if unlimited).
139    pub quota_remaining: Option<u64>,
140}
141
142/// The inner, lock-free registry holding all tenant state and configuration.
143struct TenantRegistry {
144    /// Default policy for tenants without an explicit override.
145    default_policy: Option<TenantRateLimit>,
146    /// Per-tenant policy overrides (used to (re)build a tenant's limiter).
147    overrides: HashMap<String, TenantRateLimit>,
148    /// Live per-tenant state.
149    tenants: HashMap<String, TenantState>,
150}
151
152impl TenantRegistry {
153    fn new(default_policy: Option<TenantRateLimit>) -> Self {
154        Self {
155            default_policy,
156            overrides: HashMap::new(),
157            tenants: HashMap::new(),
158        }
159    }
160
161    /// Resolve the effective policy for a tenant (override wins over default).
162    fn policy_for(&self, tenant_id: &str) -> Option<TenantRateLimit> {
163        self.overrides
164            .get(tenant_id)
165            .cloned()
166            .or_else(|| self.default_policy.clone())
167    }
168
169    /// Try to acquire a permit for a tenant, lazily creating its state.
170    fn try_acquire(&mut self, tenant_id: &str) -> bool {
171        if let Some(state) = self.tenants.get_mut(tenant_id) {
172            return state.try_acquire();
173        }
174        // Tenant not yet seen: resolve its policy (override or default).
175        let Some(policy) = self.policy_for(tenant_id) else {
176            // No rate limit configured for this tenant at all -> always allow.
177            return true;
178        };
179        let mut state = TenantState::from_policy(&policy);
180        let granted = state.try_acquire();
181        self.tenants.insert(tenant_id.to_string(), state);
182        granted
183    }
184}
185
186/// A thread-safe, per-tenant rate limiter.
187///
188/// Keyed by tenant id, each tenant receives its own independent
189/// [`RateLimiter`] built from the resolved [`RateLimitConfig`] (default or
190/// per-tenant override), plus an optional cumulative quota. The limiter is cheap
191/// to clone (`Arc`-backed) and safe to share across threads.
192#[derive(Clone)]
193pub struct TenantRateLimiter {
194    inner: Arc<RwLock<TenantRegistry>>,
195}
196
197impl std::fmt::Debug for TenantRateLimiter {
198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199        let mut dbg = f.debug_struct("TenantRateLimiter");
200        if let Ok(guard) = self.inner.read() {
201            dbg.field("tracked_tenants", &guard.tenants.len())
202                .field("overrides", &guard.overrides.len())
203                .field("has_default", &guard.default_policy.is_some());
204        }
205        dbg.finish()
206    }
207}
208
209impl TenantRateLimiter {
210    /// Create a limiter with no default policy. Tenants without an explicit
211    /// override are *not* rate limited (every request is allowed).
212    #[must_use]
213    pub fn new() -> Self {
214        Self {
215            inner: Arc::new(RwLock::new(TenantRegistry::new(None))),
216        }
217    }
218
219    /// Create a limiter whose default [`RateLimitConfig`] (without a quota) is
220    /// applied to any tenant lacking an explicit override.
221    #[must_use]
222    pub fn with_default(config: RateLimitConfig) -> Self {
223        Self {
224            inner: Arc::new(RwLock::new(TenantRegistry::new(Some(
225                TenantRateLimit::new(config),
226            )))),
227        }
228    }
229
230    /// Create a limiter whose default policy (rate *and* optional quota) is
231    /// applied to any tenant lacking an explicit override.
232    #[must_use]
233    pub fn with_default_policy(policy: TenantRateLimit) -> Self {
234        Self {
235            inner: Arc::new(RwLock::new(TenantRegistry::new(Some(policy)))),
236        }
237    }
238
239    /// Register (or replace) the rate-limit configuration override for a tenant.
240    ///
241    /// Any existing live state for the tenant is rebuilt with the new config.
242    pub fn set_tenant_config(&self, tenant_id: impl Into<String>, config: RateLimitConfig) {
243        self.set_tenant_policy(tenant_id, TenantRateLimit::new(config));
244    }
245
246    /// Register (or replace) the full policy (rate + optional quota) for a tenant.
247    ///
248    /// Any existing live state for the tenant is rebuilt, resetting its usage
249    /// counters and rate-limiter tokens.
250    pub fn set_tenant_policy(&self, tenant_id: impl Into<String>, policy: TenantRateLimit) {
251        let id = tenant_id.into();
252        if let Ok(mut guard) = self.inner.write() {
253            guard.tenants.remove(&id);
254            guard.overrides.insert(id, policy);
255        }
256    }
257
258    /// Set (or update) just the cumulative quota for a tenant, keeping its
259    /// effective rate configuration. Pass `None` to clear the quota.
260    ///
261    /// If the tenant had no override yet, the current effective configuration
262    /// (its override or the default) is captured as the basis for the override.
263    pub fn set_tenant_quota(&self, tenant_id: impl Into<String>, quota: Option<u64>) {
264        let id = tenant_id.into();
265        if let Ok(mut guard) = self.inner.write() {
266            let base = guard
267                .policy_for(&id)
268                .unwrap_or_else(|| TenantRateLimit::new(RateLimitConfig::default()));
269            let policy = TenantRateLimit {
270                config: base.config,
271                quota,
272            };
273            guard.tenants.remove(&id);
274            guard.overrides.insert(id, policy);
275        }
276    }
277
278    /// Remove a tenant's override and any live state. The next request for this
279    /// tenant is served by the default policy (if any).
280    pub fn remove_tenant(&self, tenant_id: &str) {
281        if let Ok(mut guard) = self.inner.write() {
282            guard.overrides.remove(tenant_id);
283            guard.tenants.remove(tenant_id);
284        }
285    }
286
287    /// Try to acquire a single permit for a tenant.
288    ///
289    /// Returns `true` if the request is allowed, `false` if the tenant is rate
290    /// limited or has exhausted its cumulative quota. Tenants with no override
291    /// and no default policy are always allowed.
292    #[must_use]
293    pub fn try_acquire(&self, tenant_id: &str) -> bool {
294        if let Ok(mut guard) = self.inner.write() {
295            guard.try_acquire(tenant_id)
296        } else {
297            // Poisoned lock: fail open so a panic elsewhere does not block work.
298            true
299        }
300    }
301
302    /// Returns `true` if a tenant currently has at least one permit available
303    /// (and has not exhausted its quota) without consuming anything.
304    #[must_use]
305    pub fn has_capacity(&self, tenant_id: &str) -> bool {
306        if let Ok(guard) = self.inner.read() {
307            if let Some(state) = guard.tenants.get(tenant_id) {
308                return !state.quota_exhausted() && state.limiter.available_permits() > 0;
309            }
310            // Not yet materialized: capacity is governed by the resolved policy.
311            return match guard.policy_for(tenant_id) {
312                Some(policy) => policy.config.effective_burst() > 0,
313                None => true,
314            };
315        }
316        true
317    }
318
319    /// The time until a tenant's next permit becomes available. Returns
320    /// [`Duration::ZERO`] for tenants that are not materialized or not limited.
321    #[must_use]
322    pub fn time_until_available(&self, tenant_id: &str) -> Duration {
323        if let Ok(guard) = self.inner.read() {
324            if let Some(state) = guard.tenants.get(tenant_id) {
325                if state.quota_exhausted() {
326                    // Quota is a hard ceiling; no amount of waiting frees it.
327                    return Duration::MAX;
328                }
329                return state.limiter.time_until_available();
330            }
331        }
332        Duration::ZERO
333    }
334
335    /// Returns `true` if a rate limit (override or default) applies to a tenant.
336    #[must_use]
337    pub fn has_rate_limit(&self, tenant_id: &str) -> bool {
338        if let Ok(guard) = self.inner.read() {
339            guard.overrides.contains_key(tenant_id) || guard.default_policy.is_some()
340        } else {
341            false
342        }
343    }
344
345    /// Return the effective policy for a tenant (override or default), if any.
346    #[must_use]
347    pub fn policy_for(&self, tenant_id: &str) -> Option<TenantRateLimit> {
348        self.inner.read().ok().and_then(|g| g.policy_for(tenant_id))
349    }
350
351    /// Return the usage snapshot for a tenant, if it has been materialized.
352    #[must_use]
353    pub fn usage(&self, tenant_id: &str) -> Option<TenantUsage> {
354        let guard = self.inner.read().ok()?;
355        let state = guard.tenants.get(tenant_id)?;
356        let quota_remaining = state.quota.map(|q| q.saturating_sub(state.granted));
357        Some(TenantUsage {
358            granted: state.granted,
359            denied: state.denied,
360            quota: state.quota,
361            quota_remaining,
362        })
363    }
364
365    /// Reset a single tenant's rate limiter and usage counters. Its configured
366    /// policy (rate + quota) is preserved.
367    pub fn reset_tenant(&self, tenant_id: &str) {
368        if let Ok(mut guard) = self.inner.write() {
369            if let Some(state) = guard.tenants.get_mut(tenant_id) {
370                state.limiter.reset();
371                state.granted = 0;
372                state.denied = 0;
373            }
374        }
375    }
376
377    /// Reset every materialized tenant's limiter and usage counters.
378    pub fn reset_all(&self) {
379        if let Ok(mut guard) = self.inner.write() {
380            for state in guard.tenants.values_mut() {
381                state.limiter.reset();
382                state.granted = 0;
383                state.denied = 0;
384            }
385        }
386    }
387
388    /// The number of tenants that currently have live state.
389    #[must_use]
390    pub fn tracked_count(&self) -> usize {
391        self.inner.read().map(|g| g.tenants.len()).unwrap_or(0)
392    }
393}
394
395impl Default for TenantRateLimiter {
396    fn default() -> Self {
397        Self::new()
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    #[test]
406    fn no_default_allows_unconfigured_tenants() {
407        let limiter = TenantRateLimiter::new();
408        for _ in 0..1000 {
409            assert!(limiter.try_acquire("anyone"));
410        }
411        assert!(!limiter.has_rate_limit("anyone"));
412    }
413
414    #[test]
415    fn per_tenant_isolation() {
416        let limiter = TenantRateLimiter::new();
417        // Both tenants share the same shape but have independent buckets.
418        limiter.set_tenant_config("alice", RateLimitConfig::new(1000.0).with_burst(2));
419        limiter.set_tenant_config("bob", RateLimitConfig::new(1000.0).with_burst(2));
420
421        // Alice burns through her burst.
422        assert!(limiter.try_acquire("alice"));
423        assert!(limiter.try_acquire("alice"));
424        assert!(!limiter.try_acquire("alice"));
425
426        // Bob is completely unaffected by Alice hitting her cap.
427        assert!(limiter.try_acquire("bob"));
428        assert!(limiter.try_acquire("bob"));
429        assert!(!limiter.try_acquire("bob"));
430    }
431
432    #[test]
433    fn default_vs_override() {
434        // Generous default, strict override for one tenant.
435        let limiter = TenantRateLimiter::with_default(RateLimitConfig::new(1000.0).with_burst(100));
436        limiter.set_tenant_config("trial", RateLimitConfig::new(1000.0).with_burst(1));
437
438        // Trial tenant is limited to a single burst permit.
439        assert!(limiter.try_acquire("trial"));
440        assert!(!limiter.try_acquire("trial"));
441
442        // A default tenant has the generous burst available.
443        for _ in 0..100 {
444            assert!(limiter.try_acquire("default_tenant"));
445        }
446        assert!(!limiter.try_acquire("default_tenant"));
447    }
448
449    #[test]
450    fn default_applies_to_all_unknown_tenants() {
451        let limiter = TenantRateLimiter::with_default(RateLimitConfig::new(1000.0).with_burst(2));
452        assert!(limiter.has_rate_limit("whoever"));
453        assert!(limiter.try_acquire("t1"));
454        assert!(limiter.try_acquire("t1"));
455        assert!(!limiter.try_acquire("t1"));
456        // Different tenant, fresh default bucket.
457        assert!(limiter.try_acquire("t2"));
458    }
459
460    #[test]
461    fn quota_caps_total_permits() {
462        let limiter = TenantRateLimiter::new();
463        // High rate so the bucket never limits us; quota is the only ceiling.
464        limiter.set_tenant_policy(
465            "metered",
466            TenantRateLimit::new(RateLimitConfig::new(1_000_000.0).with_burst(1000)).with_quota(3),
467        );
468
469        assert!(limiter.try_acquire("metered"));
470        assert!(limiter.try_acquire("metered"));
471        assert!(limiter.try_acquire("metered"));
472        // Quota exhausted even though the rate limiter still has tokens.
473        assert!(!limiter.try_acquire("metered"));
474        assert!(!limiter.try_acquire("metered"));
475
476        let usage = limiter.usage("metered").expect("tenant materialized");
477        assert_eq!(usage.granted, 3);
478        assert_eq!(usage.denied, 2);
479        assert_eq!(usage.quota, Some(3));
480        assert_eq!(usage.quota_remaining, Some(0));
481    }
482
483    #[test]
484    fn quota_exhaustion_does_not_affect_other_tenant() {
485        let limiter = TenantRateLimiter::new();
486        limiter.set_tenant_policy(
487            "capped",
488            TenantRateLimit::new(RateLimitConfig::new(1_000_000.0).with_burst(1000)).with_quota(1),
489        );
490        limiter.set_tenant_config("uncapped", RateLimitConfig::new(1000.0).with_burst(5));
491
492        assert!(limiter.try_acquire("capped"));
493        assert!(!limiter.try_acquire("capped")); // over quota
494
495        // Uncapped tenant proceeds normally.
496        for _ in 0..5 {
497            assert!(limiter.try_acquire("uncapped"));
498        }
499    }
500
501    #[test]
502    fn set_tenant_quota_preserves_rate() {
503        let limiter =
504            TenantRateLimiter::with_default(RateLimitConfig::new(1_000_000.0).with_burst(10));
505        limiter.set_tenant_quota("vip", Some(2));
506        let policy = limiter.policy_for("vip").expect("override created");
507        assert_eq!(policy.quota, Some(2));
508        // Rate config came from the default.
509        assert_eq!(policy.config.effective_burst(), 10);
510
511        assert!(limiter.try_acquire("vip"));
512        assert!(limiter.try_acquire("vip"));
513        assert!(!limiter.try_acquire("vip"));
514
515        // Clearing the quota lets the rate config (burst 10) govern again.
516        limiter.set_tenant_quota("vip", None);
517        assert!(limiter.policy_for("vip").expect("override").quota.is_none());
518        for _ in 0..10 {
519            assert!(limiter.try_acquire("vip"));
520        }
521    }
522
523    #[test]
524    fn reset_tenant_restores_capacity() {
525        let limiter = TenantRateLimiter::new();
526        limiter.set_tenant_config("svc", RateLimitConfig::new(0.0001).with_burst(2));
527        assert!(limiter.try_acquire("svc"));
528        assert!(limiter.try_acquire("svc"));
529        assert!(!limiter.try_acquire("svc"));
530
531        limiter.reset_tenant("svc");
532        assert!(limiter.try_acquire("svc"));
533
534        let usage = limiter.usage("svc").expect("materialized");
535        // After reset the granted count restarts; one acquire since reset.
536        assert_eq!(usage.granted, 1);
537    }
538
539    #[test]
540    fn reset_all_restores_all_tenants() {
541        let limiter = TenantRateLimiter::new();
542        limiter.set_tenant_config("a", RateLimitConfig::new(0.0001).with_burst(1));
543        limiter.set_tenant_config("b", RateLimitConfig::new(0.0001).with_burst(1));
544        assert!(limiter.try_acquire("a"));
545        assert!(limiter.try_acquire("b"));
546        assert!(!limiter.try_acquire("a"));
547        assert!(!limiter.try_acquire("b"));
548
549        limiter.reset_all();
550        assert!(limiter.try_acquire("a"));
551        assert!(limiter.try_acquire("b"));
552    }
553
554    #[test]
555    fn remove_tenant_falls_back_to_default() {
556        let limiter = TenantRateLimiter::with_default(RateLimitConfig::new(1000.0).with_burst(5));
557        limiter.set_tenant_config("temp", RateLimitConfig::new(1000.0).with_burst(1));
558        assert!(limiter.try_acquire("temp"));
559        assert!(!limiter.try_acquire("temp")); // strict override active
560
561        limiter.remove_tenant("temp");
562        // Now governed by the generous default again.
563        let policy = limiter.policy_for("temp").expect("default applies");
564        assert_eq!(policy.config.effective_burst(), 5);
565        for _ in 0..5 {
566            assert!(limiter.try_acquire("temp"));
567        }
568    }
569
570    #[test]
571    fn sliding_window_tenant_is_supported() {
572        let limiter = TenantRateLimiter::new();
573        // Sliding-window cap is `rate * window_size`; 3/s over a 1s window == 3.
574        limiter.set_tenant_config("sw", RateLimitConfig::new(3.0).with_sliding_window(1));
575        assert!(limiter.try_acquire("sw"));
576        assert!(limiter.try_acquire("sw"));
577        assert!(limiter.try_acquire("sw"));
578        assert!(!limiter.try_acquire("sw"));
579    }
580
581    #[test]
582    fn usage_absent_for_unmaterialized_tenant() {
583        let limiter = TenantRateLimiter::with_default(RateLimitConfig::new(10.0));
584        assert!(limiter.usage("never_seen").is_none());
585        assert_eq!(limiter.tracked_count(), 0);
586        let _ = limiter.try_acquire("now_seen");
587        assert_eq!(limiter.tracked_count(), 1);
588        assert!(limiter.usage("now_seen").is_some());
589    }
590
591    #[test]
592    fn time_until_available_for_exhausted_quota_is_max() {
593        let limiter = TenantRateLimiter::new();
594        limiter.set_tenant_policy(
595            "q",
596            TenantRateLimit::new(RateLimitConfig::new(1_000_000.0).with_burst(100)).with_quota(1),
597        );
598        assert!(limiter.try_acquire("q"));
599        assert!(!limiter.try_acquire("q"));
600        assert_eq!(limiter.time_until_available("q"), Duration::MAX);
601    }
602
603    #[test]
604    fn has_capacity_reflects_state() {
605        let limiter = TenantRateLimiter::new();
606        limiter.set_tenant_config("c", RateLimitConfig::new(0.0001).with_burst(1));
607        // Unmaterialized but configured with burst -> reports capacity.
608        assert!(limiter.has_capacity("c"));
609        assert!(limiter.try_acquire("c"));
610        // Bucket now empty (rate is effectively zero over the test window).
611        assert!(!limiter.has_capacity("c"));
612    }
613
614    #[test]
615    fn policy_from_config_conversion() {
616        let policy: TenantRateLimit = RateLimitConfig::new(50.0).into();
617        assert!(policy.quota.is_none());
618        assert!((policy.config.rate - 50.0).abs() < f64::EPSILON);
619    }
620
621    #[test]
622    fn tenant_rate_limit_serde_round_trip() {
623        let policy =
624            TenantRateLimit::new(RateLimitConfig::new(25.0).with_burst(50)).with_quota(500);
625        let json = serde_json::to_string(&policy).expect("serialize");
626        let parsed: TenantRateLimit = serde_json::from_str(&json).expect("deserialize");
627        assert_eq!(parsed.quota, Some(500));
628        assert_eq!(parsed.config.burst, Some(50));
629    }
630
631    #[test]
632    fn concurrent_tenants_respect_caps() {
633        use std::thread;
634
635        let limiter = TenantRateLimiter::new();
636        // Very low rate so tokens do not regenerate mid-test.
637        limiter.set_tenant_config("shared", RateLimitConfig::new(0.001).with_burst(10));
638
639        let handles: Vec<_> = (0..4)
640            .map(|_| {
641                let l = limiter.clone();
642                thread::spawn(move || {
643                    let mut count = 0;
644                    for _ in 0..10 {
645                        if l.try_acquire("shared") {
646                            count += 1;
647                        }
648                    }
649                    count
650                })
651            })
652            .collect();
653
654        let total: usize = handles
655            .into_iter()
656            .map(|h| h.join().expect("thread joins"))
657            .sum();
658        // No more than the burst capacity may ever be granted.
659        assert!(total <= 10, "granted {total} permits, expected <= 10");
660    }
661}