pub struct WindowQuotaTable { /* private fields */ }Expand description
Builds a RateLimiter from a venue’s window quotas: count requests in
any window of length period.
Every request passes the general bucket, then the buckets of the first row
whose pattern and method it matches, in the order the row lists them. Two
rows naming one BucketId share one allowance: one venue caps four
ledger routes at 900 per 10 s combined, which four buckets of their own
would turn into 3,600.
§How a bucket paces
Each bucket holds one token and refills one every
paced_interval, which aims at nine tenths of
count. There is no burst setting, on purpose:
- A token bucket admits its depth plus everything its refill adds. A
bucket holding
counttokens and refillingcountperperiodadmits twice the quota in one window. Depth is borrowed against the rate, so the least depth is also the most throughput. - A request that arrives during a hold waits before any bucket, and is paced when the hold ends. A request already waiting on a bucket when the hold begins takes its token, then waits out the hold too, and is released with the others when it ends: that burst is at most the requests in flight when the hold began (on the send loop, the concurrency permits).
- A published count is reachable as a burst and not as a rate. On
one venue’s
/closed-positions(150 per 10 s) a sustained 95% was refused and 90% ran clean for 180 s, so a bucket keeps a tenth back.
A count below 2 paces as 2 per window, since a bucket holds at least one token.
§Example
use std::time::Duration;
use polyoxide_core::{Matching, WindowQuotaTable};
use polyoxide_core::http::Method;
let ten_seconds = Duration::from_secs(10);
let mut table = WindowQuotaTable::new(1_000, ten_seconds);
let ledger = table.bucket(900, ten_seconds);
table
.prefix("/positions", None, 150, ten_seconds)
.row(Matching::Prefix, "/trades", Some(Method::GET), &[ledger])
.row(Matching::Exact, "/orders", Some(Method::GET), &[ledger]);
let limiter = table.build();
// The general bucket first, then the row's.
let trades = limiter.effective_quota(&Method::GET, "/trades");
let orders = limiter.effective_quota(&Method::GET, "/orders");
assert_eq!(trades[1].bucket, orders[1].bucket);
assert_eq!(trades[1].admitted_in_one_window(), 810);
// An exact row does not match a sub-path.
assert_eq!(limiter.effective_quota(&Method::GET, "/orders/1").len(), 1);Implementations§
Source§impl WindowQuotaTable
impl WindowQuotaTable
Sourcepub fn new(count: u32, period: Duration) -> Self
pub fn new(count: u32, period: Duration) -> Self
A table whose general bucket admits count requests per period.
§Panics
When period is too short to pace count, since a bucket that paces
at an interval of zero would never hold anyone back: a zero period,
or one shorter than a nanosecond per request it paces.
Sourcepub fn bucket(&mut self, count: u32, period: Duration) -> BucketId
pub fn bucket(&mut self, count: u32, period: Duration) -> BucketId
Make a bucket admitting count requests per period, for rows to
name.
§Panics
When period is too short to pace count, since a bucket that paces
at an interval of zero would never hold anyone back: a zero period,
or one shorter than a nanosecond per request it paces.
Sourcepub fn row(
&mut self,
matching: Matching,
pattern: &'static str,
method: Option<Method>,
buckets: &[BucketId],
) -> &mut Self
pub fn row( &mut self, matching: Matching, pattern: &'static str, method: Option<Method>, buckets: &[BucketId], ) -> &mut Self
Add a row: a request whose path matches pattern, and whose method is
method when one is given, awaits buckets in order.
Only the first row a request matches applies, so a pattern goes before any shorter one that also matches it.
§Panics
When one of buckets was made by another table.
Sourcepub fn prefix(
&mut self,
pattern: &'static str,
method: Option<Method>,
count: u32,
period: Duration,
) -> &mut Self
pub fn prefix( &mut self, pattern: &'static str, method: Option<Method>, count: u32, period: Duration, ) -> &mut Self
Add a Matching::Prefix row with a bucket of its own, admitting
count requests per period.
§Panics
When period is too short to pace count, since a bucket that paces
at an interval of zero would never hold anyone back: a zero period,
or one shorter than a nanosecond per request it paces.
Sourcepub fn with_hold(&mut self, hold: Hold) -> &mut Self
pub fn with_hold(&mut self, hold: Hold) -> &mut Self
Wait out hold instead of a hold of the limiter’s own, so that
every layer sharing it holds together. Default: Hold::unbounded.
The limiter keeps time on the hold’s runtime.
Sourcepub fn with_runtime(&mut self, runtime: Arc<dyn Runtime>) -> &mut Self
pub fn with_runtime(&mut self, runtime: Arc<dyn Runtime>) -> &mut Self
Keep time on runtime: the limiter’s waits for a bucket and for its
hold sleep on it. This gives the table a hold of its own on runtime,
replacing the one it has, so call it before
with_hold, whose hold brings its own runtime.
Default: the target’s default runtime.
§Panics
In a debug build, when with_hold was called
first, since this would silently undo the shared hold.
Sourcepub fn build(self) -> RateLimiter
pub fn build(self) -> RateLimiter
The limiter, with every bucket full.
Sourcepub fn paced_interval(count: u32, period: Duration) -> Duration
pub fn paced_interval(count: u32, period: Duration) -> Duration
The interval a bucket for count requests per period paces at:
period over nine tenths of count, less the one token of depth.
The one place the pacing formula lives; every bucket is built from it.