Skip to content

Commit eb3c39b

Browse files
ekumpgyuheon0h
andauthored
chore(ci): mock now function for rate limiter in tests to make them deterministic (#1842)
# What does this PR do? Makes the rate_limiter tests deterministic by mocking `now()`. # Motivation flakiness # Additional Notes Anything else we should know when reviewing? # How to test the change? Describe here in detail how the change can be validated. Co-authored-by: gyuheon0h <gyuheon.oh@datadoghq.com>
1 parent 8f94c91 commit eb3c39b

1 file changed

Lines changed: 71 additions & 33 deletions

File tree

libdd-common/src/rate_limiter.rs

Lines changed: 71 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,20 @@ pub struct LocalLimiter {
3030

3131
const 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+
3339
fn 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)]
146159
mod 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

Comments
 (0)