1use std::fs::{File, OpenOptions};
47use std::mem::size_of;
48use std::path::Path;
49use std::sync::atomic::{AtomicU64, Ordering};
50use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
51
52use memmap2::{MmapMut, MmapOptions};
53
54pub const RATE_LIMITER_MAGIC: u64 = 0x4150_5246_4C4D_5452;
55
56#[repr(C, align(64))]
57pub struct RateLimiterHeader {
58 pub magic: u64,
59 pub capacity: u32,
60 pub refill_rate_per_sec: u32,
61 pub state: AtomicU64,
62 _pad: [u8; 40],
63}
64
65const _: () = {
66 assert!(size_of::<RateLimiterHeader>() == 64);
67};
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum RateLimiterError {
71 InsufficientTokens { available: u32, requested: u32 },
72 Timeout,
73 InvalidConfig,
74 LayoutMismatch,
75 IoError(std::io::ErrorKind),
76}
77
78impl From<std::io::Error> for RateLimiterError {
79 fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
80}
81
82#[inline]
83fn now_us_low() -> u32 {
84 let micros = SystemTime::now()
85 .duration_since(UNIX_EPOCH)
86 .map(|d| d.as_micros() as u64)
87 .unwrap_or(0);
88 micros as u32
89}
90
91#[inline]
92fn pack_state(tokens: u32, refill_us_low: u32) -> u64 {
93 ((refill_us_low as u64) << 32) | (tokens as u64)
94}
95#[inline]
96fn unpack_state(state: u64) -> (u32, u32) {
97 (state as u32, (state >> 32) as u32)
98}
99
100pub struct SharedRateLimiter {
101 _file: File,
102 mmap: MmapMut,
103 header_sidecar: subetha_core::HandshakeHeader,
104 ring_sidecar: Box<subetha_core::ObservationRing>,
105}
106
107unsafe impl Send for SharedRateLimiter {}
108unsafe impl Sync for SharedRateLimiter {}
109
110impl subetha_sidecar::AdaptiveInstance for SharedRateLimiter {
111 fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
112 fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
113 fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
114 Box::new(subetha_sidecar::NoMigrationPolicy)
115 }
116}
117
118impl SharedRateLimiter {
119 pub fn create(
123 path: impl AsRef<Path>, capacity: u32, refill_rate_per_sec: u32,
124 ) -> Result<Self, RateLimiterError> {
125 if capacity == 0 || refill_rate_per_sec == 0 {
126 return Err(RateLimiterError::InvalidConfig);
127 }
128 let total = size_of::<RateLimiterHeader>();
129 let file = OpenOptions::new()
130 .read(true).write(true).create(true).truncate(true)
131 .open(path.as_ref())?;
132 file.set_len(total as u64)?;
133 let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
134 let hdr = mmap.as_mut_ptr() as *mut RateLimiterHeader;
135 unsafe {
136 std::ptr::write_bytes(hdr as *mut u8, 0, total);
137 (*hdr).magic = RATE_LIMITER_MAGIC;
138 (*hdr).capacity = capacity;
139 (*hdr).refill_rate_per_sec = refill_rate_per_sec;
140 (*hdr).state.store(
141 pack_state(capacity, now_us_low()),
142 Ordering::Release,
143 );
144 }
145 Ok(Self {
146 _file: file, mmap,
147 header_sidecar: subetha_core::HandshakeHeader::new(),
148 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
149 })
150 }
151
152 pub fn open(
153 path: impl AsRef<Path>, capacity: u32, refill_rate_per_sec: u32,
154 ) -> Result<Self, RateLimiterError> {
155 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
156 let total = size_of::<RateLimiterHeader>();
157 if file.metadata()?.len() < total as u64 {
158 return Err(RateLimiterError::LayoutMismatch);
159 }
160 let mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
161 let hdr = unsafe { &*(mmap.as_ptr() as *const RateLimiterHeader) };
162 if hdr.magic != RATE_LIMITER_MAGIC
163 || hdr.capacity != capacity
164 || hdr.refill_rate_per_sec != refill_rate_per_sec
165 {
166 return Err(RateLimiterError::LayoutMismatch);
167 }
168 Ok(Self {
169 _file: file, mmap,
170 header_sidecar: subetha_core::HandshakeHeader::new(),
171 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
172 })
173 }
174
175 fn header(&self) -> &RateLimiterHeader {
176 unsafe { &*(self.mmap.as_ptr() as *const RateLimiterHeader) }
177 }
178
179 #[inline]
180 pub fn capacity(&self) -> u32 { self.header().capacity }
181 #[inline]
182 pub fn refill_rate_per_sec(&self) -> u32 { self.header().refill_rate_per_sec }
183
184 #[inline]
187 fn refill_amount(&self, prev_refill_us: u32, now_us: u32) -> u32 {
188 let elapsed = now_us.wrapping_sub(prev_refill_us) as u64;
190 let rate = self.refill_rate_per_sec() as u64;
191 let refilled = (elapsed * rate) / 1_000_000;
192 refilled.min(u32::MAX as u64) as u32
194 }
195
196 pub fn available(&self) -> u32 {
200 let state = self.header().state.load(Ordering::Acquire);
201 let (tokens, refill_us) = unpack_state(state);
202 let now = now_us_low();
203 let refilled = self.refill_amount(refill_us, now);
204 let cap = self.capacity();
205 let v = (tokens.saturating_add(refilled)).min(cap);
206 self.ring_sidecar
207 .push_op(crate::sidecar_ops::rate_limiter::OP_AVAILABLE, 0);
208 v
209 }
210
211 pub fn try_acquire(&self, n: u32) -> Result<(), RateLimiterError> {
215 loop {
216 let state = self.header().state.load(Ordering::Acquire);
217 let (tokens, refill_us) = unpack_state(state);
218
219 if tokens >= n {
229 let new_state = pack_state(tokens - n, refill_us);
230 if self.header().state.compare_exchange(
231 state, new_state, Ordering::AcqRel, Ordering::Acquire,
232 ).is_ok() {
233 self.ring_sidecar
234 .push_op(crate::sidecar_ops::rate_limiter::OP_TRY_ACQUIRE, 0);
235 return Ok(());
236 }
237 continue; }
239
240 let now = now_us_low();
242 let refilled = self.refill_amount(refill_us, now);
243 let cap = self.capacity();
244 let after_refill = (tokens.saturating_add(refilled)).min(cap);
245 if after_refill < n {
246 self.ring_sidecar
247 .push_op(crate::sidecar_ops::rate_limiter::OP_TRY_ACQUIRE, 1); return Err(RateLimiterError::InsufficientTokens {
249 available: after_refill, requested: n,
250 });
251 }
252 let new_tokens = after_refill - n;
253 let new_state = pack_state(new_tokens, now);
254 if self.header().state.compare_exchange(
255 state, new_state, Ordering::AcqRel, Ordering::Acquire,
256 ).is_ok() {
257 self.ring_sidecar
258 .push_op(crate::sidecar_ops::rate_limiter::OP_TRY_ACQUIRE, 0);
259 return Ok(());
260 }
261 }
263 }
264
265 pub fn acquire_or_wait(
268 &self, n: u32, timeout: Duration,
269 ) -> Result<(), RateLimiterError> {
270 if n > self.capacity() {
271 return Err(RateLimiterError::InsufficientTokens {
272 available: self.capacity(), requested: n,
273 });
274 }
275 let deadline = Instant::now() + timeout;
276 let mut spins = 0u32;
277 loop {
278 match self.try_acquire(n) {
279 Ok(()) => return Ok(()),
280 Err(RateLimiterError::InsufficientTokens { .. }) => {}
281 Err(e) => return Err(e),
282 }
283 if Instant::now() >= deadline {
284 return Err(RateLimiterError::Timeout);
285 }
286 spins += 1;
287 if spins < 32 {
288 std::hint::spin_loop();
289 } else if spins < 256 {
290 std::thread::yield_now();
291 } else {
292 let need = n.saturating_sub(self.available());
294 if need == 0 { continue; }
295 let micros_needed = (need as u64 * 1_000_000) / self.refill_rate_per_sec() as u64;
296 let sleep_us = micros_needed.min(10_000); std::thread::sleep(Duration::from_micros(sleep_us));
298 }
299 }
300 }
301
302 pub fn reset(&self) {
306 self.header().state.store(
307 pack_state(self.capacity(), now_us_low()),
308 Ordering::Release,
309 );
310 }
311
312 pub fn flush(&self) -> Result<(), RateLimiterError> {
313 self.mmap.flush()?;
314 Ok(())
315 }
316
317 pub fn flush_async(&self) -> Result<(), RateLimiterError> {
318 self.mmap.flush_async()?;
319 Ok(())
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use std::sync::Arc;
327 use std::thread;
328
329 fn tmp(name: &str) -> std::path::PathBuf {
330 let mut p = std::env::temp_dir();
331 let pid = std::process::id();
332 p.push(format!("subetha-ratelim-{name}-{pid}.bin"));
333 p
334 }
335
336 #[test]
337 fn create_starts_with_full_bucket() {
338 let p = tmp("init");
339 let r = SharedRateLimiter::create(&p, 100, 10).unwrap();
340 assert_eq!(r.capacity(), 100);
341 assert_eq!(r.refill_rate_per_sec(), 10);
342 assert_eq!(r.available(), 100);
343 std::fs::remove_file(&p).ok();
344 }
345
346 #[test]
347 fn invalid_config_rejected() {
348 let p = tmp("invalid");
349 assert_eq!(
350 SharedRateLimiter::create(&p, 0, 10).err(),
351 Some(RateLimiterError::InvalidConfig)
352 );
353 assert_eq!(
354 SharedRateLimiter::create(&p, 10, 0).err(),
355 Some(RateLimiterError::InvalidConfig)
356 );
357 std::fs::remove_file(&p).ok();
358 }
359
360 #[test]
361 fn try_acquire_deducts_tokens() {
362 let p = tmp("deduct");
363 let r = SharedRateLimiter::create(&p, 100, 1).unwrap(); r.try_acquire(30).unwrap();
365 let avail = r.available();
366 assert!((70..=71).contains(&avail), "after 30-token acquire from cap 100, available={avail} should be ~70");
369 std::fs::remove_file(&p).ok();
370 }
371
372 #[test]
373 fn empty_bucket_rejects() {
374 let p = tmp("empty");
375 let r = SharedRateLimiter::create(&p, 5, 1).unwrap(); r.try_acquire(5).unwrap();
377 match r.try_acquire(1) {
379 Err(RateLimiterError::InsufficientTokens { available, requested }) => {
380 assert!(available < 1);
381 assert_eq!(requested, 1);
382 }
383 other => panic!("expected InsufficientTokens, got {other:?}"),
384 }
385 std::fs::remove_file(&p).ok();
386 }
387
388 #[test]
389 fn refill_scales_with_elapsed_time() {
390 let p = tmp("refill");
391 let r = SharedRateLimiter::create(&p, 100, 1000).unwrap();
393 r.try_acquire(100).unwrap();
395 assert!(r.available() < 5, "after full drain, available should be ~0");
396 thread::sleep(Duration::from_millis(30));
398 let after = r.available();
399 assert!((25..=40).contains(&after),
400 "after 30ms at 1000/s, available={after} should be ~30");
401 std::fs::remove_file(&p).ok();
402 }
403
404 #[test]
405 fn refill_clamped_to_capacity() {
406 let p = tmp("clamp");
407 let r = SharedRateLimiter::create(&p, 50, 10_000).unwrap();
408 r.try_acquire(50).unwrap();
410 thread::sleep(Duration::from_millis(100)); assert_eq!(r.available(), 50, "available should clamp to capacity");
413 std::fs::remove_file(&p).ok();
414 }
415
416 #[test]
417 fn acquire_or_wait_blocks_then_succeeds() {
418 let p = tmp("wait");
419 let r = SharedRateLimiter::create(&p, 1, 100).unwrap();
421 r.try_acquire(1).unwrap();
422 let start = Instant::now();
423 r.acquire_or_wait(1, Duration::from_millis(500)).unwrap();
425 let elapsed = start.elapsed();
426 assert!(elapsed >= Duration::from_millis(5),
427 "should have waited some time, got {elapsed:?}");
428 assert!(elapsed < Duration::from_millis(100),
429 "should have completed quickly, got {elapsed:?}");
430 std::fs::remove_file(&p).ok();
431 }
432
433 #[test]
434 fn acquire_or_wait_returns_timeout() {
435 let p = tmp("timeout");
436 let r = SharedRateLimiter::create(&p, 1, 1).unwrap(); r.try_acquire(1).unwrap();
438 let start = Instant::now();
439 let result = r.acquire_or_wait(1, Duration::from_millis(50));
440 let elapsed = start.elapsed();
441 assert!(matches!(result, Err(RateLimiterError::Timeout)));
442 assert!(elapsed >= Duration::from_millis(40),
443 "should have waited ~50ms, got {elapsed:?}");
444 std::fs::remove_file(&p).ok();
445 }
446
447 #[test]
448 fn acquire_or_wait_oversized_request_fails_fast() {
449 let p = tmp("oversize");
450 let r = SharedRateLimiter::create(&p, 10, 100).unwrap();
451 let result = r.acquire_or_wait(100, Duration::from_secs(10));
453 assert!(matches!(result, Err(RateLimiterError::InsufficientTokens { .. })));
454 std::fs::remove_file(&p).ok();
455 }
456
457 #[test]
458 fn concurrent_acquirers_sum_to_at_most_capacity_no_refill() {
459 let p = tmp("concurrent");
460 let r = Arc::new(SharedRateLimiter::create(&p, 100, 1).unwrap());
462 let n_threads = 8;
463 let per_thread = 50;
464 let mut handles = vec![];
465 for _ in 0..n_threads {
466 let r = r.clone();
467 handles.push(thread::spawn(move || {
468 let mut acquired = 0u32;
469 for _ in 0..per_thread {
470 if r.try_acquire(1).is_ok() { acquired += 1; }
471 }
472 acquired
473 }));
474 }
475 let total: u32 = handles.into_iter()
476 .map(|h| h.join().unwrap()).sum();
477 assert!(total <= 101, "total acquired {total} should not exceed capacity {} + tiny refill", 100);
480 assert!(total >= 95, "total acquired {total} should be near capacity 100");
482 std::fs::remove_file(&p).ok();
483 }
484
485 #[test]
486 fn cross_handle_state_shared() {
487 let p = tmp("cross-handle");
488 let writer = SharedRateLimiter::create(&p, 100, 10).unwrap();
489 let reader = SharedRateLimiter::open(&p, 100, 10).unwrap();
490 writer.try_acquire(40).unwrap();
491 let avail = reader.available();
492 assert!((59..=60).contains(&avail));
493 std::fs::remove_file(&p).ok();
494 }
495
496 #[test]
497 fn config_mismatch_at_open_rejected() {
498 let p = tmp("mismatch");
499 let _w = SharedRateLimiter::create(&p, 100, 10).unwrap();
500 assert!(matches!(
501 SharedRateLimiter::open(&p, 50, 10),
502 Err(RateLimiterError::LayoutMismatch)
503 ));
504 assert!(matches!(
505 SharedRateLimiter::open(&p, 100, 20),
506 Err(RateLimiterError::LayoutMismatch)
507 ));
508 std::fs::remove_file(&p).ok();
509 }
510
511 #[test]
512 fn reset_refills_to_capacity() {
513 let p = tmp("reset");
514 let r = SharedRateLimiter::create(&p, 50, 1).unwrap();
515 r.try_acquire(50).unwrap();
516 assert!(r.available() < 2);
517 r.reset();
518 assert_eq!(r.available(), 50);
519 std::fs::remove_file(&p).ok();
520 }
521
522 #[test]
523 fn disk_persistence_survives_reopen() {
524 let p = tmp("disk");
525 {
526 let r = SharedRateLimiter::create(&p, 50, 1).unwrap();
527 r.try_acquire(20).unwrap();
528 r.flush().unwrap();
529 }
530 let r2 = SharedRateLimiter::open(&p, 50, 1).unwrap();
531 let avail = r2.available();
532 assert!((30..=31).contains(&avail),
534 "after reopen available={avail} should be ~30");
535 std::fs::remove_file(&p).ok();
536 }
537
538 #[test]
539 fn acquire_zero_is_noop() {
540 let p = tmp("zero");
541 let r = SharedRateLimiter::create(&p, 100, 1).unwrap();
542 let before = r.available();
543 r.try_acquire(0).unwrap();
544 let after = r.available();
545 assert!(after >= before);
548 std::fs::remove_file(&p).ok();
549 }
550}