Skip to main content

InMemoryBackend

Struct InMemoryBackend 

Source
pub struct InMemoryBackend { /* private fields */ }
Expand description

Real in-process backend. Holds counters in a HashMap<(key, window), u64> guarded by a Mutex. Garbage-collects expired windows opportunistically on each incr call.

Implementations§

Source§

impl InMemoryBackend

Source

pub fn new() -> Self

Examples found in repository?
examples/sample_app.rs (line 277)
270fn per_account_quota() {
271    use std::sync::Arc;
272
273    use subms_rate_limiter::{DistributedLimiter, InMemoryBackend, TestClock};
274
275    println!("\n== distributed-backend: one account quota, two routers ==");
276    let clock = Arc::new(TestClock::new());
277    let shared = Arc::new(InMemoryBackend::new());
278    let window_ns = 1_000_000_000u64; // 1s window, 5 orders per account
279
280    let router_a = DistributedLimiter::with_clock(
281        Box::new(SharedBackend(shared.clone())),
282        5,
283        window_ns,
284        Box::new(SharedClock(clock.clone())),
285    );
286    let router_b = DistributedLimiter::with_clock(
287        Box::new(SharedBackend(shared.clone())),
288        5,
289        window_ns,
290        Box::new(SharedClock(clock.clone())),
291    );
292
293    let account = "acct-42";
294    let mut admitted = 0usize;
295    for round in 0..8 {
296        let router = if round % 2 == 0 { &router_a } else { &router_b };
297        if router.try_acquire(account) {
298            admitted += 1;
299        }
300    }
301    println!("  8 orders sprayed across 2 routers: {admitted} admitted (quota 5)");
302    assert_eq!(admitted, 5, "the shared quota holds across both routers");
303}
More examples
Hide additional examples
examples/perf_features.rs (line 392)
247fn main() -> io::Result<()> {
248    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
249        .join("..")
250        .join(".subms")
251        .join("features")
252        .join("rust.json");
253    let existing = std::fs::read_to_string(&path).unwrap_or_default();
254    let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
255    // Stamp the box these numbers came from. The bench runs wherever it is
256    // invoked, so an unstamped manifest is indistinguishable from a fleet
257    // capture; the renderer will not publish one it cannot attribute.
258    let (source, instance) = SubMsP99Source::from_env();
259    manifest.set_p99_source(source, instance.as_deref());
260
261    // The baseline: a plain `try_acquire` on the base GCRA limiter, cycled
262    // over a fleet the same size as the canonical sweep point so it pays the
263    // same cache footprint the features do. Every call is granted; GCRA's
264    // reject path returns before the CAS, so the grant is the dearer branch
265    // and the conservative baseline.
266    let base = measure(
267        || {
268            (0..CANON_N)
269                .map(|_| RateLimiter::new(BASE_RATE, BASE_BURST))
270                .collect::<Vec<_>>()
271        },
272        |v, i| v[i % v.len()].try_acquire(),
273        OPS,
274        BATCH,
275    );
276    let base_p50 = base.p50;
277    eprintln!(
278        "base try_acquire over {CANON_N} limiters: p50={base_p50}ns p99={}ns accept={:.0}%",
279        base.p99,
280        base.accept * 100.0
281    );
282    // One hammered limiter, for context only. It is not the classifier's base:
283    // comparing a fleet-cycling feature against a single hot limiter would
284    // charge the feature for the cache misses the workload shape causes.
285    let hot = measure(
286        || RateLimiter::new(BASE_RATE, (2 * OPS) as u64),
287        |r, _| r.try_acquire(),
288        OPS,
289        BATCH,
290    );
291    eprintln!(
292        "base try_acquire on 1 hot limiter: p50={}ns p99={}ns (context only)",
293        hot.p50, hot.p99
294    );
295
296    // ---------- token-bucket: mutex-guarded refill + batch drain ----------
297    #[cfg(feature = "token-bucket")]
298    {
299        use subms_rate_limiter::TokenBucket;
300
301        fn fleet(n: usize) -> Vec<TokenBucket> {
302            let v: Vec<TokenBucket> = (0..n)
303                .map(|_| TokenBucket::with_clock(TB_CAP, TB_RATE, Box::new(SteppingClock::new())))
304                .collect();
305            for (j, b) in v.iter().enumerate() {
306                for _ in 0..PRE_DRAIN + (j & 1) {
307                    b.try_acquire(1);
308                }
309            }
310            v
311        }
312
313        let (rows, canon) = sweep("token-bucket/try_acquire", &SIZES, |n| {
314            measure(
315                || fleet(n),
316                |v, i| v[i % v.len()].try_acquire(1),
317                OPS,
318                BATCH,
319            )
320        });
321        let (cat, reason) = classify_feature(&rows, Some(base_p50), None);
322
323        let avail = measure(
324            || fleet(CANON_N),
325            |v, i| {
326                let _ = v[i % v.len()].available();
327                true
328            },
329            OPS,
330            BATCH,
331        );
332        let mut p99 = BTreeMap::new();
333        p99.insert("try_acquire".to_string(), canon.p99);
334        p99.insert("available".to_string(), avail.p99);
335        manifest.set_feature("token-bucket", cat, &p99, &reason);
336    }
337
338    // ---------- hierarchical: child AND parent must both grant ----------
339    #[cfg(feature = "hierarchical")]
340    {
341        use subms_rate_limiter::HierarchicalLimiter;
342
343        // Swept on the CHILD COUNT, which is the only thing this feature can
344        // scale. It is not a parent CHAIN - the source holds one parent and a
345        // flat `Vec` of children, and a call is a `Vec` index plus a fixed
346        // three bucket operations - so the cost is expected to be flat and the
347        // sweep is what says so rather than a reading of the code.
348        fn hier(n: usize) -> HierarchicalLimiter {
349            let h = HierarchicalLimiter::with_clock_fn(
350                HIER_PARENT_CAP,
351                HIER_PARENT_RATE,
352                n,
353                TB_CAP,
354                TB_RATE,
355                || Box::new(SteppingClock::new()),
356            );
357            for c in 0..h.num_children() {
358                for _ in 0..PRE_DRAIN + (c & 1) {
359                    h.try_acquire(c, 1);
360                }
361            }
362            h
363        }
364
365        let (rows, canon) = sweep("hierarchical/try_acquire", &SIZES, |n| {
366            measure(
367                || hier(n),
368                |h, i| h.try_acquire(i % h.num_children(), 1),
369                OPS,
370                BATCH,
371            )
372        });
373        let (cat, reason) = classify_feature(&rows, Some(base_p50), None);
374
375        let mut p99 = BTreeMap::new();
376        p99.insert("try_acquire".to_string(), canon.p99);
377        manifest.set_feature("hierarchical", cat, &p99, &reason);
378    }
379
380    // ---------- distributed-backend: fixed-window counters ----------
381    #[cfg(feature = "distributed-backend")]
382    {
383        use subms_rate_limiter::{DistributedLimiter, InMemoryBackend};
384
385        // Keys are built once, outside every timed region: formatting one
386        // inside the loop would put string construction in the measurement.
387        // Fixed width so key length is not a second variable.
388        let keys: Vec<String> = (0..DIST_CANON).map(|i| format!("key-{i:06}")).collect();
389
390        let prefilled = |n: usize| {
391            let d = DistributedLimiter::with_clock(
392                Box::new(InMemoryBackend::new()),
393                DIST_LIMIT,
394                DIST_WINDOW_NS,
395                Box::new(SteppingClock::new()),
396            );
397            for k in &keys[..n] {
398                d.try_acquire(k);
399            }
400            d
401        };
402
403        let (rows, canon) = sweep("distributed-backend/try_acquire", &DIST_SIZES, |n| {
404            measure(
405                || prefilled(n),
406                |d, i| d.try_acquire(&keys[i % DIST_HOT]),
407                DIST_OPS,
408                1,
409            )
410        });
411        let (cat, reason) = classify_feature(&rows, Some(base_p50), None);
412
413        let mut p99 = BTreeMap::new();
414        p99.insert("try_acquire".to_string(), canon.p99);
415        manifest.set_feature("distributed-backend", cat, &p99, &reason);
416    }
417
418    // ---------- metrics: counters around the same bucket ----------
419    #[cfg(feature = "metrics")]
420    {
421        use subms_rate_limiter::MeteredTokenBucket;
422
423        fn fleet(n: usize) -> Vec<MeteredTokenBucket> {
424            let v: Vec<MeteredTokenBucket> = (0..n)
425                .map(|_| {
426                    MeteredTokenBucket::with_clock(TB_CAP, MET_RATE, Box::new(SteppingClock::new()))
427                })
428                .collect();
429            for (j, b) in v.iter().enumerate() {
430                for _ in 0..PRE_DRAIN + (j & 1) {
431                    b.try_acquire(1);
432                }
433            }
434            v
435        }
436
437        let (rows, canon) = sweep("metrics/try_acquire", &SIZES, |n| {
438            measure(
439                || fleet(n),
440                |v, i| v[i % v.len()].try_acquire(1),
441                OPS,
442                BATCH,
443            )
444        });
445        let (cat, reason) = classify_feature(&rows, Some(base_p50), None);
446
447        let snap = measure(
448            || fleet(CANON_N),
449            |v, i| {
450                let _ = v[i % v.len()].snapshot();
451                true
452            },
453            OPS,
454            BATCH,
455        );
456        let mut p99 = BTreeMap::new();
457        p99.insert("try_acquire".to_string(), canon.p99);
458        p99.insert("snapshot".to_string(), snap.p99);
459        manifest.set_feature("metrics", cat, &p99, &reason);
460    }
461
462    // ---------- keyed: one GCRA limiter per key, sharded ----------
463    #[cfg(feature = "keyed")]
464    {
465        use subms_rate_limiter::KeyedRateLimiter;
466
467        // Swept on the LIVE KEY COUNT, the only axis this feature scales on.
468        // A hash map lookup is expected to read flat; what the sweep is really
469        // testing is that the sharded lock does not turn into the bottleneck as
470        // the key set outgrows cache.
471        //
472        // Keys are formatted once, outside every timed region, and the clock is
473        // driven rather than read: a fixed `now` of 0 with a burst wide enough
474        // to cover every timed call keeps each op on the grant branch, which is
475        // the dearer one (a reject returns before the map write).
476        let keys: Vec<String> = (0..CANON_N).map(|i| format!("key-{i:06}")).collect();
477        let burst = (OPS / SIZES[0] + 2) as u64;
478
479        let (rows, canon) = sweep("keyed/try_acquire", &SIZES, |n| {
480            measure(
481                || KeyedRateLimiter::new(BASE_RATE, burst),
482                |k, i| matches!(k.try_acquire_at(0, &keys[i % n], 1), Acquire::Ok),
483                OPS,
484                BATCH,
485            )
486        });
487        let (cat, reason) = classify_feature(&rows, Some(base_p50), None);
488
489        let mut p99 = BTreeMap::new();
490        p99.insert("try_acquire".to_string(), canon.p99);
491        manifest.set_feature("keyed", cat, &p99, &reason);
492    }
493
494    std::fs::create_dir_all(path.parent().unwrap())?;
495    std::fs::write(&path, manifest.to_json())?;
496    io::stdout().write_all(manifest.to_json().as_bytes())?;
497    Ok(())
498}

Trait Implementations§

Source§

impl Backend for InMemoryBackend

Source§

fn incr(&self, key: &str, window_start_ns: u64, ttl_ns: u64) -> u64

Increment the counter at key for window_start_ns. Returns the new count after the bump. Must be atomic across concurrent callers.
Source§

fn read(&self, key: &str, window_start_ns: u64) -> u64

Read the current counter without bumping. Returns 0 if the (key, window) pair is unknown or expired.
Source§

impl Default for InMemoryBackend

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.