mirror of
https://github.com/telemt/telemt.git
synced 2026-04-25 14:34:10 +03:00
Bounded ME Route fairness and IP-Cleanup-Backlog
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com> Signed-off-by: Alexey <247128645+axkurcom@users.noreply.github.com>
This commit is contained in:
@@ -17,6 +17,20 @@ fn remove_temp_config(path: &PathBuf) {
|
|||||||
let _ = fs::remove_file(path);
|
let _ = fs::remove_file(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn defaults_enable_byte_bounded_route_fairness() {
|
||||||
|
let cfg = ProxyConfig::default();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
cfg.general.me_route_fairshare_enabled,
|
||||||
|
"D2C route fairness must be enabled by default to bound queued bytes"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
cfg.general.me_route_backpressure_enabled,
|
||||||
|
"D2C route backpressure must be enabled by default to shed under sustained pressure"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn load_rejects_writer_cmd_capacity_above_upper_bound() {
|
fn load_rejects_writer_cmd_capacity_above_upper_bound() {
|
||||||
let path = write_temp_config(
|
let path = write_temp_config(
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ pub struct UserIpTracker {
|
|||||||
limit_mode: Arc<RwLock<UserMaxUniqueIpsMode>>,
|
limit_mode: Arc<RwLock<UserMaxUniqueIpsMode>>,
|
||||||
limit_window: Arc<RwLock<Duration>>,
|
limit_window: Arc<RwLock<Duration>>,
|
||||||
last_compact_epoch_secs: Arc<AtomicU64>,
|
last_compact_epoch_secs: Arc<AtomicU64>,
|
||||||
cleanup_queue: Arc<Mutex<Vec<(String, IpAddr)>>>,
|
cleanup_queue: Arc<Mutex<HashMap<(String, IpAddr), usize>>>,
|
||||||
cleanup_drain_lock: Arc<AsyncMutex<()>>,
|
cleanup_drain_lock: Arc<AsyncMutex<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,17 +45,21 @@ impl UserIpTracker {
|
|||||||
limit_mode: Arc::new(RwLock::new(UserMaxUniqueIpsMode::ActiveWindow)),
|
limit_mode: Arc::new(RwLock::new(UserMaxUniqueIpsMode::ActiveWindow)),
|
||||||
limit_window: Arc::new(RwLock::new(Duration::from_secs(30))),
|
limit_window: Arc::new(RwLock::new(Duration::from_secs(30))),
|
||||||
last_compact_epoch_secs: Arc::new(AtomicU64::new(0)),
|
last_compact_epoch_secs: Arc::new(AtomicU64::new(0)),
|
||||||
cleanup_queue: Arc::new(Mutex::new(Vec::new())),
|
cleanup_queue: Arc::new(Mutex::new(HashMap::new())),
|
||||||
cleanup_drain_lock: Arc::new(AsyncMutex::new(())),
|
cleanup_drain_lock: Arc::new(AsyncMutex::new(())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enqueue_cleanup(&self, user: String, ip: IpAddr) {
|
pub fn enqueue_cleanup(&self, user: String, ip: IpAddr) {
|
||||||
match self.cleanup_queue.lock() {
|
match self.cleanup_queue.lock() {
|
||||||
Ok(mut queue) => queue.push((user, ip)),
|
Ok(mut queue) => {
|
||||||
|
let count = queue.entry((user, ip)).or_insert(0);
|
||||||
|
*count = count.saturating_add(1);
|
||||||
|
}
|
||||||
Err(poisoned) => {
|
Err(poisoned) => {
|
||||||
let mut queue = poisoned.into_inner();
|
let mut queue = poisoned.into_inner();
|
||||||
queue.push((user.clone(), ip));
|
let count = queue.entry((user.clone(), ip)).or_insert(0);
|
||||||
|
*count = count.saturating_add(1);
|
||||||
self.cleanup_queue.clear_poison();
|
self.cleanup_queue.clear_poison();
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"UserIpTracker cleanup_queue lock poisoned; recovered and enqueued IP cleanup for {} ({})",
|
"UserIpTracker cleanup_queue lock poisoned; recovered and enqueued IP cleanup for {} ({})",
|
||||||
@@ -75,7 +79,9 @@ impl UserIpTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn cleanup_queue_mutex_for_tests(&self) -> Arc<Mutex<Vec<(String, IpAddr)>>> {
|
pub(crate) fn cleanup_queue_mutex_for_tests(
|
||||||
|
&self,
|
||||||
|
) -> Arc<Mutex<HashMap<(String, IpAddr), usize>>> {
|
||||||
Arc::clone(&self.cleanup_queue)
|
Arc::clone(&self.cleanup_queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,11 +111,14 @@ impl UserIpTracker {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut active_ips = self.active_ips.write().await;
|
let mut active_ips = self.active_ips.write().await;
|
||||||
for (user, ip) in to_remove {
|
for ((user, ip), pending_count) in to_remove {
|
||||||
|
if pending_count == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if let Some(user_ips) = active_ips.get_mut(&user) {
|
if let Some(user_ips) = active_ips.get_mut(&user) {
|
||||||
if let Some(count) = user_ips.get_mut(&ip) {
|
if let Some(count) = user_ips.get_mut(&ip) {
|
||||||
if *count > 1 {
|
if *count > pending_count {
|
||||||
*count -= 1;
|
*count -= pending_count;
|
||||||
} else {
|
} else {
|
||||||
user_ips.remove(&ip);
|
user_ips.remove(&ip);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -649,6 +649,25 @@ async fn duplicate_cleanup_entries_do_not_break_future_admission() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn duplicate_cleanup_entries_are_coalesced_until_drain() {
|
||||||
|
let tracker = UserIpTracker::new();
|
||||||
|
let ip = ip_from_idx(7150);
|
||||||
|
|
||||||
|
tracker.enqueue_cleanup("coalesced-cleanup".to_string(), ip);
|
||||||
|
tracker.enqueue_cleanup("coalesced-cleanup".to_string(), ip);
|
||||||
|
tracker.enqueue_cleanup("coalesced-cleanup".to_string(), ip);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
tracker.cleanup_queue_len_for_tests(),
|
||||||
|
1,
|
||||||
|
"duplicate queued cleanup entries must retain one allocation slot"
|
||||||
|
);
|
||||||
|
|
||||||
|
tracker.drain_cleanup_queue().await;
|
||||||
|
assert_eq!(tracker.cleanup_queue_len_for_tests(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn stress_repeated_queue_poison_recovery_preserves_admission_progress() {
|
async fn stress_repeated_queue_poison_recovery_preserves_admission_progress() {
|
||||||
let tracker = UserIpTracker::new();
|
let tracker = UserIpTracker::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user