pub struct TestClock { /* private fields */ }Expand description
Deterministic clock for tests. advance(ns) moves the clock
forward; now_ns() reads the current value.
Implementations§
Source§impl TestClock
impl TestClock
Sourcepub fn new() -> Self
pub fn new() -> Self
Examples found in repository?
examples/sample_app.rs (line 218)
212fn weighted_message_budget() {
213 use std::sync::Arc;
214
215 use subms_rate_limiter::{TestClock, TokenBucket};
216
217 println!("\n== token-bucket: weighted budget that banks idle credit ==");
218 let clock = Arc::new(TestClock::new());
219 // 10 units of budget, refilling 5 units/sec.
220 let budget = TokenBucket::with_clock(10, 5.0, Box::new(SharedClock(clock.clone())));
221
222 assert!(budget.try_acquire(1), "new order costs 1 unit");
223 assert!(budget.try_acquire(5), "bulk cancel-replace costs 5 units");
224 println!(" after 1 + 5 units: {} left", budget.available());
225 assert_eq!(budget.available(), 4);
226
227 // All-or-nothing: a batch of 5 against 4 remaining spends nothing.
228 assert!(
229 !budget.try_acquire(5),
230 "insufficient budget rejects the batch"
231 );
232 assert_eq!(budget.available(), 4, "a rejected batch spends nothing");
233
234 clock.advance_ms(1_000); // +5 units, capped at 10
235 println!(" after 1s idle: {} left", budget.available());
236 assert!(budget.try_acquire(5), "banked credit admits the batch");
237}
238
239/// `hierarchical` feature: a desk runs two strategy sessions, each rated for
240/// its own flow, but the desk's single venue uplink caps the aggregate below
241/// the sum - so one hot strategy cannot starve the other.
242#[cfg(feature = "hierarchical")]
243fn desk_gateway_cap() {
244 use std::sync::Arc;
245
246 use subms_rate_limiter::{HierarchicalLimiter, TestClock};
247
248 println!("\n== hierarchical: desk uplink caps two strategies ==");
249 let clock = Arc::new(TestClock::new());
250 let c = clock.clone();
251 // Uplink admits 5 total; each of the two strategies could do 10 alone.
252 let desk =
253 HierarchicalLimiter::with_clock_fn(5, 0.0, 2, 10, 0.0, || Box::new(SharedClock(c.clone())));
254
255 let mut sent = 0usize;
256 for round in 0..10 {
257 if desk.try_acquire(round % 2, 1) {
258 sent += 1;
259 }
260 }
261 println!(" strategies offered 10 orders; uplink admitted {sent}");
262 assert_eq!(sent, 5, "the parent caps the desk aggregate at 5");
263}
264
265/// `distributed-backend` feature: an account's venue quota must hold across a
266/// fleet of stateless routers. Both consult the same fixed-window counter (the
267/// Redis INCR + EXPIRE shape), so the account cannot beat the cap by spraying
268/// orders across routers.
269#[cfg(feature = "distributed-backend")]
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}
304
305/// `metrics` feature: cap outbound requests to a market-data vendor and let
306/// the limiter be its own metric source - grant / reject counts, refill
307/// events, live headroom - with no separate observability layer.
308#[cfg(feature = "metrics")]
309fn metered_feed_throttle() {
310 use std::sync::Arc;
311
312 use subms_rate_limiter::{MeteredTokenBucket, TestClock};
313
314 println!("\n== metrics: self-observing market-data throttle ==");
315 let clock = Arc::new(TestClock::new());
316 // 5 requests per burst, refilling 100/sec.
317 let feed = MeteredTokenBucket::with_clock(5, 100.0, Box::new(SharedClock(clock.clone())));
318
319 for _ in 0..8 {
320 feed.try_acquire(1);
321 }
322 let s = feed.snapshot();
323 println!(
324 " granted {}, rejected {}, headroom {}",
325 s.granted, s.rejected, s.available
326 );
327 assert_eq!(s.granted, 5);
328 assert_eq!(s.rejected, 3);
329
330 clock.advance_ms(100); // 100/sec -> +10, capped at 5
331 assert!(feed.try_acquire(1), "the refilled feed admits again");
332 let s2 = feed.snapshot();
333 println!(
334 " after refill: granted {}, refills {}",
335 s2.granted, s2.refills
336 );
337 assert_eq!(s2.granted, 6);
338 assert!(s2.refills >= 1, "a refill step was observed");
339}pub fn with_start(start_ns: u64) -> Self
pub fn advance(&self, ns: u64)
Sourcepub fn advance_ms(&self, ms: u64)
pub fn advance_ms(&self, ms: u64)
Examples found in repository?
examples/sample_app.rs (line 234)
212fn weighted_message_budget() {
213 use std::sync::Arc;
214
215 use subms_rate_limiter::{TestClock, TokenBucket};
216
217 println!("\n== token-bucket: weighted budget that banks idle credit ==");
218 let clock = Arc::new(TestClock::new());
219 // 10 units of budget, refilling 5 units/sec.
220 let budget = TokenBucket::with_clock(10, 5.0, Box::new(SharedClock(clock.clone())));
221
222 assert!(budget.try_acquire(1), "new order costs 1 unit");
223 assert!(budget.try_acquire(5), "bulk cancel-replace costs 5 units");
224 println!(" after 1 + 5 units: {} left", budget.available());
225 assert_eq!(budget.available(), 4);
226
227 // All-or-nothing: a batch of 5 against 4 remaining spends nothing.
228 assert!(
229 !budget.try_acquire(5),
230 "insufficient budget rejects the batch"
231 );
232 assert_eq!(budget.available(), 4, "a rejected batch spends nothing");
233
234 clock.advance_ms(1_000); // +5 units, capped at 10
235 println!(" after 1s idle: {} left", budget.available());
236 assert!(budget.try_acquire(5), "banked credit admits the batch");
237}
238
239/// `hierarchical` feature: a desk runs two strategy sessions, each rated for
240/// its own flow, but the desk's single venue uplink caps the aggregate below
241/// the sum - so one hot strategy cannot starve the other.
242#[cfg(feature = "hierarchical")]
243fn desk_gateway_cap() {
244 use std::sync::Arc;
245
246 use subms_rate_limiter::{HierarchicalLimiter, TestClock};
247
248 println!("\n== hierarchical: desk uplink caps two strategies ==");
249 let clock = Arc::new(TestClock::new());
250 let c = clock.clone();
251 // Uplink admits 5 total; each of the two strategies could do 10 alone.
252 let desk =
253 HierarchicalLimiter::with_clock_fn(5, 0.0, 2, 10, 0.0, || Box::new(SharedClock(c.clone())));
254
255 let mut sent = 0usize;
256 for round in 0..10 {
257 if desk.try_acquire(round % 2, 1) {
258 sent += 1;
259 }
260 }
261 println!(" strategies offered 10 orders; uplink admitted {sent}");
262 assert_eq!(sent, 5, "the parent caps the desk aggregate at 5");
263}
264
265/// `distributed-backend` feature: an account's venue quota must hold across a
266/// fleet of stateless routers. Both consult the same fixed-window counter (the
267/// Redis INCR + EXPIRE shape), so the account cannot beat the cap by spraying
268/// orders across routers.
269#[cfg(feature = "distributed-backend")]
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}
304
305/// `metrics` feature: cap outbound requests to a market-data vendor and let
306/// the limiter be its own metric source - grant / reject counts, refill
307/// events, live headroom - with no separate observability layer.
308#[cfg(feature = "metrics")]
309fn metered_feed_throttle() {
310 use std::sync::Arc;
311
312 use subms_rate_limiter::{MeteredTokenBucket, TestClock};
313
314 println!("\n== metrics: self-observing market-data throttle ==");
315 let clock = Arc::new(TestClock::new());
316 // 5 requests per burst, refilling 100/sec.
317 let feed = MeteredTokenBucket::with_clock(5, 100.0, Box::new(SharedClock(clock.clone())));
318
319 for _ in 0..8 {
320 feed.try_acquire(1);
321 }
322 let s = feed.snapshot();
323 println!(
324 " granted {}, rejected {}, headroom {}",
325 s.granted, s.rejected, s.available
326 );
327 assert_eq!(s.granted, 5);
328 assert_eq!(s.rejected, 3);
329
330 clock.advance_ms(100); // 100/sec -> +10, capped at 5
331 assert!(feed.try_acquire(1), "the refilled feed admits again");
332 let s2 = feed.snapshot();
333 println!(
334 " after refill: granted {}, refills {}",
335 s2.granted, s2.refills
336 );
337 assert_eq!(s2.granted, 6);
338 assert!(s2.refills >= 1, "a refill step was observed");
339}Trait Implementations§
Auto Trait Implementations§
impl !Freeze for TestClock
impl RefUnwindSafe for TestClock
impl Send for TestClock
impl Sync for TestClock
impl Unpin for TestClock
impl UnsafeUnpin for TestClock
impl UnwindSafe for TestClock
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