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
impl InMemoryBackend
Sourcepub fn new() -> Self
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
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§
Auto Trait Implementations§
impl !Freeze for InMemoryBackend
impl RefUnwindSafe for InMemoryBackend
impl Send for InMemoryBackend
impl Sync for InMemoryBackend
impl Unpin for InMemoryBackend
impl UnsafeUnpin for InMemoryBackend
impl UnwindSafe for InMemoryBackend
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more