Skip to content

Commit 50e0c11

Browse files
committed
feat: add wrap-around and time-based iteration support
Iterator cycling features for workloads exceeding keyspace size: - Add duration_ms to SamplingConfig for time-based iteration limits - RandomIter cycles when limit > range_size (e.g., 1M requests on 100K keys) - SequentialIter auto-enables wrap_around when duration_ms is set - FilterContext tracks start_time_ms and checks both count and duration limits New tests for multi-threaded wrap-around behavior: - test_random_cycling_limit_exceeds_keyspace - test_random_cycling_multithreaded - test_random_duration_based_cycling / _multithreaded - test_sequential_duration_based / _multithreaded - test_partitioned_with_limit / _multithreaded_disjoint - test_delete_iter_single_pass - test_limit_with_duration_limit_wins - test_duration_with_limit_duration_wins Documentation updates with examples for: - Wrap-around mode (.wrap_around()) - Cycling with limit > keyspace - Time-based iteration (duration_ms)
1 parent 713fa2d commit 50e0c11

3 files changed

Lines changed: 618 additions & 15 deletions

File tree

src/config.rs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ pub struct SamplingConfig {
6868
/// Maximum number of items to return. None = unlimited.
6969
pub limit: Option<u64>,
7070

71+
/// Duration limit for iteration (in milliseconds). None = unlimited.
72+
/// When set, iteration continues until the duration expires.
73+
/// Combines with `limit` - iteration stops when either is reached.
74+
pub duration_ms: Option<u64>,
75+
7176
/// Probabilistic sampling ratio (0.0-1.0).
7277
/// Each matching item has this probability of being returned.
7378
/// 1.0 = return all matching, 0.5 = return ~50% of matching.
@@ -92,6 +97,7 @@ impl SamplingConfig {
9297
Self {
9398
set_ratio: None,
9499
limit: None,
100+
duration_ms: None,
95101
sample_probability: 1.0,
96102
seed: None,
97103
distribution: AccessDistribution::Uniform,
@@ -117,6 +123,16 @@ impl SamplingConfig {
117123
self
118124
}
119125

126+
/// Set duration limit for iteration (in milliseconds).
127+
///
128+
/// Iteration will continue (with wraparound) until the duration expires.
129+
/// This enables time-based workloads like "run for 60 seconds".
130+
#[inline]
131+
pub const fn with_duration_ms(mut self, duration_ms: u64) -> Self {
132+
self.duration_ms = Some(duration_ms);
133+
self
134+
}
135+
120136
/// Set probabilistic sampling (each item has `prob` chance of being returned).
121137
///
122138
/// Use this to randomly sample a percentage of the keyspace.
@@ -164,6 +180,7 @@ impl SamplingConfig {
164180
#[inline]
165181
pub fn has_sampling(&self) -> bool {
166182
self.limit.is_some()
183+
|| self.duration_ms.is_some()
167184
|| self.sample_probability < 1.0
168185
|| self.set_ratio.is_some()
169186
|| !matches!(self.distribution, AccessDistribution::Uniform)
@@ -224,28 +241,52 @@ pub struct FilterContext {
224241
pub(crate) sampling: SamplingConfig,
225242
rng: fastrand::Rng,
226243
yielded: u64,
244+
/// Start time for duration-based iteration (milliseconds since UNIX epoch).
245+
start_time_ms: Option<u64>,
227246
}
228247

229248
impl FilterContext {
230249
/// Create a new filter context.
231250
#[inline]
232251
pub fn new(filter: BitFilter, sampling: SamplingConfig) -> Self {
233252
let rng = sampling.make_rng();
253+
let start_time_ms = sampling.duration_ms.map(|_| Self::current_time_ms());
234254
Self {
235255
filter,
236256
sampling,
237257
rng,
238258
yielded: 0,
259+
start_time_ms,
239260
}
240261
}
241262

242-
/// Check if we should continue iterating (respects limit).
263+
/// Get current time in milliseconds since UNIX epoch.
264+
#[inline]
265+
fn current_time_ms() -> u64 {
266+
use std::time::{SystemTime, UNIX_EPOCH};
267+
SystemTime::now()
268+
.duration_since(UNIX_EPOCH)
269+
.map(|d| d.as_millis() as u64)
270+
.unwrap_or(0)
271+
}
272+
273+
/// Check if we should continue iterating (respects limit and duration).
243274
#[inline]
244275
pub fn check_limit(&self) -> bool {
245-
match self.sampling.limit {
246-
Some(limit) => self.yielded < limit,
247-
None => true,
276+
// Check count limit
277+
if let Some(limit) = self.sampling.limit {
278+
if self.yielded >= limit {
279+
return false;
280+
}
248281
}
282+
// Check duration limit
283+
if let (Some(duration_ms), Some(start_time_ms)) = (self.sampling.duration_ms, self.start_time_ms) {
284+
let elapsed = Self::current_time_ms().saturating_sub(start_time_ms);
285+
if elapsed >= duration_ms {
286+
return false;
287+
}
288+
}
289+
true
249290
}
250291

251292
/// Check if item matches filter (with mixed-ratio support).

0 commit comments

Comments
 (0)