nautilus_network/ratelimiter/
quota.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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, prelude::v1::*, time::Duration};
17
18use nonzero_ext::nonzero;
19use pyo3::{exceptions::PyException, prelude::*};
20
21use super::nanos::Nanos;
22
23/// A rate-limiting quota.
24///
25/// Quotas are expressed in a positive number of "cells" (the maximum number of positive decisions /
26/// allowed items until the rate limiter needs to replenish) and the amount of time for the rate
27/// limiter to replenish a single cell.
28///
29/// Neither the number of cells nor the replenishment unit of time may be zero.
30///
31/// # Burst sizes
32/// There are multiple ways of expressing the same quota: a quota given as `Quota::per_second(1)`
33/// allows, on average, the same number of cells through as a quota given as `Quota::per_minute(60)`.
34/// The quota of `Quota::per_minute(60)` has a burst size of 60 cells, meaning it is
35/// possible to accomodate 60 cells in one go, after which the equivalent of a minute of inactivity
36/// is required for the burst allowance to be fully restored.
37///
38/// Burst size gets really important when you construct a rate limiter that should allow multiple
39/// elements through at one time (using [`RateLimiter.check_n`](struct.RateLimiter.html#method.check_n)
40/// and its related functions): Only
41/// at most as many cells can be let through in one call as are given as the burst size.
42///
43/// In other words, the burst size is the maximum number of cells that the rate limiter will ever
44/// allow through without replenishing them.
45#[derive(Debug, PartialEq, Eq, Clone, Copy)]
46#[cfg_attr(
47    feature = "python",
48    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.network")
49)]
50pub struct Quota {
51    pub(crate) max_burst: NonZeroU32,
52    pub(crate) replenish_1_per: Duration,
53}
54
55#[pymethods]
56impl Quota {
57    #[staticmethod]
58    pub fn rate_per_second(max_burst: u32) -> PyResult<Self> {
59        match NonZeroU32::new(max_burst) {
60            Some(max_burst) => Ok(Self::per_second(max_burst)),
61            None => Err(PyErr::new::<PyException, _>(
62                "Max burst capacity should be a non-zero integer",
63            )),
64        }
65    }
66
67    #[staticmethod]
68    pub fn rate_per_minute(max_burst: u32) -> PyResult<Self> {
69        match NonZeroU32::new(max_burst) {
70            Some(max_burst) => Ok(Self::per_minute(max_burst)),
71            None => Err(PyErr::new::<PyException, _>(
72                "Max burst capacity should be a non-zero integer",
73            )),
74        }
75    }
76
77    #[staticmethod]
78    pub fn rate_per_hour(max_burst: u32) -> PyResult<Self> {
79        match NonZeroU32::new(max_burst) {
80            Some(max_burst) => Ok(Self::per_hour(max_burst)),
81            None => Err(PyErr::new::<PyException, _>(
82                "Max burst capacity should be a non-zero integer",
83            )),
84        }
85    }
86}
87
88/// Constructors for Quotas
89impl Quota {
90    /// Construct a quota for a number of cells per second. The given number of cells is also
91    /// assumed to be the maximum burst size.
92    pub const fn per_second(max_burst: NonZeroU32) -> Self {
93        let replenish_interval_ns = Duration::from_secs(1).as_nanos() / (max_burst.get() as u128);
94        Self {
95            max_burst,
96            replenish_1_per: Duration::from_nanos(replenish_interval_ns as u64),
97        }
98    }
99
100    /// Construct a quota for a number of cells per 60-second period. The given number of cells is
101    /// also assumed to be the maximum burst size.
102    pub const fn per_minute(max_burst: NonZeroU32) -> Self {
103        let replenish_interval_ns = Duration::from_secs(60).as_nanos() / (max_burst.get() as u128);
104        Self {
105            max_burst,
106            replenish_1_per: Duration::from_nanos(replenish_interval_ns as u64),
107        }
108    }
109
110    /// Construct a quota for a number of cells per 60-minute (3600-second) period. The given number
111    /// of cells is also assumed to be the maximum burst size.
112    pub const fn per_hour(max_burst: NonZeroU32) -> Self {
113        let replenish_interval_ns =
114            Duration::from_secs(60 * 60).as_nanos() / (max_burst.get() as u128);
115        Self {
116            max_burst,
117            replenish_1_per: Duration::from_nanos(replenish_interval_ns as u64),
118        }
119    }
120
121    /// Construct a quota that replenishes one cell in a given
122    /// interval.
123    ///
124    /// This constructor is meant to replace [`::new`](#method.new),
125    /// in cases where a longer refresh period than 1 cell/hour is
126    /// necessary.
127    ///
128    /// If the time interval is zero, returns `None`.
129    pub const fn with_period(replenish_1_per: Duration) -> Option<Self> {
130        if replenish_1_per.as_nanos() == 0 {
131            None
132        } else {
133            Some(Self {
134                max_burst: nonzero!(1u32),
135                replenish_1_per,
136            })
137        }
138    }
139
140    /// Adjusts the maximum burst size for a quota to construct a rate limiter with a capacity
141    /// for at most the given number of cells.
142    pub const fn allow_burst(self, max_burst: NonZeroU32) -> Self {
143        Self { max_burst, ..self }
144    }
145
146    /// Construct a quota for a given burst size, replenishing the entire burst size in that
147    /// given unit of time.
148    ///
149    /// Returns `None` if the duration is zero.
150    ///
151    /// This constructor allows greater control over the resulting
152    /// quota, but doesn't make as much intuitive sense as other
153    /// methods of constructing the same quotas. Unless your quotas
154    /// are given as "max burst size, and time it takes to replenish
155    /// that burst size", you are better served by the
156    /// [`Quota::per_second`](#method.per_second) (and similar)
157    /// constructors with the [`allow_burst`](#method.allow_burst)
158    /// modifier.
159    #[deprecated(
160        since = "0.2.0",
161        note = "This constructor is often confusing and non-intuitive. \
162    Use the `per_(interval)` / `with_period` and `max_burst` constructors instead."
163    )]
164    pub fn new(max_burst: NonZeroU32, replenish_all_per: Duration) -> Option<Self> {
165        if replenish_all_per.as_nanos() == 0 {
166            None
167        } else {
168            Some(Self {
169                max_burst,
170                replenish_1_per: replenish_all_per / max_burst.get(),
171            })
172        }
173    }
174}
175
176/// Retrieving information about a quota
177impl Quota {
178    /// The time it takes for a rate limiter with an exhausted burst budget to replenish
179    /// a single element.
180    pub const fn replenish_interval(&self) -> Duration {
181        self.replenish_1_per
182    }
183
184    /// The maximum number of cells that can be allowed in one burst.
185    pub const fn burst_size(&self) -> NonZeroU32 {
186        self.max_burst
187    }
188
189    /// The time it takes to replenish the entire maximum burst size.
190    pub const fn burst_size_replenished_in(&self) -> Duration {
191        let fill_in_ns = self.replenish_1_per.as_nanos() * self.max_burst.get() as u128;
192        Duration::from_nanos(fill_in_ns as u64)
193    }
194}
195
196impl Quota {
197    /// A way to reconstruct a Quota from an in-use Gcra.
198    ///
199    /// This is useful mainly for [`crate::middleware::RateLimitingMiddleware`]
200    /// where custom code may want to construct information based on
201    /// the amount of burst balance remaining.
202    pub(crate) fn from_gcra_parameters(t: Nanos, tau: Nanos) -> Self {
203        // Safety assurance: As we're calling this from this crate
204        // only, and we do not allow creating a Gcra from 0
205        // parameters, this is, in fact, safe.
206        //
207        // The casts may look a little sketch, but again, they're
208        // constructed from parameters that came from the crate
209        // exactly like that.
210        let max_burst = unsafe { NonZeroU32::new_unchecked((tau.as_u64() / t.as_u64()) as u32) };
211        let replenish_1_per = t.into();
212        Self {
213            max_burst,
214            replenish_1_per,
215        }
216    }
217}
218
219////////////////////////////////////////////////////////////////////////////////
220// Tests
221////////////////////////////////////////////////////////////////////////////////
222// #[cfg(test)]
223// mod test {
224//     use nonzero_ext::nonzero;
225
226//     use super::*;
227
228//     #[test]
229//     fn time_multiples() {
230//         let hourly = Quota::per_hour(nonzero!(1u32));
231//         let minutely = Quota::per_minute(nonzero!(1u32));
232//         let secondly = Quota::per_second(nonzero!(1u32));
233
234//         assert_eq!(
235//             hourly.replenish_interval() / 60,
236//             minutely.replenish_interval()
237//         );
238//         assert_eq!(
239//             minutely.replenish_interval() / 60,
240//             secondly.replenish_interval()
241//         );
242//     }
243
244//     #[test]
245//     fn period_error_cases() {
246//         assert!(Quota::with_period(Duration::from_secs(0)).is_none());
247
248//         #[allow(deprecated)]
249//         {
250//             assert!(Quota::new(nonzero!(1u32), Duration::from_secs(0)).is_none());
251//         }
252//     }
253// }