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(
127 path: impl AsRef<Path>, capacity: u32, refill_rate_per_sec: u32,
128 ) -> Result<Self, RateLimiterError> {
129 if capacity == 0 || refill_rate_per_sec == 0 {
130 return Err(RateLimiterError::InvalidConfig);
131 }
132 let (file, mmap) = crate::mmf_attach::create_or_attach(
133 path.as_ref(),
134 size_of::<RateLimiterHeader>(),
135 |ptr| unsafe { Self::init_region(ptr, capacity, refill_rate_per_sec) },
136 |ptr| unsafe { (*(ptr as *const RateLimiterHeader)).magic == RATE_LIMITER_MAGIC },
137 )?;
138 Self::from_region(file, mmap, capacity, refill_rate_per_sec)
139 }
140
141 unsafe fn init_region(ptr: *mut u8, capacity: u32, refill_rate_per_sec: u32) {
148 let hdr = ptr as *mut RateLimiterHeader;
149 unsafe {
150 (*hdr).capacity = capacity;
151 (*hdr).refill_rate_per_sec = refill_rate_per_sec;
152 std::ptr::write(
153 &raw mut (*hdr).state,
154 AtomicU64::new(pack_state(capacity, now_us_low())),
155 );
156 std::ptr::write_volatile(&raw mut (*hdr).magic, RATE_LIMITER_MAGIC);
157 }
158 }
159
160 fn from_region(
163 file: File,
164 mmap: MmapMut,
165 capacity: u32,
166 refill_rate_per_sec: u32,
167 ) -> Result<Self, RateLimiterError> {
168 let hdr = unsafe { &*(mmap.as_ptr() as *const RateLimiterHeader) };
169 if hdr.magic != RATE_LIMITER_MAGIC
170 || hdr.capacity != capacity
171 || hdr.refill_rate_per_sec != refill_rate_per_sec
172 {
173 return Err(RateLimiterError::LayoutMismatch);
174 }
175 Ok(Self {
176 _file: file, mmap,
177 header_sidecar: subetha_core::HandshakeHeader::new(),
178 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
179 })
180 }
181
182 pub fn open(
183 path: impl AsRef<Path>, capacity: u32, refill_rate_per_sec: u32,
184 ) -> Result<Self, RateLimiterError> {
185 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
186 let total = size_of::<RateLimiterHeader>();
187 if file.metadata()?.len() < total as u64 {
188 return Err(RateLimiterError::LayoutMismatch);
189 }
190 let mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
191 Self::from_region(file, mmap, capacity, refill_rate_per_sec)
192 }
193
194 fn header(&self) -> &RateLimiterHeader {
195 unsafe { &*(self.mmap.as_ptr() as *const RateLimiterHeader) }
196 }
197
198 #[inline]
199 pub fn capacity(&self) -> u32 { self.header().capacity }
200 #[inline]
201 pub fn refill_rate_per_sec(&self) -> u32 { self.header().refill_rate_per_sec }
202
203 #[inline]
206 fn refill_amount(&self, prev_refill_us: u32, now_us: u32) -> u32 {
207 let elapsed = now_us.wrapping_sub(prev_refill_us) as u64;
209 let rate = self.refill_rate_per_sec() as u64;
210 let refilled = (elapsed * rate) / 1_000_000;
211 refilled.min(u32::MAX as u64) as u32
213 }
214
215 pub fn available(&self) -> u32 {
219 let state = self.header().state.load(Ordering::Acquire);
220 let (tokens, refill_us) = unpack_state(state);
221 let now = now_us_low();
222 let refilled = self.refill_amount(refill_us, now);
223 let cap = self.capacity();
224 let v = (tokens.saturating_add(refilled)).min(cap);
225 self.ring_sidecar
226 .push_op(crate::sidecar_ops::rate_limiter::OP_AVAILABLE, 0);
227 v
228 }
229
230 pub fn try_acquire(&self, n: u32) -> Result<(), RateLimiterError> {
234 loop {
235 let state = self.header().state.load(Ordering::Acquire);
236 let (tokens, refill_us) = unpack_state(state);
237
238 if tokens >= n {
248 let new_state = pack_state(tokens - n, refill_us);
249 if self.header().state.compare_exchange(
250 state, new_state, Ordering::AcqRel, Ordering::Acquire,
251 ).is_ok() {
252 self.ring_sidecar
253 .push_op(crate::sidecar_ops::rate_limiter::OP_TRY_ACQUIRE, 0);
254 return Ok(());
255 }
256 continue; }
258
259 let now = now_us_low();
261 let refilled = self.refill_amount(refill_us, now);
262 let cap = self.capacity();
263 let after_refill = (tokens.saturating_add(refilled)).min(cap);
264 if after_refill < n {
265 self.ring_sidecar
266 .push_op(crate::sidecar_ops::rate_limiter::OP_TRY_ACQUIRE, 1); return Err(RateLimiterError::InsufficientTokens {
268 available: after_refill, requested: n,
269 });
270 }
271 let new_tokens = after_refill - n;
272 let new_state = pack_state(new_tokens, now);
273 if self.header().state.compare_exchange(
274 state, new_state, Ordering::AcqRel, Ordering::Acquire,
275 ).is_ok() {
276 self.ring_sidecar
277 .push_op(crate::sidecar_ops::rate_limiter::OP_TRY_ACQUIRE, 0);
278 return Ok(());
279 }
280 }
282 }
283
284 pub fn acquire_or_wait(
287 &self, n: u32, timeout: Duration,
288 ) -> Result<(), RateLimiterError> {
289 if n > self.capacity() {
290 return Err(RateLimiterError::InsufficientTokens {
291 available: self.capacity(), requested: n,
292 });
293 }
294 let deadline = Instant::now() + timeout;
295 let mut spins = 0u32;
296 loop {
297 match self.try_acquire(n) {
298 Ok(()) => return Ok(()),
299 Err(RateLimiterError::InsufficientTokens { .. }) => {}
300 Err(e) => return Err(e),
301 }
302 if Instant::now() >= deadline {
303 return Err(RateLimiterError::Timeout);
304 }
305 spins += 1;
306 if spins < 32 {
307 std::hint::spin_loop();
308 } else if spins < 256 {
309 std::thread::yield_now();
310 } else {
311 let need = n.saturating_sub(self.available());
313 if need == 0 { continue; }
314 let micros_needed = (need as u64 * 1_000_000) / self.refill_rate_per_sec() as u64;
315 let sleep_us = micros_needed.min(10_000); std::thread::sleep(Duration::from_micros(sleep_us));
317 }
318 }
319 }
320
321 pub fn reset(&self) {
325 self.header().state.store(
326 pack_state(self.capacity(), now_us_low()),
327 Ordering::Release,
328 );
329 }
330
331 pub fn flush(&self) -> Result<(), RateLimiterError> {
332 self.mmap.flush()?;
333 Ok(())
334 }
335
336 pub fn flush_async(&self) -> Result<(), RateLimiterError> {
337 self.mmap.flush_async()?;
338 Ok(())
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345 use std::sync::Arc;
346 use std::thread;
347
348 fn tmp(name: &str) -> std::path::PathBuf {
349 let mut p = std::env::temp_dir();
350 let pid = std::process::id();
351 p.push(format!("subetha-ratelim-{name}-{pid}.bin"));
352 p
353 }
354
355 #[test]
356 fn create_starts_with_full_bucket() {
357 let p = tmp("init");
358 let r = SharedRateLimiter::create(&p, 100, 10).unwrap();
359 assert_eq!(r.capacity(), 100);
360 assert_eq!(r.refill_rate_per_sec(), 10);
361 assert_eq!(r.available(), 100);
362 std::fs::remove_file(&p).ok();
363 }
364
365 #[test]
368 fn second_create_attaches_and_keeps_the_bucket() {
369 let p = tmp("attach");
370 std::fs::remove_file(&p).ok();
371 let r = SharedRateLimiter::create(&p, 100, 1).unwrap();
372 r.try_acquire(40).unwrap();
373
374 let r2 = SharedRateLimiter::create(&p, 100, 1).unwrap();
375 assert_eq!(r2.available(), 60, "attach refilled a live bucket");
376 assert!(matches!(
377 SharedRateLimiter::create(&p, 50, 1),
378 Err(RateLimiterError::LayoutMismatch),
379 ));
380
381 r2.reset();
382 assert_eq!(r.available(), 100, "reset did not refill for every handle");
383 drop(r);
384 drop(r2);
385 std::fs::remove_file(&p).ok();
386 }
387
388 #[test]
389 fn invalid_config_rejected() {
390 let p = tmp("invalid");
391 assert_eq!(
392 SharedRateLimiter::create(&p, 0, 10).err(),
393 Some(RateLimiterError::InvalidConfig)
394 );
395 assert_eq!(
396 SharedRateLimiter::create(&p, 10, 0).err(),
397 Some(RateLimiterError::InvalidConfig)
398 );
399 std::fs::remove_file(&p).ok();
400 }
401
402 #[test]
403 fn try_acquire_deducts_tokens() {
404 let p = tmp("deduct");
405 let r = SharedRateLimiter::create(&p, 100, 1).unwrap(); r.try_acquire(30).unwrap();
407 let avail = r.available();
408 assert!((70..=71).contains(&avail), "after 30-token acquire from cap 100, available={avail} should be ~70");
411 std::fs::remove_file(&p).ok();
412 }
413
414 #[test]
415 fn empty_bucket_rejects() {
416 let p = tmp("empty");
417 let r = SharedRateLimiter::create(&p, 5, 1).unwrap(); r.try_acquire(5).unwrap();
419 match r.try_acquire(1) {
421 Err(RateLimiterError::InsufficientTokens { available, requested }) => {
422 assert!(available < 1);
423 assert_eq!(requested, 1);
424 }
425 other => panic!("expected InsufficientTokens, got {other:?}"),
426 }
427 std::fs::remove_file(&p).ok();
428 }
429
430 #[test]
431 fn refill_scales_with_elapsed_time() {
432 let p = tmp("refill");
433 let r = SharedRateLimiter::create(&p, 100, 1000).unwrap();
435 r.try_acquire(100).unwrap();
437 assert!(r.available() < 5, "after full drain, available should be ~0");
438 thread::sleep(Duration::from_millis(30));
440 let after = r.available();
441 assert!((25..=40).contains(&after),
442 "after 30ms at 1000/s, available={after} should be ~30");
443 std::fs::remove_file(&p).ok();
444 }
445
446 #[test]
447 fn refill_clamped_to_capacity() {
448 let p = tmp("clamp");
449 let r = SharedRateLimiter::create(&p, 50, 10_000).unwrap();
450 r.try_acquire(50).unwrap();
452 thread::sleep(Duration::from_millis(100)); assert_eq!(r.available(), 50, "available should clamp to capacity");
455 std::fs::remove_file(&p).ok();
456 }
457
458 #[test]
459 fn acquire_or_wait_blocks_then_succeeds() {
460 let p = tmp("wait");
461 let r = SharedRateLimiter::create(&p, 1, 100).unwrap();
463 r.try_acquire(1).unwrap();
464 let start = Instant::now();
465 r.acquire_or_wait(1, Duration::from_millis(500)).unwrap();
467 let elapsed = start.elapsed();
468 assert!(elapsed >= Duration::from_millis(5),
469 "should have waited some time, got {elapsed:?}");
470 assert!(elapsed < Duration::from_millis(100),
471 "should have completed quickly, got {elapsed:?}");
472 std::fs::remove_file(&p).ok();
473 }
474
475 #[test]
476 fn acquire_or_wait_returns_timeout() {
477 let p = tmp("timeout");
478 let r = SharedRateLimiter::create(&p, 1, 1).unwrap(); r.try_acquire(1).unwrap();
480 let start = Instant::now();
481 let result = r.acquire_or_wait(1, Duration::from_millis(50));
482 let elapsed = start.elapsed();
483 assert!(matches!(result, Err(RateLimiterError::Timeout)));
484 assert!(elapsed >= Duration::from_millis(40),
485 "should have waited ~50ms, got {elapsed:?}");
486 std::fs::remove_file(&p).ok();
487 }
488
489 #[test]
490 fn acquire_or_wait_oversized_request_fails_fast() {
491 let p = tmp("oversize");
492 let r = SharedRateLimiter::create(&p, 10, 100).unwrap();
493 let result = r.acquire_or_wait(100, Duration::from_secs(10));
495 assert!(matches!(result, Err(RateLimiterError::InsufficientTokens { .. })));
496 std::fs::remove_file(&p).ok();
497 }
498
499 #[test]
500 fn concurrent_acquirers_sum_to_at_most_capacity_no_refill() {
501 let p = tmp("concurrent");
502 let r = Arc::new(SharedRateLimiter::create(&p, 100, 1).unwrap());
504 let n_threads = 8;
505 let per_thread = 50;
506 let mut handles = vec![];
507 for _ in 0..n_threads {
508 let r = r.clone();
509 handles.push(thread::spawn(move || {
510 let mut acquired = 0u32;
511 for _ in 0..per_thread {
512 if r.try_acquire(1).is_ok() { acquired += 1; }
513 }
514 acquired
515 }));
516 }
517 let total: u32 = handles.into_iter()
518 .map(|h| h.join().unwrap()).sum();
519 assert!(total <= 101, "total acquired {total} should not exceed capacity {} + tiny refill", 100);
522 assert!(total >= 95, "total acquired {total} should be near capacity 100");
524 std::fs::remove_file(&p).ok();
525 }
526
527 #[test]
528 fn cross_handle_state_shared() {
529 let p = tmp("cross-handle");
530 let writer = SharedRateLimiter::create(&p, 100, 10).unwrap();
531 let reader = SharedRateLimiter::open(&p, 100, 10).unwrap();
532 writer.try_acquire(40).unwrap();
533 let avail = reader.available();
534 assert!((59..=60).contains(&avail));
535 std::fs::remove_file(&p).ok();
536 }
537
538 #[test]
539 fn config_mismatch_at_open_rejected() {
540 let p = tmp("mismatch");
541 let _w = SharedRateLimiter::create(&p, 100, 10).unwrap();
542 assert!(matches!(
543 SharedRateLimiter::open(&p, 50, 10),
544 Err(RateLimiterError::LayoutMismatch)
545 ));
546 assert!(matches!(
547 SharedRateLimiter::open(&p, 100, 20),
548 Err(RateLimiterError::LayoutMismatch)
549 ));
550 std::fs::remove_file(&p).ok();
551 }
552
553 #[test]
554 fn reset_refills_to_capacity() {
555 let p = tmp("reset");
556 let r = SharedRateLimiter::create(&p, 50, 1).unwrap();
557 r.try_acquire(50).unwrap();
558 assert!(r.available() < 2);
559 r.reset();
560 assert_eq!(r.available(), 50);
561 std::fs::remove_file(&p).ok();
562 }
563
564 #[test]
565 fn disk_persistence_survives_reopen() {
566 let p = tmp("disk");
567 {
568 let r = SharedRateLimiter::create(&p, 50, 1).unwrap();
569 r.try_acquire(20).unwrap();
570 r.flush().unwrap();
571 }
572 let r2 = SharedRateLimiter::open(&p, 50, 1).unwrap();
573 let avail = r2.available();
574 assert!((30..=31).contains(&avail),
576 "after reopen available={avail} should be ~30");
577 std::fs::remove_file(&p).ok();
578 }
579
580 #[test]
581 fn acquire_zero_is_noop() {
582 let p = tmp("zero");
583 let r = SharedRateLimiter::create(&p, 100, 1).unwrap();
584 let before = r.available();
585 r.try_acquire(0).unwrap();
586 let after = r.available();
587 assert!(after >= before);
590 std::fs::remove_file(&p).ok();
591 }
592}