@@ -30,7 +30,20 @@ pub struct LocalLimiter {
3030
3131const TIME_PER_SECOND : i64 = 1_000_000_000 ; // nanoseconds
3232
33+ /// When set to a non-zero value, `now()` returns this instead of the real clock.
34+ /// This allows tests to control time deterministically, avoiding flakiness from
35+ /// wall-clock timing on CI machines.
36+ #[ cfg( test) ]
37+ static MOCK_NOW : AtomicU64 = AtomicU64 :: new ( 0 ) ;
38+
3339fn now ( ) -> u64 {
40+ #[ cfg( test) ]
41+ {
42+ let mock = MOCK_NOW . load ( Ordering :: Relaxed ) ;
43+ if mock != 0 {
44+ return mock;
45+ }
46+ }
3447 #[ cfg( windows) ]
3548 let now = unsafe {
3649 static FREQUENCY : AtomicU64 = AtomicU64 :: new ( 0 ) ;
@@ -144,64 +157,89 @@ impl Limiter for LocalLimiter {
144157
145158#[ cfg( test) ]
146159mod tests {
147- use crate :: rate_limiter:: { Limiter , LocalLimiter , TIME_PER_SECOND } ;
160+ use crate :: rate_limiter:: { now , Limiter , LocalLimiter , MOCK_NOW , TIME_PER_SECOND } ;
148161 use std:: sync:: atomic:: Ordering ;
149- use std:: thread:: sleep;
150- use std:: time:: Duration ;
162+
163+ fn set_mock_time ( nanos : u64 ) {
164+ MOCK_NOW . store ( nanos, Ordering :: Relaxed ) ;
165+ }
166+
167+ fn advance_mock_time ( nanos : u64 ) {
168+ MOCK_NOW . fetch_add ( nanos, Ordering :: Relaxed ) ;
169+ }
170+
171+ /// A small time tick (100ns) used to simulate minimal time passing between operations.
172+ const TICK : u64 = 100 ;
151173
152174 #[ test]
153175 #[ cfg_attr( miri, ignore) ]
154176 fn test_rate_limiter ( ) {
177+ // Use mock time for deterministic behavior — real wall-clock sleeps are flaky on CI.
178+ set_mock_time ( 1_000_000_000 ) ;
179+
155180 let limiter = LocalLimiter :: default ( ) ;
156- // Two are allowed, then one more because a small amount of time passed since the first one
181+
182+ // First inc uses 1 of 2 slots: rate is exactly 0.5
157183 assert ! ( limiter. inc( 2 ) ) ;
158- // Work around floating point precision issues
159- assert ! ( limiter . rate ( ) > 0.49999 && limiter . rate ( ) <= 0.5 ) ;
160- // Add a minimal amount of time to ensure the test doesn't run faster than timer precision
161- sleep ( Duration :: from_micros ( 100 ) ) ;
184+ assert_eq ! ( 0.5 , limiter . rate ( ) ) ;
185+
186+ // Second inc: rate approaches 1.0 but not quite (tiny time elapsed)
187+ advance_mock_time ( TICK ) ;
162188 assert ! ( limiter. inc( 2 ) ) ;
163- // We're close to 1, but not quite, due to the minimal time passed
164189 assert ! ( limiter. rate( ) > 0.5 && limiter. rate( ) < 1. ) ;
165- sleep ( Duration :: from_micros ( 100 ) ) ;
190+
191+ // Third inc fills the bucket: rate clamps to 1.0
192+ advance_mock_time ( TICK ) ;
166193 assert ! ( limiter. inc( 2 ) ) ;
167- // Rate capped at 1
168194 assert_eq ! ( 1. , limiter. rate( ) ) ;
169- sleep ( Duration :: from_micros ( 100 ) ) ;
195+
196+ // Over limit — both rejected
197+ advance_mock_time ( TICK ) ;
170198 assert ! ( !limiter. inc( 2 ) ) ;
171- sleep ( Duration :: from_micros ( 100 ) ) ;
199+ advance_mock_time ( TICK ) ;
172200 assert ! ( !limiter. inc( 2 ) ) ;
173- sleep ( Duration :: from_micros ( 100 ) ) ;
174201
175- // reduce 4 times, we're going into negative territory. Next increment will reset to zero.
176- limiter
177- . last_update
178- . fetch_sub ( 3 * TIME_PER_SECOND as u64 , Ordering :: Relaxed ) ;
202+ // 3 seconds pass — capacity fully refills, hit count goes negative then resets to zero
203+ advance_mock_time ( 3 * TIME_PER_SECOND as u64 ) ;
179204 assert ! ( limiter. inc( 2 ) ) ;
180- // Work around floating point precision issues
181- assert ! ( limiter . rate ( ) > 0.49999 && limiter . rate ( ) <= 0.5 ) ; // We're starting from scratch
182- sleep ( Duration :: from_micros ( 100 ) ) ;
205+ assert_eq ! ( 0.5 , limiter . rate ( ) ) ; // Starting from scratch
206+
207+ advance_mock_time ( TICK ) ;
183208 assert ! ( limiter. inc( 2 ) ) ;
184- sleep ( Duration :: from_micros ( 100 ) ) ;
209+ advance_mock_time ( TICK ) ;
185210 assert ! ( limiter. inc( 2 ) ) ;
186- sleep ( Duration :: from_micros ( 100 ) ) ;
211+ advance_mock_time ( TICK ) ;
187212 assert ! ( !limiter. inc( 2 ) ) ;
188- sleep ( Duration :: from_micros ( 100 ) ) ;
189213
190- // Test change to higher value
214+ // Test change to higher limit
215+ advance_mock_time ( TICK ) ;
191216 assert ! ( limiter. inc( 3 ) ) ;
192- sleep ( Duration :: from_micros ( 100 ) ) ;
217+ advance_mock_time ( TICK ) ;
193218 assert ! ( !limiter. inc( 3 ) ) ;
194219
195- // Then change to lower value - but we have no capacity
220+ // Change to lower limit — no capacity available
196221 assert ! ( !limiter. inc( 1 ) ) ;
197222
198- // The counter is around 4 (because last limit was 3)
199- // We're keeping the highest successful limit stored, thus subtracting 3 twice will reset it
200- limiter
201- . last_update
202- . fetch_sub ( 2 * TIME_PER_SECOND as u64 , Ordering :: Relaxed ) ;
223+ // 2 seconds pass — the counter resets (last successful limit was 3, so subtracting
224+ // 3 per second twice clears it)
225+ advance_mock_time ( 2 * TIME_PER_SECOND as u64 ) ;
203226
204- // And now 1 succeeds again.
227+ // Now 1 succeeds again
205228 assert ! ( limiter. inc( 1 ) ) ;
229+
230+ set_mock_time ( 0 ) ;
231+ }
232+
233+ /// Validates the real clock implementation (MOCK_NOW is 0, so `now()` hits the actual
234+ /// platform clock).
235+ // We normally shouldn't test private functions directly, but is necessary here since
236+ // now() is mocked for the other tests.
237+ #[ test]
238+ #[ cfg_attr( miri, ignore) ]
239+ fn test_now_monotonic ( ) {
240+ let t1 = now ( ) ;
241+ assert ! ( t1 > 0 ) ;
242+ let t2 = now ( ) ;
243+ assert ! ( t2 >= t1) ;
206244 }
207245}
0 commit comments