Skip to main content

TestClock

Struct TestClock 

Source
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

Source

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}
Source

pub fn with_start(start_ns: u64) -> Self

Source

pub fn advance(&self, ns: u64)

Source

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§

Source§

impl Clock for TestClock

Source§

fn now_ns(&self) -> u64

Nanoseconds since the clock’s origin. Monotonic non-decreasing.
Source§

impl Default for TestClock

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.