mirror of
https://github.com/telemt/telemt.git
synced 2026-11-01 13:21:58 +03:00
292 lines
8.1 KiB
Rust
292 lines
8.1 KiB
Rust
use std::collections::{HashMap, HashSet};
|
|
use std::net::IpAddr;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
|
use arc_swap::ArcSwap;
|
|
use dashmap::DashMap;
|
|
use ipnetwork::IpNetwork;
|
|
use parking_lot::Mutex as ParkingMutex;
|
|
|
|
use crate::config::RateLimitBps;
|
|
|
|
// Atomic per-user and per-CIDR accounting.
|
|
mod buckets;
|
|
// Immutable policy matching and sharded registries.
|
|
mod policy;
|
|
// Traffic lease accounting and cleanup.
|
|
mod lease;
|
|
// Runtime policy application and admission.
|
|
mod limiter;
|
|
// Epoch and arithmetic helpers.
|
|
mod helpers;
|
|
|
|
pub use helpers::next_refill_delay;
|
|
use helpers::{
|
|
auto_cidr_bucket_key, bytes_per_epoch, current_epoch, decrement_atomic_saturating,
|
|
now_epoch_secs,
|
|
};
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|
|
const REGISTRY_SHARDS: usize = 64;
|
|
const FAIR_EPOCH_MS: u64 = 20;
|
|
const MAX_BORROW_CHUNK_BYTES: u64 = 32 * 1024;
|
|
const CLEANUP_INTERVAL_SECS: u64 = 60;
|
|
const RESERVE_CAS_ATTEMPT_LIMIT: usize = 16;
|
|
const REFUND_CAS_ATTEMPT_LIMIT: usize = 8;
|
|
const PACKED_USAGE_BITS: u32 = 28;
|
|
const PACKED_USAGE_MASK: u64 = (1u64 << PACKED_USAGE_BITS) - 1;
|
|
const PACKED_EPOCH_MAX: u64 = (1u64 << (u64::BITS - PACKED_USAGE_BITS)) - 1;
|
|
|
|
/// Traffic direction used by rate-limit accounting.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum RateDirection {
|
|
/// Traffic received from a client.
|
|
Up,
|
|
/// Traffic sent to a client.
|
|
Down,
|
|
}
|
|
|
|
/// Result of an immediate traffic-budget request.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct TrafficConsumeResult {
|
|
/// Number of bytes granted by all applicable limits.
|
|
pub granted: u64,
|
|
/// Whether the per-user limit prevented a grant.
|
|
pub blocked_user: bool,
|
|
/// Whether the per-CIDR limit prevented a grant.
|
|
pub blocked_cidr: bool,
|
|
}
|
|
|
|
/// Process-wide traffic-limiter counters and gauges.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct TrafficLimiterMetricsSnapshot {
|
|
/// Per-user upload throttle events.
|
|
pub user_throttle_up_total: u64,
|
|
/// Per-user download throttle events.
|
|
pub user_throttle_down_total: u64,
|
|
/// Per-CIDR upload throttle events.
|
|
pub cidr_throttle_up_total: u64,
|
|
/// Per-CIDR download throttle events.
|
|
pub cidr_throttle_down_total: u64,
|
|
/// Per-user accumulated upload wait time in milliseconds.
|
|
pub user_wait_up_ms_total: u64,
|
|
/// Per-user accumulated download wait time in milliseconds.
|
|
pub user_wait_down_ms_total: u64,
|
|
/// Per-CIDR accumulated upload wait time in milliseconds.
|
|
pub cidr_wait_up_ms_total: u64,
|
|
/// Per-CIDR accumulated download wait time in milliseconds.
|
|
pub cidr_wait_down_ms_total: u64,
|
|
/// Per-user upload reservations that exhausted their CAS budget.
|
|
pub user_reserve_cas_retry_exhausted_up_total: u64,
|
|
/// Per-user download reservations that exhausted their CAS budget.
|
|
pub user_reserve_cas_retry_exhausted_down_total: u64,
|
|
/// Per-user upload refunds that exhausted their CAS budget.
|
|
pub user_refund_cas_retry_exhausted_up_total: u64,
|
|
/// Per-user download refunds that exhausted their CAS budget.
|
|
pub user_refund_cas_retry_exhausted_down_total: u64,
|
|
/// Per-CIDR upload reservations that exhausted their CAS budget.
|
|
pub cidr_reserve_cas_retry_exhausted_up_total: u64,
|
|
/// Per-CIDR download reservations that exhausted their CAS budget.
|
|
pub cidr_reserve_cas_retry_exhausted_down_total: u64,
|
|
/// Per-CIDR upload refunds that exhausted their CAS budget.
|
|
pub cidr_refund_cas_retry_exhausted_up_total: u64,
|
|
/// Per-CIDR download refunds that exhausted their CAS budget.
|
|
pub cidr_refund_cas_retry_exhausted_down_total: u64,
|
|
/// Active leases with a per-user rate limit.
|
|
pub user_active_leases: u64,
|
|
/// Active leases with a per-CIDR rate limit.
|
|
pub cidr_active_leases: u64,
|
|
/// Configured per-user rate-limit entries.
|
|
pub user_policy_entries: u64,
|
|
/// Configured per-CIDR rate-limit entries.
|
|
pub cidr_policy_entries: u64,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct CasContentionMetrics {
|
|
reserve_exhausted_total: AtomicU64,
|
|
refund_exhausted_total: AtomicU64,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct ScopeMetrics {
|
|
throttle_up_total: AtomicU64,
|
|
throttle_down_total: AtomicU64,
|
|
wait_up_ms_total: AtomicU64,
|
|
wait_down_ms_total: AtomicU64,
|
|
contention_up: Arc<CasContentionMetrics>,
|
|
contention_down: Arc<CasContentionMetrics>,
|
|
active_leases: AtomicU64,
|
|
policy_entries: AtomicU64,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct AtomicRatePair {
|
|
up_bps: AtomicU64,
|
|
down_bps: AtomicU64,
|
|
revision: ParkingMutex<u64>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct DirectionBucket {
|
|
state: AtomicU64,
|
|
contention: Arc<CasContentionMetrics>,
|
|
#[cfg(test)]
|
|
forced_reserve_failures: AtomicU64,
|
|
#[cfg(test)]
|
|
forced_refund_failures: AtomicU64,
|
|
#[cfg(test)]
|
|
reserve_cas_attempts: AtomicU64,
|
|
#[cfg(test)]
|
|
refund_cas_attempts: AtomicU64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum BucketReserveError {
|
|
StaleEpoch,
|
|
Contended,
|
|
RefundContended,
|
|
ReserveAndRefundContended,
|
|
FairShareContended,
|
|
}
|
|
|
|
impl BucketReserveError {
|
|
fn exhausted_reserve_budget(self) -> bool {
|
|
matches!(self, Self::Contended | Self::ReserveAndRefundContended)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum BucketRefundOutcome {
|
|
Complete,
|
|
Contended,
|
|
}
|
|
|
|
struct ReserveCasBudget {
|
|
remaining: usize,
|
|
}
|
|
|
|
struct BucketReservation {
|
|
granted: u64,
|
|
debit: Option<DirectionDebit>,
|
|
}
|
|
|
|
struct CidrReservation {
|
|
granted: u64,
|
|
aggregate_debit: Option<DirectionDebit>,
|
|
user_debit: Option<DirectionDebit>,
|
|
}
|
|
|
|
struct UserBucket {
|
|
rates: AtomicRatePair,
|
|
up: Arc<DirectionBucket>,
|
|
down: Arc<DirectionBucket>,
|
|
active_leases: AtomicU64,
|
|
}
|
|
|
|
struct CidrDirectionBucket {
|
|
used: Arc<DirectionBucket>,
|
|
active_users: Arc<DirectionBucket>,
|
|
}
|
|
|
|
struct CidrUserDirectionState {
|
|
used: Arc<DirectionBucket>,
|
|
}
|
|
|
|
struct CidrUserShare {
|
|
active_conns: AtomicU64,
|
|
up: CidrUserDirectionState,
|
|
down: CidrUserDirectionState,
|
|
}
|
|
|
|
struct CidrBucket {
|
|
rates: AtomicRatePair,
|
|
up: CidrDirectionBucket,
|
|
down: CidrDirectionBucket,
|
|
users: ShardedRegistry<CidrUserShare>,
|
|
active_leases: AtomicU64,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct CidrRule {
|
|
key: String,
|
|
cidr: IpNetwork,
|
|
limits: RateLimitBps,
|
|
prefix_len: u8,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct CidrAutoRule {
|
|
prefix_len: u8,
|
|
limits: RateLimitBps,
|
|
}
|
|
|
|
enum CidrPolicyMatch<'a> {
|
|
Explicit(&'a CidrRule),
|
|
Auto { key: String, limits: RateLimitBps },
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct PolicySnapshot {
|
|
revision: u64,
|
|
source_generation: u64,
|
|
user_limits: HashMap<String, RateLimitBps>,
|
|
cidr_rules_v4: Vec<CidrRule>,
|
|
cidr_rules_v6: Vec<CidrRule>,
|
|
cidr_auto_rules_v4: Vec<CidrAutoRule>,
|
|
cidr_auto_rules_v6: Vec<CidrAutoRule>,
|
|
cidr_rule_keys: HashSet<String>,
|
|
}
|
|
|
|
struct ShardedRegistry<T> {
|
|
shards: Box<[DashMap<String, Arc<T>>]>,
|
|
mask: usize,
|
|
}
|
|
|
|
struct TrafficLeaseBinding {
|
|
limiter: Arc<TrafficLimiter>,
|
|
revision: u64,
|
|
user_bucket: Option<Arc<UserBucket>>,
|
|
cidr_bucket: Option<Arc<CidrBucket>>,
|
|
cidr_user_key: Option<String>,
|
|
cidr_user_share: Option<Arc<CidrUserShare>>,
|
|
}
|
|
|
|
/// A live traffic-limiter binding for one authenticated client session.
|
|
pub struct TrafficLease {
|
|
limiter: Arc<TrafficLimiter>,
|
|
user: String,
|
|
client_ip: IpAddr,
|
|
binding: ArcSwap<TrafficLeaseBinding>,
|
|
refresh: ParkingMutex<()>,
|
|
}
|
|
|
|
/// Owns rate-limit policy, shared buckets, and limiter telemetry.
|
|
pub struct TrafficLimiter {
|
|
policy: ArcSwap<PolicySnapshot>,
|
|
policy_update: ParkingMutex<()>,
|
|
user_buckets: ShardedRegistry<UserBucket>,
|
|
cidr_buckets: ShardedRegistry<CidrBucket>,
|
|
user_scope: ScopeMetrics,
|
|
cidr_scope: ScopeMetrics,
|
|
last_cleanup_epoch_secs: AtomicU64,
|
|
}
|
|
|
|
struct DirectionDebit {
|
|
bucket: Arc<DirectionBucket>,
|
|
epoch: u64,
|
|
refundable: u64,
|
|
}
|
|
|
|
/// Refunds uncommitted shaping budget when an I/O attempt is cancelled.
|
|
#[must_use = "traffic reservations must be settled after the I/O attempt"]
|
|
pub(crate) struct TrafficReservation {
|
|
result: TrafficConsumeResult,
|
|
_binding: Arc<TrafficLeaseBinding>,
|
|
user: Option<DirectionDebit>,
|
|
cidr: Option<DirectionDebit>,
|
|
cidr_user: Option<DirectionDebit>,
|
|
}
|