technitium 0.4.0

Typed async Rust client for the Technitium DNS Server API
Documentation
use std::sync::Arc;
use std::time::Duration;

use tokio::sync::Semaphore;
use tokio::task::JoinHandle;

/// A token-bucket rate limiter backed by a tokio semaphore.
///
/// Permits are acquired before each request. A background task
/// refills permits at the configured rate. When permits are
/// exhausted, callers wait (backpressure).
#[derive(Debug)]
pub(crate) struct RateLimiter {
    semaphore: Arc<Semaphore>,
    refill_task: JoinHandle<()>,
}

impl RateLimiter {
    /// Create a new rate limiter that allows `rps` requests per second.
    ///
    /// # Panics
    ///
    /// Panics if `rps` is not positive.
    pub fn new(rps: f64) -> Self {
        assert!(rps > 0.0, "requests_per_second must be positive");

        #[allow(
            clippy::cast_possible_truncation,
            clippy::cast_sign_loss,
            reason = "rps is validated positive and ceil() fits in usize"
        )]
        let capacity = rps.ceil() as usize;
        let semaphore = Arc::new(Semaphore::new(capacity));
        let refill_interval = Duration::from_secs_f64(1.0 / rps);

        let sem = Arc::clone(&semaphore);
        let refill_task = tokio::spawn(async move {
            let mut interval = tokio::time::interval(refill_interval);
            loop {
                interval.tick().await;
                if sem.available_permits() < capacity {
                    sem.add_permits(1);
                }
            }
        });

        Self {
            semaphore,
            refill_task,
        }
    }

    /// Acquire a permit, waiting if the rate limit is exceeded.
    pub async fn acquire(&self) {
        // acquire_owned is not needed — we just want to decrement
        let permit = self
            .semaphore
            .acquire()
            .await
            .expect("rate limiter semaphore closed unexpectedly");
        permit.forget(); // consume the permit (refill task will add it back)
    }
}

impl Drop for RateLimiter {
    fn drop(&mut self) {
        self.refill_task.abort();
    }
}