Skip to main content

nautilus_network/ratelimiter/
quota.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::{num::NonZeroU32, time::Duration};
17
18use super::nanos::Nanos;
19
20/// A rate-limiting quota.
21///
22/// Quotas are expressed in a positive number of "cells" (the maximum number of positive decisions /
23/// allowed items until the rate limiter needs to replenish) and the amount of time for the rate
24/// limiter to replenish a single cell.
25///
26/// Neither the number of cells nor the replenishment unit of time may be zero.
27///
28/// # Burst Sizes
29/// There are multiple ways of expressing the same quota: a quota given as `Quota::per_second(1)`
30/// allows, on average, the same number of cells through as a quota given as `Quota::per_minute(60)`.
31/// The quota of `Quota::per_minute(60)` has a burst size of 60 cells, meaning it is
32/// possible to accommodate 60 cells in one go, after which the equivalent of a minute of inactivity
33/// is required for the burst allowance to be fully restored.
34///
35/// Burst size gets really important when you construct a rate limiter that should allow multiple
36/// elements through at one time (using [`RateLimiter.check_n`](struct.RateLimiter.html#method.check_n)
37/// and its related functions): Only
38/// at most as many cells can be let through in one call as are given as the burst size.
39///
40/// In other words, the burst size is the maximum number of cells that the rate limiter will ever
41/// allow through without replenishing them.
42#[derive(Debug, PartialEq, Eq, Clone, Copy)]
43pub struct Quota {
44    pub(crate) max_burst: NonZeroU32,
45    pub(crate) replenish_1_per: Duration,
46}
47
48/// Constructors for Quotas
49impl Quota {
50    /// Constructs a quota for a number of cells per second. The given number of cells is also
51    /// assumed to be the maximum burst size.
52    ///
53    /// Returns `None` if `max_burst` is so large that the replenish interval rounds to zero
54    /// nanoseconds (i.e. `max_burst > 1_000_000_000`).
55    #[must_use]
56    pub const fn per_second(max_burst: NonZeroU32) -> Option<Self> {
57        let replenish_interval_ns = Duration::from_secs(1).as_nanos() / (max_burst.get() as u128);
58        if replenish_interval_ns == 0 {
59            return None;
60        }
61
62        Some(Self {
63            max_burst,
64            replenish_1_per: Duration::from_nanos(replenish_interval_ns as u64),
65        })
66    }
67
68    /// Constructs a quota for a number of cells per 60-second period. The given number of cells
69    /// is also assumed to be the maximum burst size.
70    #[must_use]
71    pub const fn per_minute(max_burst: NonZeroU32) -> Self {
72        let replenish_interval_ns = Duration::from_mins(1).as_nanos() / (max_burst.get() as u128);
73
74        Self {
75            max_burst,
76            replenish_1_per: Duration::from_nanos(replenish_interval_ns as u64),
77        }
78    }
79
80    /// Constructs a quota for a number of cells per 60-minute period. The given number of cells
81    /// is also assumed to be the maximum burst size.
82    #[must_use]
83    pub const fn per_hour(max_burst: NonZeroU32) -> Self {
84        let replenish_interval_ns = Duration::from_hours(1).as_nanos() / (max_burst.get() as u128);
85
86        Self {
87            max_burst,
88            replenish_1_per: Duration::from_nanos(replenish_interval_ns as u64),
89        }
90    }
91
92    /// Constructs a quota that replenishes one cell in a given interval.
93    ///
94    /// If the time interval is zero, returns `None`.
95    #[must_use]
96    pub const fn with_period(replenish_1_per: Duration) -> Option<Self> {
97        if replenish_1_per.as_nanos() == 0 {
98            None
99        } else {
100            Some(Self {
101                max_burst: NonZeroU32::MIN,
102                replenish_1_per,
103            })
104        }
105    }
106
107    /// Adjusts the maximum burst size for a quota to construct a rate limiter with a capacity
108    /// for at most the given number of cells.
109    #[must_use]
110    pub const fn allow_burst(self, max_burst: NonZeroU32) -> Self {
111        Self { max_burst, ..self }
112    }
113}
114
115/// Retrieving information about a quota
116impl Quota {
117    /// The time it takes for a rate limiter with an exhausted burst budget to replenish
118    /// a single element.
119    #[must_use]
120    pub const fn replenish_interval(&self) -> Duration {
121        self.replenish_1_per
122    }
123
124    /// The maximum number of cells that can be allowed in one burst.
125    #[must_use]
126    pub const fn burst_size(&self) -> NonZeroU32 {
127        self.max_burst
128    }
129
130    /// The time it takes to replenish the entire maximum burst size.
131    ///
132    /// Saturates at [`Duration::MAX`] if the full duration cannot be represented.
133    #[must_use]
134    pub const fn burst_size_replenished_in(&self) -> Duration {
135        self.replenish_1_per.saturating_mul(self.max_burst.get())
136    }
137}
138
139impl Quota {
140    /// A way to reconstruct a Quota from an in-use Gcra.
141    ///
142    /// This is useful mainly for [`crate::middleware::RateLimitingMiddleware`]
143    /// where custom code may want to construct information based on
144    /// the amount of burst balance remaining.
145    ///
146    /// # Panics
147    ///
148    /// Panics if the division result is 0 or exceeds `u32::MAX`.
149    pub(crate) fn from_gcra_parameters(t: Nanos, tau: Nanos) -> Self {
150        let t_u64 = t.as_u64();
151        let tau_u64 = tau.as_u64();
152
153        // Validate division won't be zero or overflow
154        assert!(t_u64 != 0, "Invalid GCRA parameter: t cannot be zero");
155
156        let division_result = tau_u64 / t_u64;
157        assert!(
158            division_result != 0,
159            "Invalid GCRA parameters: tau/t results in zero burst capacity"
160        );
161        assert!(
162            u32::try_from(division_result).is_ok(),
163            "Invalid GCRA parameters: tau/t exceeds u32::MAX"
164        );
165
166        // We've verified the result is non-zero and fits in u32
167        let max_burst = NonZeroU32::new(division_result as u32)
168            .expect("Division result should be non-zero after validation");
169        let replenish_1_per = t.into();
170
171        Self {
172            max_burst,
173            replenish_1_per,
174        }
175    }
176}