Skip to main content

nautilus_common/
throttler.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Message throttling and rate limiting functionality.
17//!
18//! This module provides throttling capabilities to control the rate of message processing
19//! and prevent system overload. The throttler can buffer, drop, or delay messages based
20//! on configured rate limits and time intervals.
21
22use std::{
23    any::Any,
24    cell::{RefCell, UnsafeCell},
25    collections::VecDeque,
26    fmt::Debug,
27    marker::PhantomData,
28    num::{NonZeroU64, NonZeroUsize},
29    rc::Rc,
30};
31
32use nautilus_core::{UnixNanos, correctness::FAILED};
33use serde::{Deserialize, Serialize};
34use ustr::Ustr;
35
36use crate::{
37    actor::{
38        Actor,
39        registry::{register_actor, try_get_actor_unchecked, with_actor_registry},
40    },
41    clock::Clock,
42    msgbus::{self, Endpoint, Handler, MStr, ShareableMessageHandler},
43    timer::{TimeEvent, TimeEventCallback},
44};
45
46const MAX_INITIAL_TIMESTAMPS_CAPACITY: usize = 1024;
47
48/// Represents a throttling limit per interval.
49///
50/// The non-zero field types make a degenerate rate limit unrepresentable: a zero `limit`
51/// underflows the throttler's `limit - 1` indexing, and a zero `interval_ns` disables
52/// throttling entirely.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
54#[serde(deny_unknown_fields)]
55pub struct RateLimit {
56    limit: NonZeroUsize,
57    interval_ns: NonZeroU64,
58}
59
60impl RateLimit {
61    /// Creates a new [`RateLimit`] instance with correctness checking.
62    ///
63    /// # Errors
64    ///
65    /// Returns an error if `limit` or `interval_ns` is zero.
66    pub fn new_checked(limit: usize, interval_ns: u64) -> anyhow::Result<Self> {
67        let limit = NonZeroUsize::new(limit)
68            .ok_or_else(|| anyhow::anyhow!("Invalid limit: {limit} (must be non-zero)"))?;
69        let interval_ns = NonZeroU64::new(interval_ns).ok_or_else(|| {
70            anyhow::anyhow!("Invalid interval_ns: {interval_ns} (must be non-zero)")
71        })?;
72        Ok(Self { limit, interval_ns })
73    }
74
75    /// Creates a new [`RateLimit`] instance.
76    ///
77    /// # Panics
78    ///
79    /// Panics if `limit` or `interval_ns` is zero.
80    #[must_use]
81    pub fn new(limit: usize, interval_ns: u64) -> Self {
82        Self::new_checked(limit, interval_ns).expect(FAILED)
83    }
84
85    /// Maximum number of messages that can be sent within the interval.
86    #[must_use]
87    pub const fn limit(&self) -> usize {
88        self.limit.get()
89    }
90
91    /// Interval between messages in nanoseconds.
92    #[must_use]
93    pub const fn interval_ns(&self) -> u64 {
94        self.interval_ns.get()
95    }
96}
97
98/// Throttler rate limits messages by dropping or buffering them.
99///
100/// Throttler takes messages of type T and callback of type F for dropping
101/// or processing messages.
102///
103/// The throttler stores its limit and interval as non-zero values from
104/// [`RateLimit`]. Internal counters, buffers, and timer state stay private so
105/// callers can observe state without breaking rate-limit invariants.
106///
107/// # Callback contract
108///
109/// The `output_send` and `output_drop` callbacks are invoked inline from
110/// [`Throttler::send`] and the drain handler. They must not reenter the
111/// throttler (for example by calling `send` synchronously), since the
112/// throttler mutates its buffer and window state through `UnsafeCell` without
113/// borrow-check protection. Route side effects through an asynchronous queue
114/// when in doubt.
115pub struct Throttler<T, F> {
116    clock: Rc<RefCell<dyn Clock>>,
117    actor_id: Ustr,
118    timer_name: Ustr,
119    limit: NonZeroUsize,
120    interval_ns: NonZeroU64,
121    large_limit: bool,
122    buffer: VecDeque<T>,
123    timestamps: VecDeque<UnixNanos>,
124    is_limiting: bool,
125    recv_count: usize,
126    sent_count: usize,
127    output_send: F,
128    output_drop: Option<F>,
129}
130
131impl<T, F> Actor for Throttler<T, F>
132where
133    T: 'static + Debug,
134    F: Fn(T) + 'static,
135{
136    fn id(&self) -> Ustr {
137        self.actor_id
138    }
139
140    fn handle(&mut self, _msg: &dyn Any) {}
141
142    fn as_any(&self) -> &dyn Any {
143        self
144    }
145}
146
147impl<T, F> Debug for Throttler<T, F>
148where
149    T: Debug,
150{
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        f.debug_struct(stringify!(Throttler))
153            .field("actor_id", &self.actor_id)
154            .field("timer_name", &self.timer_name)
155            .field("limit", &self.limit())
156            .field("interval_ns", &self.interval_ns())
157            .field("buffer", &self.buffer)
158            .field("timestamps", &self.timestamps)
159            .field("is_limiting", &self.is_limiting)
160            .field("recv_count", &self.recv_count)
161            .field("sent_count", &self.sent_count)
162            .finish()
163    }
164}
165
166impl<T, F> Throttler<T, F>
167where
168    T: Debug,
169{
170    /// Creates a new [`Throttler`] instance.
171    ///
172    /// The timer is registered under a name namespaced by `actor_id` so multiple
173    /// throttlers can share one clock.
174    #[inline]
175    pub fn new(
176        rate_limit: RateLimit,
177        clock: Rc<RefCell<dyn Clock>>,
178        timer_name: &str,
179        output_send: F,
180        output_drop: Option<F>,
181        actor_id: Ustr,
182    ) -> Self {
183        Self {
184            clock,
185            actor_id,
186            timer_name: Ustr::from(format!("{timer_name}-{actor_id}").as_str()),
187            limit: rate_limit.limit,
188            interval_ns: rate_limit.interval_ns,
189            large_limit: rate_limit.limit.get() > MAX_INITIAL_TIMESTAMPS_CAPACITY,
190            buffer: VecDeque::new(),
191            timestamps: VecDeque::with_capacity(
192                rate_limit.limit.get().min(MAX_INITIAL_TIMESTAMPS_CAPACITY),
193            ),
194            is_limiting: false,
195            recv_count: 0,
196            sent_count: 0,
197            output_send,
198            output_drop,
199        }
200    }
201
202    /// Set timer with a callback to be triggered on next interval.
203    ///
204    /// Typically used to register callbacks:
205    /// - to process buffered messages
206    /// - to stop buffering
207    ///
208    /// `allow_past` is set explicitly so a zero `delta_next` clamps to the
209    /// current time and fires immediately instead of returning an error.
210    ///
211    /// # Panics
212    ///
213    /// Panics if setting the time alert on the internal clock fails.
214    #[inline]
215    pub(crate) fn set_timer(&self, callback: Option<TimeEventCallback>) {
216        let delta = self.delta_next();
217        self.set_timer_after(delta, callback);
218    }
219
220    #[inline]
221    fn set_timer_after(&self, delta: u64, callback: Option<TimeEventCallback>) {
222        let mut clock = self.clock.borrow_mut();
223        if clock.timer_exists(&self.timer_name) {
224            clock.cancel_timer(&self.timer_name);
225        }
226        let alert_ts = clock.timestamp_ns() + delta;
227
228        clock
229            .set_time_alert_ns(self.timer_name.as_str(), alert_ts, callback, Some(true))
230            .expect(FAILED);
231    }
232
233    /// Time delta when the next message can be sent.
234    ///
235    /// Uses saturating subtraction so a clock regression or a future-dated
236    /// timestamp yields a zero delta instead of panicking.
237    #[inline]
238    pub fn delta_next(&self) -> u64 {
239        match self.timestamps.get(self.limit.get() - 1) {
240            Some(ts) => {
241                let diff = self
242                    .clock
243                    .borrow()
244                    .timestamp_ns()
245                    .as_u64()
246                    .saturating_sub(ts.as_u64());
247                self.interval_ns.get().saturating_sub(diff)
248            }
249            None => 0,
250        }
251    }
252
253    #[inline]
254    fn delta_next_at(&self, now: UnixNanos) -> u64 {
255        match self.timestamps.get(self.limit.get() - 1) {
256            Some(ts) => {
257                let diff = now.as_u64().saturating_sub(ts.as_u64());
258                self.interval_ns.get().saturating_sub(diff)
259            }
260            None => 0,
261        }
262    }
263
264    /// Reset the throttler which clears internal state and cancels any pending
265    /// timer so no drain or resume callback fires after reset.
266    #[inline]
267    pub fn reset(&mut self) {
268        self.cancel_timer_internal();
269        self.buffer.clear();
270        self.recv_count = 0;
271        self.sent_count = 0;
272        self.is_limiting = false;
273        self.timestamps.clear();
274    }
275}
276
277impl<T, F> Throttler<T, F> {
278    /// Cancels the throttler's timer if one is pending. Silently does nothing
279    /// when the clock is borrowed elsewhere or no timer exists (best-effort,
280    /// e.g. from `Drop`).
281    ///
282    /// Lives in a boundless impl block so `Drop` (which has no `T: Debug` bound)
283    /// can call it.
284    fn cancel_timer_internal(&self) {
285        if let Ok(mut clock) = self.clock.try_borrow_mut() {
286            clock.cancel_timer(&self.timer_name);
287        }
288    }
289
290    /// Counts sent messages whose timestamps fall inside the current interval
291    /// window. Shared by [`Throttler::used`] and [`Throttler::try_reserve`].
292    fn count_in_window(&self, now: UnixNanos) -> usize {
293        let interval_start = now.as_i64() - self.interval_ns.get() as i64;
294
295        if let Some(oldest) = self.timestamps.back()
296            && oldest.as_i64() > interval_start
297        {
298            return self.timestamps.len();
299        }
300
301        match self.timestamps.front() {
302            Some(newest) if newest.as_i64() > interval_start => {}
303            _ => return 0,
304        }
305
306        self.timestamps
307            .iter()
308            .take_while(|&&ts| ts.as_i64() > interval_start)
309            .count()
310    }
311
312    /// Maximum number of messages that can be sent within the interval.
313    #[inline]
314    pub const fn limit(&self) -> usize {
315        self.limit.get()
316    }
317
318    /// Interval between messages in nanoseconds.
319    #[inline]
320    pub const fn interval_ns(&self) -> u64 {
321        self.interval_ns.get()
322    }
323
324    /// Rate limit configured for this throttler.
325    #[inline]
326    pub const fn rate_limit(&self) -> RateLimit {
327        RateLimit {
328            limit: self.limit,
329            interval_ns: self.interval_ns,
330        }
331    }
332
333    /// Number of messages queued in buffer.
334    #[inline]
335    pub fn qsize(&self) -> usize {
336        self.buffer.len()
337    }
338
339    /// Fractional value of rate limit consumed in current interval.
340    #[inline]
341    pub fn used(&self) -> f64 {
342        if self.timestamps.is_empty() {
343            return 0.0;
344        }
345        let messages_in_current_interval = self.count_in_window(self.clock.borrow().timestamp_ns());
346        (messages_in_current_interval as f64) / (self.limit.get() as f64)
347    }
348
349    /// Whether the throttler is currently limiting the message rate.
350    #[inline]
351    pub const fn is_limiting(&self) -> bool {
352        self.is_limiting
353    }
354
355    /// Number of messages received.
356    #[inline]
357    pub const fn recv_count(&self) -> usize {
358        self.recv_count
359    }
360
361    /// Number of messages sent.
362    #[inline]
363    pub const fn sent_count(&self) -> usize {
364        self.sent_count
365    }
366}
367
368impl<T, F> Throttler<T, F>
369where
370    T: 'static + Debug,
371    F: Fn(T) + 'static,
372{
373    pub fn to_actor(self) -> Rc<UnsafeCell<Self>> {
374        // Register process endpoint
375        let process_handler = ThrottlerProcess::<T, F>::new(self.actor_id);
376        msgbus::register_any(
377            process_handler.id().as_str().into(),
378            ShareableMessageHandler::from(Rc::new(process_handler) as Rc<dyn Handler<dyn Any>>),
379        );
380
381        // Register actor state and return the wrapped reference
382        register_actor(self)
383    }
384
385    /// Disposes of the throttler by cancelling its timer, deregistering its
386    /// process endpoint from the message bus, and removing it from the actor
387    /// registry.
388    ///
389    /// Call this before dropping a throttler registered via [`Throttler::to_actor`]
390    /// to avoid leaking the process endpoint. For embedded throttlers (not
391    /// registered) this is still safe: the endpoint and registry removals are
392    /// no-ops.
393    pub fn dispose(&mut self) {
394        self.cancel_timer_internal();
395        msgbus::deregister_any(process_endpoint(self.actor_id));
396        with_actor_registry(|registry| {
397            registry.remove(&self.actor_id);
398        });
399    }
400
401    #[inline]
402    pub(crate) fn send_msg(&mut self, msg: T) {
403        let now = self.clock.borrow().timestamp_ns();
404
405        if self.timestamps.len() >= self.limit.get() {
406            self.timestamps.pop_back();
407        }
408        self.timestamps.push_front(now);
409
410        self.sent_count += 1;
411        (self.output_send)(msg);
412    }
413
414    /// Reserves capacity for `count` messages without sending callbacks.
415    ///
416    /// Returns `false` when the current window cannot accept all messages. No partial
417    /// reservation is made in that case. The resume timer is armed only when the
418    /// window is genuinely full (`delta_next > 0`); when the window already slid
419    /// (`delta_next == 0`) the next call re-evaluates without arming a zero-delta
420    /// timer that would fire immediately and log spam.
421    #[inline]
422    pub fn try_reserve(&mut self, count: usize) -> bool {
423        self.recv_count += count;
424
425        if count == 0 {
426            return true;
427        }
428
429        let now = self.clock.borrow().timestamp_ns();
430        let delta = self.delta_next_at(now);
431        if self.is_limiting && delta == 0 && self.buffer.is_empty() {
432            self.is_limiting = false;
433        }
434
435        if self.is_limiting {
436            return false;
437        }
438
439        let used = self.count_in_window(now);
440
441        if self.limit.get().saturating_sub(used) < count {
442            self.is_limiting = true;
443
444            if delta > 0 {
445                self.set_timer_after(delta, Some(throttler_resume::<T, F>(self.actor_id)));
446            }
447            return false;
448        }
449
450        for _ in 0..count {
451            if self.timestamps.len() >= self.limit.get() {
452                self.timestamps.pop_back();
453            }
454            self.timestamps.push_front(now);
455        }
456        self.sent_count += count;
457        true
458    }
459
460    #[inline]
461    pub(crate) fn limit_msg(&mut self, msg: T) {
462        if self.output_drop.is_none() {
463            self.buffer.push_front(msg);
464            log::debug!("Buffering {}", self.buffer.len());
465
466            if !self.is_limiting {
467                log::debug!("Limiting");
468                let cb = Some(ThrottlerProcess::<T, F>::new(self.actor_id).get_timer_callback());
469                let delta = self.delta_next();
470                self.set_timer_after(delta, cb);
471                self.is_limiting = true;
472            }
473        } else {
474            log::debug!("Dropping");
475
476            if let Some(drop) = &self.output_drop {
477                drop(msg);
478            }
479
480            if !self.is_limiting {
481                log::debug!("Limiting");
482                let delta = self.delta_next();
483                self.set_timer_after(delta, Some(throttler_resume::<T, F>(self.actor_id)));
484                self.is_limiting = true;
485            }
486        }
487    }
488
489    #[inline]
490    pub fn send(&mut self, msg: T)
491    where
492        T: 'static,
493        F: Fn(T) + 'static,
494    {
495        self.recv_count += 1;
496
497        if self.large_limit && self.timestamps.len() < self.limit.get() && !self.is_limiting {
498            self.send_msg(msg);
499            return;
500        }
501
502        let delta = if self.is_limiting && !self.buffer.is_empty() {
503            0
504        } else {
505            self.delta_next()
506        };
507
508        // Auto-reset when the rate window has passed but no timer callback
509        // arrived (e.g. for embedded throttlers not registered as actors).
510        // Gated on an empty buffer so buffered throttlers keep draining via
511        // ThrottlerProcess; only drop-mode throttlers have an empty buffer here.
512        if self.is_limiting && delta == 0 && self.buffer.is_empty() {
513            self.is_limiting = false;
514        }
515
516        if self.is_limiting || delta > 0 {
517            self.limit_msg(msg);
518        } else {
519            self.send_msg(msg);
520        }
521    }
522}
523
524/// Builds the message-bus endpoint used to drive the buffered drain handler.
525/// Centralized so registration, `dispose`, and `Drop` agree on the name.
526fn process_endpoint(actor_id: Ustr) -> MStr<Endpoint> {
527    MStr::endpoint(format!("{actor_id}_process")).expect(FAILED)
528}
529
530/// Process buffered messages for throttler
531///
532/// When limit is reached, schedules a timer event to call self again. The handler
533/// is registered as a separated endpoint on the message bus as `{actor_id}_process`.
534struct ThrottlerProcess<T, F> {
535    actor_id: Ustr,
536    endpoint: MStr<Endpoint>,
537    phantom_t: PhantomData<T>,
538    phantom_f: PhantomData<F>,
539}
540
541impl<T, F> ThrottlerProcess<T, F>
542where
543    T: Debug,
544{
545    pub(crate) fn new(actor_id: Ustr) -> Self {
546        Self {
547            actor_id,
548            endpoint: process_endpoint(actor_id),
549            phantom_t: PhantomData,
550            phantom_f: PhantomData,
551        }
552    }
553
554    pub(crate) fn get_timer_callback(&self) -> TimeEventCallback {
555        let endpoint = self.endpoint;
556        TimeEventCallback::from(move |event: TimeEvent| {
557            msgbus::send_any(endpoint, &(event));
558        })
559    }
560}
561
562impl<T, F> Handler<dyn Any> for ThrottlerProcess<T, F>
563where
564    T: 'static + Debug,
565    F: Fn(T) + 'static,
566{
567    fn id(&self) -> Ustr {
568        *self.endpoint
569    }
570
571    fn handle(&self, _message: &dyn Any) {
572        // Use the fallible lookup so a late timer fire after teardown is a
573        // no-op rather than a panic.
574        let Some(mut throttler) = try_get_actor_unchecked::<Throttler<T, F>>(&self.actor_id) else {
575            return;
576        };
577
578        while let Some(msg) = throttler.buffer.pop_back() {
579            throttler.send_msg(msg);
580
581            // Set timer to process more buffered messages
582            // if interval limit reached and there are more
583            // buffered messages to process
584            if !throttler.buffer.is_empty() && throttler.delta_next() > 0 {
585                throttler.is_limiting = true;
586                throttler.set_timer(Some(self.get_timer_callback()));
587                return;
588            }
589        }
590
591        throttler.is_limiting = false;
592    }
593}
594
595impl<T, F> Drop for Throttler<T, F> {
596    fn drop(&mut self) {
597        // Cancel any pending timer so drain/resume callbacks do not fire after
598        // teardown. Best-effort: skip silently if the shared clock is currently
599        // borrowed (e.g. during a nested drop).
600        self.cancel_timer_internal();
601    }
602}
603
604/// Sets throttler to resume sending messages.
605///
606/// Uses `try_get_actor_unchecked` so that embedded throttlers (not registered
607/// in the actor registry) are handled gracefully. The `send()` auto-reset
608/// ensures such throttlers recover once the rate window passes.
609pub fn throttler_resume<T, F>(actor_id: Ustr) -> TimeEventCallback
610where
611    T: 'static + Debug,
612    F: Fn(T) + 'static,
613{
614    TimeEventCallback::from(move |_event: TimeEvent| {
615        if let Some(mut throttler) = try_get_actor_unchecked::<Throttler<T, F>>(&actor_id) {
616            throttler.is_limiting = false;
617        }
618    })
619}
620
621#[cfg(test)]
622mod tests {
623    use std::{
624        cell::{RefCell, UnsafeCell},
625        rc::Rc,
626    };
627
628    use nautilus_core::UUID4;
629    use rstest::{fixture, rstest};
630    use ustr::Ustr;
631
632    use super::{MAX_INITIAL_TIMESTAMPS_CAPACITY, RateLimit, Throttler, ThrottlerProcess};
633    use crate::{
634        clock::{Clock, TestClock},
635        msgbus::{self, Handler},
636    };
637    type SharedThrottler = Rc<UnsafeCell<Throttler<u64, Box<dyn Fn(u64)>>>>;
638
639    /// Test throttler with default values for testing
640    ///
641    /// - Rate limit is 5 messages in 10 intervals.
642    /// - Message handling is decided by specific fixture
643    #[derive(Clone)]
644    struct TestThrottler {
645        throttler: SharedThrottler,
646        clock: Rc<RefCell<TestClock>>,
647        interval: u64,
648    }
649
650    #[allow(unsafe_code)]
651    impl TestThrottler {
652        #[expect(clippy::mut_from_ref)]
653        pub(crate) fn get_throttler(&self) -> &mut Throttler<u64, Box<dyn Fn(u64)>> {
654            unsafe { &mut *self.throttler.get() }
655        }
656    }
657
658    // Timer names are namespaced as `{base}-{actor_id}` with a random actor_id,
659    // so tests match on the base prefix and the expected count instead of an
660    // exact name.
661    fn timer_count_with_prefix(
662        throttler: &Throttler<u64, Box<dyn Fn(u64)>>,
663        prefix: &str,
664    ) -> usize {
665        throttler
666            .clock
667            .borrow()
668            .timer_names()
669            .iter()
670            .filter(|name| name.starts_with(prefix))
671            .count()
672    }
673
674    #[allow(unsafe_code)]
675    #[expect(clippy::mut_from_ref)]
676    fn access_shared(t: &SharedThrottler) -> &mut Throttler<u64, Box<dyn Fn(u64)>> {
677        unsafe { &mut *t.get() }
678    }
679
680    #[rstest]
681    #[case(0, 1_000)]
682    #[case(1_000, 0)]
683    fn test_rate_limit_new_checked_rejects_zero(#[case] limit: usize, #[case] interval_ns: u64) {
684        assert!(RateLimit::new_checked(limit, interval_ns).is_err());
685    }
686
687    #[rstest]
688    #[case(0, 1_000)]
689    #[case(1_000, 0)]
690    #[should_panic]
691    fn test_rate_limit_new_panics_on_zero(#[case] limit: usize, #[case] interval_ns: u64) {
692        let _ = RateLimit::new(limit, interval_ns);
693    }
694
695    #[rstest]
696    fn test_rate_limit_new_checked_accepts_positive() {
697        let rate = RateLimit::new_checked(5, 10).unwrap();
698
699        assert_eq!(rate.limit(), 5);
700        assert_eq!(rate.interval_ns(), 10);
701    }
702
703    #[fixture]
704    pub fn test_throttler_buffered() -> TestThrottler {
705        let output_send: Box<dyn Fn(u64)> = Box::new(|msg: u64| {
706            log::debug!("Sent: {msg}");
707        });
708        let clock = Rc::new(RefCell::new(TestClock::new()));
709        let inner_clock = Rc::clone(&clock);
710        let rate_limit = RateLimit::new(5, 10);
711        let interval = rate_limit.interval_ns();
712        let actor_id = Ustr::from(UUID4::new().as_str());
713
714        TestThrottler {
715            throttler: Throttler::new(
716                rate_limit,
717                clock,
718                "buffer_timer",
719                output_send,
720                None,
721                actor_id,
722            )
723            .to_actor(),
724            clock: inner_clock,
725            interval,
726        }
727    }
728
729    #[fixture]
730    pub fn test_throttler_unbuffered() -> TestThrottler {
731        let output_send: Box<dyn Fn(u64)> = Box::new(|msg: u64| {
732            log::debug!("Sent: {msg}");
733        });
734        let output_drop: Box<dyn Fn(u64)> = Box::new(|msg: u64| {
735            log::debug!("Dropped: {msg}");
736        });
737        let clock = Rc::new(RefCell::new(TestClock::new()));
738        let inner_clock = Rc::clone(&clock);
739        let rate_limit = RateLimit::new(5, 10);
740        let interval = rate_limit.interval_ns();
741        let actor_id = Ustr::from(UUID4::new().as_str());
742
743        TestThrottler {
744            throttler: Throttler::new(
745                rate_limit,
746                clock,
747                "dropper_timer",
748                output_send,
749                Some(output_drop),
750                actor_id,
751            )
752            .to_actor(),
753            clock: inner_clock,
754            interval,
755        }
756    }
757
758    #[rstest]
759    fn test_buffering_send_to_limit_becomes_throttled(test_throttler_buffered: TestThrottler) {
760        let throttler = test_throttler_buffered.get_throttler();
761        for _ in 0..6 {
762            throttler.send(42);
763        }
764        assert_eq!(throttler.qsize(), 1);
765
766        assert!(throttler.is_limiting);
767        assert_eq!(throttler.recv_count, 6);
768        assert_eq!(throttler.sent_count, 5);
769        assert_eq!(timer_count_with_prefix(throttler, "buffer_timer"), 1);
770    }
771
772    #[rstest]
773    fn test_buffering_used_when_sent_to_limit_returns_one(test_throttler_buffered: TestThrottler) {
774        let throttler = test_throttler_buffered.get_throttler();
775
776        for _ in 0..5 {
777            throttler.send(42);
778        }
779
780        assert_eq!(throttler.used(), 1.0);
781        assert_eq!(throttler.recv_count, 5);
782        assert_eq!(throttler.sent_count, 5);
783    }
784
785    #[rstest]
786    fn test_buffering_used_when_half_interval_from_limit_returns_one(
787        test_throttler_buffered: TestThrottler,
788    ) {
789        let throttler = test_throttler_buffered.get_throttler();
790
791        for _ in 0..5 {
792            throttler.send(42);
793        }
794
795        let half_interval = test_throttler_buffered.interval / 2;
796        // Advance the clock by half the interval
797        {
798            let mut clock = test_throttler_buffered.clock.borrow_mut();
799            clock.advance_time(half_interval.into(), true);
800        }
801
802        assert_eq!(throttler.used(), 1.0);
803        assert_eq!(throttler.recv_count, 5);
804        assert_eq!(throttler.sent_count, 5);
805    }
806
807    #[rstest]
808    fn test_buffering_used_before_limit_when_halfway_returns_half(
809        test_throttler_buffered: TestThrottler,
810    ) {
811        let throttler = test_throttler_buffered.get_throttler();
812
813        for _ in 0..3 {
814            throttler.send(42);
815        }
816
817        assert_eq!(throttler.used(), 0.6);
818        assert_eq!(throttler.recv_count, 3);
819        assert_eq!(throttler.sent_count, 3);
820    }
821
822    #[rstest]
823    fn test_try_reserve_counts_messages_without_output(test_throttler_buffered: TestThrottler) {
824        let throttler = test_throttler_buffered.get_throttler();
825
826        assert!(throttler.try_reserve(3));
827
828        assert_eq!(throttler.used(), 0.6);
829        assert_eq!(throttler.recv_count, 3);
830        assert_eq!(throttler.sent_count, 3);
831        assert_eq!(throttler.qsize(), 0);
832    }
833
834    #[rstest]
835    fn test_try_reserve_rejects_when_full_batch_exceeds_limit(
836        test_throttler_buffered: TestThrottler,
837    ) {
838        let throttler = test_throttler_buffered.get_throttler();
839
840        assert!(throttler.try_reserve(3));
841        assert!(!throttler.try_reserve(3));
842
843        assert_eq!(throttler.used(), 0.6);
844        assert_eq!(throttler.recv_count, 6);
845        assert_eq!(throttler.sent_count, 3);
846        assert_eq!(throttler.qsize(), 0);
847        assert!(throttler.is_limiting);
848        // delta_next == 0 here (only 3 of 5 slots used), so the resume timer is
849        // not armed to avoid an immediate-fire zero-delta timer. The next call
850        // re-evaluates via the auto-reset branch.
851        assert_eq!(timer_count_with_prefix(throttler, "buffer_timer"), 0);
852
853        assert!(throttler.try_reserve(2));
854
855        assert_eq!(throttler.used(), 1.0);
856        assert_eq!(throttler.recv_count, 8);
857        assert_eq!(throttler.sent_count, 5);
858        assert_eq!(throttler.qsize(), 0);
859        assert!(!throttler.is_limiting);
860    }
861
862    #[rstest]
863    fn test_try_reserve_rejects_batch_larger_than_limit() {
864        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
865        let mut throttler = Throttler::<u64, Box<dyn Fn(u64)>>::new(
866            RateLimit::new(5, 10),
867            clock,
868            "reserve_over_limit",
869            Box::new(|_| ()) as Box<dyn Fn(u64)>,
870            None,
871            Ustr::from("reserve-over-limit-actor"),
872        );
873
874        assert!(!throttler.try_reserve(6));
875
876        assert_eq!(throttler.used(), 0.0);
877        assert_eq!(throttler.recv_count, 6);
878        assert_eq!(throttler.sent_count, 0);
879        assert_eq!(throttler.qsize(), 0);
880        assert!(throttler.is_limiting);
881        assert_eq!(throttler.clock.borrow().timer_count(), 0);
882    }
883
884    #[rstest]
885    fn test_try_reserve_zero_count_is_noop() {
886        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
887        let mut throttler = Throttler::<u64, Box<dyn Fn(u64)>>::new(
888            RateLimit::new(5, 10),
889            clock,
890            "reserve_zero",
891            Box::new(|_| ()) as Box<dyn Fn(u64)>,
892            None,
893            Ustr::from("reserve-zero-actor"),
894        );
895
896        assert!(throttler.try_reserve(0));
897
898        assert_eq!(throttler.used(), 0.0);
899        assert_eq!(throttler.recv_count, 0);
900        assert_eq!(throttler.sent_count, 0);
901        assert_eq!(throttler.qsize(), 0);
902        assert!(!throttler.is_limiting);
903        assert_eq!(throttler.clock.borrow().timer_count(), 0);
904    }
905
906    #[rstest]
907    fn test_buffering_refresh_when_at_limit_sends_remaining_items(
908        test_throttler_buffered: TestThrottler,
909    ) {
910        let throttler = test_throttler_buffered.get_throttler();
911
912        for _ in 0..6 {
913            throttler.send(42);
914        }
915
916        // Advance time and process events
917        {
918            let mut clock = test_throttler_buffered.clock.borrow_mut();
919            let time_events = clock.advance_time(test_throttler_buffered.interval.into(), true);
920            for each_event in clock.match_handlers(time_events) {
921                drop(clock); // Release the mutable borrow
922
923                each_event.callback.call(each_event.event);
924
925                // Re-borrow the clock for the next iteration
926                clock = test_throttler_buffered.clock.borrow_mut();
927            }
928        }
929
930        // Assert final state
931        assert_eq!(throttler.used(), 0.2);
932        assert_eq!(throttler.recv_count, 6);
933        assert_eq!(throttler.sent_count, 6);
934        assert_eq!(throttler.qsize(), 0);
935    }
936
937    #[rstest]
938    fn test_buffering_send_message_after_buffering_message(test_throttler_buffered: TestThrottler) {
939        let throttler = test_throttler_buffered.get_throttler();
940
941        for _ in 0..6 {
942            throttler.send(43);
943        }
944
945        // Advance time and process events
946        {
947            let mut clock = test_throttler_buffered.clock.borrow_mut();
948            let time_events = clock.advance_time(test_throttler_buffered.interval.into(), true);
949            for each_event in clock.match_handlers(time_events) {
950                drop(clock); // Release the mutable borrow
951
952                each_event.callback.call(each_event.event);
953
954                // Re-borrow the clock for the next iteration
955                clock = test_throttler_buffered.clock.borrow_mut();
956            }
957        }
958
959        for _ in 0..6 {
960            throttler.send(42);
961        }
962
963        // Assert final state
964        assert_eq!(throttler.used(), 1.0);
965        assert_eq!(throttler.recv_count, 12);
966        assert_eq!(throttler.sent_count, 10);
967        assert_eq!(throttler.qsize(), 2);
968    }
969
970    #[rstest]
971    fn test_buffering_send_message_after_halfway_after_buffering_message(
972        test_throttler_buffered: TestThrottler,
973    ) {
974        let throttler = test_throttler_buffered.get_throttler();
975
976        for _ in 0..6 {
977            throttler.send(42);
978        }
979
980        // Advance time and process events
981        {
982            let mut clock = test_throttler_buffered.clock.borrow_mut();
983            let time_events = clock.advance_time(test_throttler_buffered.interval.into(), true);
984            for each_event in clock.match_handlers(time_events) {
985                drop(clock); // Release the mutable borrow
986
987                each_event.callback.call(each_event.event);
988
989                // Re-borrow the clock for the next iteration
990                clock = test_throttler_buffered.clock.borrow_mut();
991            }
992        }
993
994        for _ in 0..3 {
995            throttler.send(42);
996        }
997
998        // Assert final state
999        assert_eq!(throttler.used(), 0.8);
1000        assert_eq!(throttler.recv_count, 9);
1001        assert_eq!(throttler.sent_count, 9);
1002        assert_eq!(throttler.qsize(), 0);
1003    }
1004
1005    #[rstest]
1006    fn test_dropping_send_sends_message_to_handler(test_throttler_unbuffered: TestThrottler) {
1007        let throttler = test_throttler_unbuffered.get_throttler();
1008        throttler.send(42);
1009
1010        assert!(!throttler.is_limiting);
1011        assert_eq!(throttler.recv_count, 1);
1012        assert_eq!(throttler.sent_count, 1);
1013    }
1014
1015    #[rstest]
1016    fn test_dropping_send_to_limit_drops_message(test_throttler_unbuffered: TestThrottler) {
1017        let throttler = test_throttler_unbuffered.get_throttler();
1018        for _ in 0..6 {
1019            throttler.send(42);
1020        }
1021        assert_eq!(throttler.qsize(), 0);
1022
1023        assert!(throttler.is_limiting);
1024        assert_eq!(throttler.used(), 1.0);
1025        assert_eq!(throttler.clock.borrow().timer_count(), 1);
1026        assert_eq!(timer_count_with_prefix(throttler, "dropper_timer"), 1);
1027        assert_eq!(throttler.recv_count, 6);
1028        assert_eq!(throttler.sent_count, 5);
1029    }
1030
1031    #[rstest]
1032    fn test_dropping_advance_time_when_at_limit_dropped_message(
1033        test_throttler_unbuffered: TestThrottler,
1034    ) {
1035        let throttler = test_throttler_unbuffered.get_throttler();
1036        for _ in 0..6 {
1037            throttler.send(42);
1038        }
1039
1040        // Advance time and process events
1041        {
1042            let mut clock = test_throttler_unbuffered.clock.borrow_mut();
1043            let time_events = clock.advance_time(test_throttler_unbuffered.interval.into(), true);
1044            for each_event in clock.match_handlers(time_events) {
1045                drop(clock); // Release the mutable borrow
1046
1047                each_event.callback.call(each_event.event);
1048
1049                // Re-borrow the clock for the next iteration
1050                clock = test_throttler_unbuffered.clock.borrow_mut();
1051            }
1052        }
1053
1054        assert_eq!(throttler.clock.borrow().timer_count(), 0);
1055        assert!(!throttler.is_limiting);
1056        assert_eq!(throttler.used(), 0.0);
1057        assert_eq!(throttler.recv_count, 6);
1058        assert_eq!(throttler.sent_count, 5);
1059    }
1060
1061    #[rstest]
1062    fn test_dropping_send_message_after_dropping_message(test_throttler_unbuffered: TestThrottler) {
1063        let throttler = test_throttler_unbuffered.get_throttler();
1064        for _ in 0..6 {
1065            throttler.send(42);
1066        }
1067
1068        // Advance time and process events
1069        {
1070            let mut clock = test_throttler_unbuffered.clock.borrow_mut();
1071            let time_events = clock.advance_time(test_throttler_unbuffered.interval.into(), true);
1072            for each_event in clock.match_handlers(time_events) {
1073                drop(clock); // Release the mutable borrow
1074
1075                each_event.callback.call(each_event.event);
1076
1077                // Re-borrow the clock for the next iteration
1078                clock = test_throttler_unbuffered.clock.borrow_mut();
1079            }
1080        }
1081
1082        throttler.send(42);
1083
1084        assert_eq!(throttler.used(), 0.2);
1085        assert_eq!(throttler.clock.borrow().timer_count(), 0);
1086        assert!(!throttler.is_limiting);
1087        assert_eq!(throttler.recv_count, 7);
1088        assert_eq!(throttler.sent_count, 6);
1089    }
1090
1091    #[rstest]
1092    fn test_embedded_dropping_auto_resets_after_window_without_actor_callback() {
1093        let clock: Rc<RefCell<TestClock>> = Rc::new(RefCell::new(TestClock::new()));
1094        let sent = Rc::new(RefCell::new(0));
1095        let dropped = Rc::new(RefCell::new(0));
1096
1097        let sent_cb = {
1098            let sent = Rc::clone(&sent);
1099            Box::new(move |_| *sent.borrow_mut() += 1) as Box<dyn Fn(u64)>
1100        };
1101        let drop_cb = {
1102            let dropped = Rc::clone(&dropped);
1103            Box::new(move |_| *dropped.borrow_mut() += 1) as Box<dyn Fn(u64)>
1104        };
1105
1106        let mut throttler = Throttler::new(
1107            RateLimit::new(5, 10),
1108            Rc::clone(&clock) as Rc<RefCell<dyn Clock>>,
1109            "embedded_drop_timer",
1110            sent_cb,
1111            Some(drop_cb),
1112            Ustr::from("embedded-drop-actor"),
1113        );
1114
1115        for _ in 0..6 {
1116            throttler.send(42);
1117        }
1118        let events = clock.borrow_mut().advance_time(10.into(), true);
1119        throttler.send(42);
1120
1121        assert_eq!(events.len(), 1);
1122        assert_eq!(*sent.borrow(), 6);
1123        assert_eq!(*dropped.borrow(), 1);
1124        assert_eq!(throttler.recv_count, 7);
1125        assert_eq!(throttler.sent_count, 6);
1126        assert!(!throttler.is_limiting);
1127        assert_eq!(throttler.clock.borrow().timer_count(), 0);
1128    }
1129
1130    #[rstest]
1131    fn test_large_limit_fast_path_admits_until_limit_then_limits() {
1132        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
1133        let limit = MAX_INITIAL_TIMESTAMPS_CAPACITY + 1;
1134        let mut throttler = Throttler::<u64, Box<dyn Fn(u64)>>::new(
1135            RateLimit::new(limit, 10),
1136            clock,
1137            "large_limit_timer",
1138            Box::new(|_| ()) as Box<dyn Fn(u64)>,
1139            None,
1140            Ustr::from("large-limit-actor"),
1141        );
1142
1143        for _ in 0..limit {
1144            throttler.send(42);
1145        }
1146        throttler.send(42);
1147
1148        assert_eq!(throttler.used(), 1.0);
1149        assert_eq!(throttler.recv_count, limit + 1);
1150        assert_eq!(throttler.sent_count, limit);
1151        assert_eq!(throttler.qsize(), 1);
1152        assert!(throttler.is_limiting);
1153        assert_eq!(throttler.clock.borrow().timer_count(), 1);
1154    }
1155
1156    #[rstest]
1157    fn test_new_preserves_rate_limit() {
1158        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
1159        let rate_limit = RateLimit::new(5, 10);
1160
1161        let throttler = Throttler::<u64, Box<dyn Fn(u64)>>::new(
1162            rate_limit,
1163            clock,
1164            "rate_limit",
1165            Box::new(|_| ()) as Box<dyn Fn(u64)>,
1166            None,
1167            Ustr::from("rate-limit-actor"),
1168        );
1169
1170        assert_eq!(throttler.rate_limit(), rate_limit);
1171        assert_eq!(throttler.limit(), 5);
1172        assert_eq!(throttler.interval_ns(), 10);
1173    }
1174
1175    #[rstest]
1176    fn test_debug_output_includes_identity_and_state() {
1177        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
1178        let actor_id = Ustr::from("debug-actor");
1179        let mut throttler = Throttler::<u64, Box<dyn Fn(u64)>>::new(
1180            RateLimit::new(5, 10),
1181            clock,
1182            "debug_timer",
1183            Box::new(|_| ()) as Box<dyn Fn(u64)>,
1184            None,
1185            actor_id,
1186        );
1187
1188        throttler.send(42);
1189
1190        let debug = format!("{throttler:?}");
1191        let timer_name = Ustr::from("debug_timer-debug-actor");
1192
1193        assert!(debug.contains(&format!("actor_id: {actor_id:?}")));
1194        assert!(debug.contains(&format!("timer_name: {timer_name:?}")));
1195        assert!(debug.contains("limit: 5"));
1196        assert!(debug.contains("interval_ns: 10"));
1197        assert!(debug.contains("is_limiting: false"));
1198        assert!(debug.contains("recv_count: 1"));
1199        assert!(debug.contains("sent_count: 1"));
1200    }
1201
1202    #[rstest]
1203    fn test_reset_clears_state_and_cancels_timer(test_throttler_buffered: TestThrottler) {
1204        let throttler = test_throttler_buffered.get_throttler();
1205
1206        for _ in 0..6 {
1207            throttler.send(42);
1208        }
1209        assert_eq!(timer_count_with_prefix(throttler, "buffer_timer"), 1);
1210        assert_eq!(throttler.qsize(), 1);
1211
1212        throttler.reset();
1213
1214        assert_eq!(throttler.qsize(), 0);
1215        assert_eq!(throttler.recv_count, 0);
1216        assert_eq!(throttler.sent_count, 0);
1217        assert!(!throttler.is_limiting);
1218        assert!(throttler.timestamps.is_empty());
1219        assert_eq!(timer_count_with_prefix(throttler, "buffer_timer"), 0);
1220        assert_eq!(throttler.clock.borrow().timer_count(), 0);
1221    }
1222
1223    #[rstest]
1224    fn test_two_throttlers_share_clock_without_timer_collision() {
1225        let clock: Rc<RefCell<TestClock>> = Rc::new(RefCell::new(TestClock::new()));
1226        let interval = 10u64;
1227
1228        let mk = |base: &str| -> SharedThrottler {
1229            let clock: Rc<RefCell<dyn Clock>> = Rc::clone(&clock) as Rc<RefCell<dyn Clock>>;
1230            Throttler::new(
1231                RateLimit::new(5, interval),
1232                clock,
1233                base,
1234                Box::new(|_| ()) as Box<dyn Fn(u64)>,
1235                None,
1236                Ustr::from(UUID4::new().as_str()),
1237            )
1238            .to_actor()
1239        };
1240
1241        let t1 = mk("shared_timer");
1242        let t2 = mk("shared_timer");
1243
1244        // Both throttlers use the same base timer name on a shared clock; the
1245        // namespaced names must keep both timers distinct.
1246        {
1247            let t1 = access_shared(&t1);
1248
1249            for _ in 0..6 {
1250                t1.send(42);
1251            }
1252        }
1253        {
1254            let t2 = access_shared(&t2);
1255
1256            for _ in 0..6 {
1257                t2.send(42);
1258            }
1259        }
1260
1261        let clock_ref = clock.borrow();
1262        let names = clock_ref.timer_names();
1263        let shared_count = names
1264            .iter()
1265            .filter(|n| n.starts_with("shared_timer"))
1266            .count();
1267        assert_eq!(
1268            shared_count, 2,
1269            "two distinct timers expected, found {names:?}"
1270        );
1271    }
1272
1273    #[rstest]
1274    fn test_try_reserve_then_send_interleaved(test_throttler_buffered: TestThrottler) {
1275        let throttler = test_throttler_buffered.get_throttler();
1276
1277        // Reserve 3 of 5 slots, then send one more via the send path. Both
1278        // paths share the same window: 4 of 5 slots should be used.
1279        assert!(throttler.try_reserve(3));
1280        throttler.send(42);
1281
1282        assert_eq!(throttler.recv_count, 4);
1283        assert_eq!(throttler.sent_count, 4);
1284        assert_eq!(throttler.used(), 0.8);
1285        assert!(!throttler.is_limiting);
1286    }
1287
1288    #[rstest]
1289    fn test_dispose_cancels_timer_and_deregisters_endpoint(test_throttler_buffered: TestThrottler) {
1290        let throttler = test_throttler_buffered.get_throttler();
1291
1292        for _ in 0..6 {
1293            throttler.send(42);
1294        }
1295        let actor_id = throttler.actor_id;
1296        let endpoint_name = format!("{actor_id}_process");
1297        assert_eq!(timer_count_with_prefix(throttler, "buffer_timer"), 1);
1298        assert!(msgbus::has_endpoint(&endpoint_name));
1299
1300        throttler.dispose();
1301
1302        assert_eq!(throttler.clock.borrow().timer_count(), 0);
1303        assert!(
1304            !msgbus::has_endpoint(&endpoint_name),
1305            "dispose must deregister the process endpoint"
1306        );
1307    }
1308
1309    ////////////////////////////////////////////////////////////////////////////////
1310    // Property-based testing
1311    ////////////////////////////////////////////////////////////////////////////////
1312
1313    use proptest::prelude::*;
1314
1315    #[derive(Clone, Debug)]
1316    enum ThrottlerInput {
1317        SendMessage(u64),
1318        AdvanceClock(u8),
1319    }
1320
1321    // Custom strategy for ThrottlerInput
1322    fn throttler_input_strategy() -> impl Strategy<Value = ThrottlerInput> {
1323        prop_oneof![
1324            2 => prop::bool::ANY.prop_map(|_| ThrottlerInput::SendMessage(42)),
1325            8 => prop::num::u8::ANY.prop_map(|v| ThrottlerInput::AdvanceClock(v % 5 + 5)),
1326        ]
1327    }
1328
1329    // Custom strategy for ThrottlerTest
1330    fn throttler_test_strategy() -> impl Strategy<Value = Vec<ThrottlerInput>> {
1331        prop::collection::vec(throttler_input_strategy(), 10..=150)
1332    }
1333
1334    fn test_throttler_with_inputs(inputs: Vec<ThrottlerInput>, test_throttler: &TestThrottler) {
1335        let test_clock = test_throttler.clock.clone();
1336        let interval = test_throttler.interval;
1337        let throttler = test_throttler.get_throttler();
1338        let mut sent_count = 0;
1339
1340        for input in inputs {
1341            match input {
1342                ThrottlerInput::SendMessage(msg) => {
1343                    throttler.send(msg);
1344                    sent_count += 1;
1345                }
1346                ThrottlerInput::AdvanceClock(duration) => {
1347                    let mut clock_ref = test_clock.borrow_mut();
1348                    let current_time = clock_ref.get_time_ns();
1349                    let time_events =
1350                        clock_ref.advance_time(current_time + u64::from(duration), true);
1351                    for each_event in clock_ref.match_handlers(time_events) {
1352                        drop(clock_ref);
1353                        each_event.callback.call(each_event.event);
1354                        clock_ref = test_clock.borrow_mut();
1355                    }
1356                }
1357            }
1358
1359            // Check the throttler rate limits on the appropriate conditions
1360            // * At least one message is buffered
1361            // * Timestamp queue is filled upto limit
1362            // * Least recent timestamp in queue exceeds interval
1363            let buffered_messages = throttler.qsize() > 0;
1364            let now = throttler.clock.borrow().timestamp_ns().as_u64();
1365            let limit_filled_within_interval = throttler
1366                .timestamps
1367                .get(throttler.limit() - 1)
1368                .is_some_and(|&ts| (now - ts.as_u64()) < interval);
1369            let expected_limiting = buffered_messages && limit_filled_within_interval;
1370            assert_eq!(throttler.is_limiting, expected_limiting);
1371
1372            // Message conservation
1373            assert_eq!(sent_count, throttler.sent_count + throttler.qsize());
1374        }
1375
1376        // Drain all buffered messages by repeatedly advancing the clock.
1377        // Each timer callback may send up to `limit` messages and schedule
1378        // a new timer for the next batch, so we must keep advancing.
1379        for i in 1..=100u64 {
1380            if throttler.qsize() == 0 {
1381                break;
1382            }
1383            let advance_to = interval * 100 * i;
1384            let time_events = test_clock
1385                .borrow_mut()
1386                .advance_time(advance_to.into(), true);
1387            let mut clock_ref = test_clock.borrow_mut();
1388            for each_event in clock_ref.match_handlers(time_events) {
1389                drop(clock_ref);
1390                each_event.callback.call(each_event.event);
1391                clock_ref = test_clock.borrow_mut();
1392            }
1393        }
1394        assert_eq!(throttler.qsize(), 0);
1395    }
1396
1397    #[rstest]
1398    fn prop_test() {
1399        // Create a fresh throttler for each iteration to ensure clean state,
1400        // even when tests panic (which would skip the reset code)
1401        proptest!(|(inputs in throttler_test_strategy())| {
1402            let test_throttler = test_throttler_buffered();
1403            test_throttler_with_inputs(inputs, &test_throttler);
1404        });
1405    }
1406
1407    #[rstest]
1408    fn prop_test_dropping() {
1409        // Drop-mode coverage: every received message is either sent or dropped,
1410        // and sent_count tracks the send callback exactly. Catches conservation
1411        // violations and panics under random send/advance sequences.
1412        proptest!(|(inputs in throttler_test_strategy())| {
1413            let clock = Rc::new(RefCell::new(TestClock::new()));
1414            let sent: Rc<RefCell<usize>> = Rc::new(RefCell::new(0));
1415            let dropped: Rc<RefCell<usize>> = Rc::new(RefCell::new(0));
1416
1417            let sent_cb = {
1418                let sent = Rc::clone(&sent);
1419                Box::new(move |_| *sent.borrow_mut() += 1) as Box<dyn Fn(u64)>
1420            };
1421            let drop_cb = {
1422                let dropped = Rc::clone(&dropped);
1423                Box::new(move |_| *dropped.borrow_mut() += 1) as Box<dyn Fn(u64)>
1424            };
1425
1426            let interval = 10u64;
1427            let throttler = Throttler::new(
1428                RateLimit::new(5, interval),
1429                Rc::clone(&clock) as Rc<RefCell<dyn Clock>>,
1430                "prop_drop_timer",
1431                sent_cb,
1432                Some(drop_cb),
1433                Ustr::from(UUID4::new().as_str()),
1434            )
1435            .to_actor();
1436            let throttler = access_shared(&throttler);
1437
1438            for input in inputs {
1439                match input {
1440                    ThrottlerInput::SendMessage(msg) => {
1441                        throttler.send(msg);
1442                    }
1443                    ThrottlerInput::AdvanceClock(duration) => {
1444                        let mut clock_ref = clock.borrow_mut();
1445                        let current_time = clock_ref.get_time_ns();
1446                        let time_events =
1447                            clock_ref.advance_time(current_time + u64::from(duration), true);
1448                        for each_event in clock_ref.match_handlers(time_events) {
1449                            drop(clock_ref);
1450                            each_event.callback.call(each_event.event);
1451                            clock_ref = clock.borrow_mut();
1452                        }
1453                    }
1454                }
1455
1456                let sent_now = *sent.borrow();
1457                let dropped_now = *dropped.borrow();
1458                // Conservation: every received message was sent or dropped.
1459                assert_eq!(sent_now + dropped_now, throttler.recv_count);
1460                assert_eq!(throttler.sent_count, sent_now);
1461                assert!(throttler.qsize() == 0, "drop mode must never buffer");
1462            }
1463        });
1464    }
1465
1466    #[rstest]
1467    fn test_throttler_process_id_returns_ustr() {
1468        // This test verifies that ThrottlerProcess::id() correctly returns Ustr
1469        // by dereferencing MStr<Endpoint> (tests *self.endpoint -> Ustr conversion)
1470        let actor_id = Ustr::from("test_throttler");
1471        let process = ThrottlerProcess::<String, fn(String)>::new(actor_id);
1472
1473        // Call id() which does *self.endpoint
1474        let handler_id: Ustr = process.id();
1475
1476        // Verify it's a valid Ustr with expected format
1477        assert!(handler_id.as_str().contains("test_throttler_process"));
1478        assert!(!handler_id.is_empty());
1479
1480        // Verify type - this wouldn't compile if id() didn't return Ustr
1481        let _type_check: Ustr = handler_id;
1482    }
1483}