Module governor::_guide

source ·
Expand description

§A more in-depth guide to governor

Governor is a fork/rewrite/rebranding of the ratelimit_meter and ratelimit_futures crates. Many of the things that worked there still work here, and this guide’s aim is to help you accomplish them.

§Constructing a rate limiter

If you’re used to ratelimit_meter parlance, both “direct” and “keyed” rate limiters are supported. Direct rate limiters keep only a single state (such as the rate of outflowing e-mail conversations). Keyed rate limiters on the other hand have one rate limiting state per key: e.g., the flow of API requests made by each customer.

Construction of rate limiters is designed to be mostly infallible via types that ensure you can’t pass the wrong parameters, mostly around non-zero integers. Since that kind of checking is a little tedious to do by hand, governor makes heavy use of the NonZeroU32 type.

To conveniently construct these nonzero numbers yourself, use the nonzero! macro.

§Quotas

Each rate limiter has a quota: A rate of elements (could be API requests, emails, phone calls… anything really) per unit of time (second, minute, hour). Specify these in a Quota object like so:

use governor::Quota;
Quota::per_second(nonzero!(20u32));
§Constructing a direct rate limiter

To make a direct rate limiter, you have to construct a quota, as above; and then use this to construct the rate limiter itself. In std mode, this is easily accomplished like so:

RateLimiter::direct(Quota::per_second(nonzero!(50u32)));

In no_std mode, there are is no default monotonic (or system) clock available. To effectively limit rates, you will have to either use the provided “fake” clock (which must be manually advanced, and is mainly useful for tests), or implement the Clock trait for your platform. Once that decision is made, constructing a rate limiter with an explicit clock works like this:

let clock = FakeRelativeClock::default();
RateLimiter::direct_with_clock(Quota::per_second(nonzero!(50u32)), &clock);
§Constructing a keyed rate limiter

For a keyed rate limiter, you have to specify the type of the key: Otherwise they function exactly as their direct counterpart. They are stored in a hash-table like state store. The default in std mode is provided by the dashmap crate:

let lim = RateLimiter::keyed(Quota::per_second(nonzero!(50u32)));
lim.check_key(&"cus_1").unwrap(); // one key
lim.check_key(&"cus_2").unwrap(); // another!

You can supply your own keyed state store implementation if you wish. That requires implementing the KeyedStateStore trait, and optionally the ShrinkableKeyedStateStore trait.

§Type signatures for rate limiters

Rate limiters tend to be long-lived, and need to be stored somewhere - sometimes in struct fields, or even just to pass in function arguments. The crate::RateLimiter type signatures tend to be pretty unwieldy for that, so this crate exports a pair of handy type aliases, crate::DefaultDirectRateLimiter and crate::DefaultKeyedRateLimiter.

Here’s an example for embedding a direct rate limiter in a struct:

struct MyApiClient {
    limiter: DefaultDirectRateLimiter,
}

If you need to provide a different clock, or a different implementation of the keyed state, you will still have to fall back to the regular type.

§Data ownership and references to rate limiters

governor’s rate limiter state is not hidden behind an interior mutability pattern, and so it is perfectly valid to have multiple references to a rate limiter in a program. Since its state lives in AtomicU64 integers (which do not implement Clone), the rate limiters themselves can not be cloned.

§Usage in multiple threads

Sharing references to a rate limiter across threads is completely OK (rate limiters are Send and Sync by default), but there is a problem: A rate limiter’s lifetime might be up before a thread ends, which would invalidate the reference.

So, to use a rate limiter in multiple threads without lifetime issues, there are two equally valid strategies:

§crossbeam scoped tasks

The crossbeam crate’s scopes allow code to guarantee that a thread spawned in a scope terminates before the scope terminates. This allows using

stack-allocated variables. Here is an example test using crossbeam scopes:


let mut clock = FakeRelativeClock::default();
let lim = RateLimiter::direct_with_clock(Quota::per_second(nonzero!(20u32)), &clock);
let ms = Duration::from_millis(1);

crossbeam::scope(|scope| {
    for _i in 0..20 {
        scope.spawn(|_| {
            assert_eq!(Ok(()), lim.check());
        });
    }
})
.unwrap();
§Wrapping the limiter in an Arc

The other method uses only the standard library: Wrapping the rate limiter in an Arc will keep the limiter alive for as long as there exist references to it - perfect for passing to threads.

In this example, note that we’re cloning the Arc; the rate limiter stays identical (rate limiters do not implement Clone), only its references are duplicated (and refcounts incremented atomically).

Note also the placement of the clone: As we’re creating a move closure, a binding that can be moved into the closure must be set up outside it. Rustc will be upset at you if you try to clone the Arc too early outside the closure, or even inside it. See the Arc docs for some more usage examples.

let bucket = Arc::new(RateLimiter::direct(Quota::per_second(nonzero!(20u32))));
for _i in 0..20 {
    let bucket = Arc::clone(&bucket);
    thread::spawn(move || {
        assert_eq!(Ok(()), bucket.check());
    })
    .join()
    .unwrap();
}