systematically use lazy map cleaner on lazy maps

This commit is contained in:
Chris Beck
2025-12-15 14:42:01 -07:00
parent 2306364bbb
commit 0d284b54e4
8 changed files with 89 additions and 13 deletions
+5
View File
@@ -38,6 +38,11 @@ impl<T> CircularBuffer<T> {
self.buf.push_back(value); self.buf.push_back(value);
} }
/// Returns the back element of the buffer
pub fn back(&self) -> Option<&T> {
self.buf.back()
}
/// Returns the number of elements in the buffer. /// Returns the number of elements in the buffer.
#[allow(unused)] #[allow(unused)]
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
+14 -1
View File
@@ -2,7 +2,7 @@
//! //!
//! Wraps a circular buffer with a synchronous mutex for fast, blocking access. //! Wraps a circular buffer with a synchronous mutex for fast, blocking access.
use crate::{circular_buffer::CircularBuffer, log_message::LogMessage}; use crate::{circular_buffer::CircularBuffer, lazy_map_cleaner::TsSecs, log_message::LogMessage};
use std::sync::Mutex; use std::sync::Mutex;
/// A thread-safe circular buffer for log messages. /// A thread-safe circular buffer for log messages.
@@ -58,4 +58,17 @@ impl LogBuffer {
let mut iter = buf.iter().rev(); let mut iter = buf.iter().rev();
f(&mut iter) f(&mut iter)
} }
/// Access the back of the buffer, if it exists. Returns None if not.
pub fn peek_back<R>(&self, f: impl FnOnce(&LogMessage) -> R) -> Option<R> {
let buf = self.buf.lock().unwrap();
Some(f(buf.back()?))
}
}
impl TsSecs for LogBuffer {
fn ts_secs(&self) -> i64 {
self.peek_back(|log| log.get_timestamp_or_fallback())
.unwrap_or(0)
}
} }
+16 -3
View File
@@ -6,6 +6,7 @@ use super::{
use crate::{ use crate::{
assistant::{Tool, ToolExecutor, ToolResult}, assistant::{Tool, ToolExecutor, ToolResult},
concurrent_map::LazyMap, concurrent_map::LazyMap,
lazy_map_cleaner::LazyMapCleaner,
limiter_sequence::{Limit, LimiterSequence}, limiter_sequence::{Limit, LimiterSequence},
log_format::LogFormatConfig, log_format::LogFormatConfig,
log_message::{LogMessage, Origin}, log_message::{LogMessage, Origin},
@@ -13,7 +14,7 @@ use crate::{
use async_trait::async_trait; use async_trait::async_trait;
use chrono::Utc; use chrono::Utc;
use conf::Conf; use conf::Conf;
use std::fmt; use std::{fmt, time::Duration};
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use tracing::{error, info}; use tracing::{error, info};
@@ -57,6 +58,9 @@ pub struct LogHandlerConfig {
/// Number of recent log messages to buffer per origin /// Number of recent log messages to buffer per origin
#[conf(long, env, default_value = "64")] #[conf(long, env, default_value = "64")]
pub log_buffer_size: usize, pub log_buffer_size: usize,
/// Max age of an origin, after this we remove it to reclaim memory
#[conf(long, env, value_parser = conf_extra::parse_duration, default_value = "3d")]
pub max_origin_age: Duration,
/// Debug logging level for suppressed messages. /// Debug logging level for suppressed messages.
/// 0 = no logging, 1 = log only overall limiter, 2 = log routes + overall, 3 = log all. /// 0 = no logging, 1 = log only overall limiter, 2 = log routes + overall, 3 = log all.
#[conf(long, env, default_value = "0")] #[conf(long, env, default_value = "0")]
@@ -86,6 +90,8 @@ pub struct LogHandler {
signal_alert_mq_tx: UnboundedSender<SignalAlertMessage>, signal_alert_mq_tx: UnboundedSender<SignalAlertMessage>,
/// Log buffers keyed by origin (app + host). Lazily created. /// Log buffers keyed by origin (app + host). Lazily created.
log_buffers: LazyMap<Origin, LogBuffer>, log_buffers: LazyMap<Origin, LogBuffer>,
/// Clean up log buffers as origins get old
lazy_map_cleaner: LazyMapCleaner,
/// Routes with their associated limiter sets. /// Routes with their associated limiter sets.
routes: Vec<(Route, LimiterSet)>, routes: Vec<(Route, LimiterSet)>,
/// Overall rate limiters applied after route checks pass. /// Overall rate limiters applied after route checks pass.
@@ -101,17 +107,19 @@ impl LogHandler {
let routes = config let routes = config
.routes .routes
.iter() .iter()
.map(|route| (route.clone(), route.make_limiter_set())) .map(|route| (route.clone(), route.make_limiter_set(config.max_origin_age)))
.collect(); .collect();
let overall_limits = config.overall_limits.iter().collect(); let overall_limits = config.overall_limits.iter().collect();
let buffer_size = config.log_buffer_size; let buffer_size = config.log_buffer_size;
let max_age = config.max_origin_age.as_secs() as i64;
Self { Self {
config, config,
signal_alert_mq_tx, signal_alert_mq_tx,
log_buffers: LazyMap::new(move |_key| LogBuffer::new(buffer_size)), log_buffers: LazyMap::new(move |_key| LogBuffer::new(buffer_size)),
lazy_map_cleaner: LazyMapCleaner::new(max_age),
routes, routes,
overall_limits, overall_limits,
} }
@@ -175,6 +183,8 @@ impl LogHandler {
} }
} }
let now = Utc::now();
// Get or create the buffer for this origin, then record the message // Get or create the buffer for this origin, then record the message
let formatted_text = self.log_buffers.get(&origin, |buffer| { let formatted_text = self.log_buffers.get(&origin, |buffer| {
if rate_limit_result.is_err() { if rate_limit_result.is_err() {
@@ -186,7 +196,6 @@ impl LogHandler {
let mut text = String::with_capacity(4096); let mut text = String::with_capacity(4096);
let mut first_msg_len = 0; let mut first_msg_len = 0;
let mut is_first = true; let mut is_first = true;
let now = Utc::now();
buffer.push_back_and_drain(log_msg, |log_msg| { buffer.push_back_and_drain(log_msg, |log_msg| {
self.config self.config
@@ -218,6 +227,10 @@ impl LogHandler {
); );
} }
} }
// Maybe cleanup old log buffers
self.lazy_map_cleaner
.maybe_clean(now.timestamp_millis(), &self.log_buffers);
} }
/// Check if a log message passes all rate limiters. /// Check if a log message passes all rate limiters.
@@ -2,9 +2,11 @@
use crate::{ use crate::{
concurrent_map::LazyMap, concurrent_map::LazyMap,
lazy_map_cleaner::LazyMapCleaner,
limiter_sequence::LimiterSequence, limiter_sequence::LimiterSequence,
log_message::{LogMessage, Origin}, log_message::{LogMessage, Origin},
}; };
use std::time::Duration;
/// Result of evaluating a limiter set. /// Result of evaluating a limiter set.
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -23,6 +25,8 @@ pub struct LimiterSet {
limiters: LazyMap<Origin, LimiterSequence>, limiters: LazyMap<Origin, LimiterSequence>,
/// Global rate limiters (shared across all origins). /// Global rate limiters (shared across all origins).
global_limiters: LimiterSequence, global_limiters: LimiterSequence,
/// Lazy map cleaner
lazy_map_cleaner: LazyMapCleaner,
} }
impl LimiterSet { impl LimiterSet {
@@ -30,10 +34,12 @@ impl LimiterSet {
pub fn new( pub fn new(
make_limiters: impl Fn() -> LimiterSequence + Send + Sync + 'static, make_limiters: impl Fn() -> LimiterSequence + Send + Sync + 'static,
global_limiters: LimiterSequence, global_limiters: LimiterSequence,
max_origin_age: Duration,
) -> Self { ) -> Self {
Self { Self {
limiters: LazyMap::new(move |_key| make_limiters()), limiters: LazyMap::new(move |_key| make_limiters()),
global_limiters, global_limiters,
lazy_map_cleaner: LazyMapCleaner::new(max_origin_age.as_secs() as i64),
} }
} }
@@ -55,6 +61,9 @@ impl LimiterSet {
.map(LimitResult::Limiter) .map(LimitResult::Limiter)
}); });
self.lazy_map_cleaner
.maybe_clean(log_msg.get_timestamp_or_fallback(), &self.limiters);
if let Some(result) = origin_result { if let Some(result) = origin_result {
return result; return result;
} }
+7 -2
View File
@@ -9,6 +9,7 @@ use crate::{
log_message::{Level, LogFilter}, log_message::{Level, LogFilter},
}; };
use serde::Deserialize; use serde::Deserialize;
use std::time::Duration;
/// A route configuration for processing log messages. /// A route configuration for processing log messages.
/// ///
@@ -51,10 +52,14 @@ fn default_alert_level() -> Level {
impl Route { impl Route {
/// Create a limiter set from this route's limit configurations. /// Create a limiter set from this route's limit configurations.
pub fn make_limiter_set(&self) -> LimiterSet { pub fn make_limiter_set(&self, max_origin_age: Duration) -> LimiterSet {
let limits = self.limits.clone(); let limits = self.limits.clone();
let global_limiters = self.global_limits.iter().collect(); let global_limiters = self.global_limits.iter().collect();
LimiterSet::new(move || limits.iter().collect(), global_limiters) LimiterSet::new(
move || limits.iter().collect(),
global_limiters,
max_origin_age,
)
} }
} }
+7 -5
View File
@@ -1,5 +1,5 @@
use crate::concurrent_map::LazyMap; use crate::concurrent_map::LazyMap;
use std::{sync::Mutex}; use std::sync::Mutex;
/// Trait for types that expose a timestamp in seconds. /// Trait for types that expose a timestamp in seconds.
/// For containers, this should be the oldest timestamp in the container. /// For containers, this should be the oldest timestamp in the container.
@@ -27,7 +27,7 @@ impl LazyMapCleaner {
pub fn new(max_age: i64) -> Self { pub fn new(max_age: i64) -> Self {
Self { Self {
max_age, max_age,
min_checkup_period: 3600, min_checkup_period: 3600,
state: Default::default(), state: Default::default(),
} }
} }
@@ -45,15 +45,17 @@ impl LazyMapCleaner {
/// Maybe clean a lazy map, if we have met conditions to do so /// Maybe clean a lazy map, if we have met conditions to do so
pub fn maybe_clean<K, V>(&self, now: i64, map: &LazyMap<K, V>) pub fn maybe_clean<K, V>(&self, now: i64, map: &LazyMap<K, V>)
where K: Eq + std::hash::Hash + Clone, where
V: TsSecs K: Eq + std::hash::Hash + Clone,
V: TsSecs,
{ {
// Only lock if we can do so without contention, if someone else is checking to clean, // Only lock if we can do so without contention, if someone else is checking to clean,
// we don't have to do so ourselves. // we don't have to do so ourselves.
if let Ok(mut lk) = self.state.try_lock() { if let Ok(mut lk) = self.state.try_lock() {
// If the last cleanup size is 0, we'll still treat it as 1 and only look to clean if we reach 2. // If the last cleanup size is 0, we'll still treat it as 1 and only look to clean if we reach 2.
if map.len() >= lk.last_cleanup_size.max(1) * 2 if map.len() >= lk.last_cleanup_size.max(1) * 2
|| lk.last_checkup + self.min_checkup_period < now { || lk.last_checkup + self.min_checkup_period < now
{
let cutoff = now - self.max_age; let cutoff = now - self.max_age;
let new_cleanup_size = map.retain(|_key, val| val.ts_secs() >= cutoff); let new_cleanup_size = map.retain(|_key, val| val.ts_secs() >= cutoff);
lk.last_cleanup_size = new_cleanup_size; lk.last_cleanup_size = new_cleanup_size;
+7
View File
@@ -4,6 +4,7 @@
//! and the first one to do so is returned. //! and the first one to do so is returned.
use crate::{ use crate::{
lazy_map_cleaner::TsSecs,
log_message::{LogFilter, LogMessage}, log_message::{LogFilter, LogMessage},
rate_limiter::{Limiter, RateThreshold}, rate_limiter::{Limiter, RateThreshold},
}; };
@@ -65,6 +66,12 @@ impl LimiterSequence {
} }
} }
impl TsSecs for LimiterSequence {
fn ts_secs(&self) -> i64 {
self.0.iter().map(|(_f, l)| l.ts_secs()).max().unwrap_or(0)
}
}
impl<'a> FromIterator<&'a Limit> for LimiterSequence { impl<'a> FromIterator<&'a Limit> for LimiterSequence {
fn from_iter<T>(iter: T) -> Self fn from_iter<T>(iter: T) -> Self
where where
+23 -1
View File
@@ -1,6 +1,10 @@
//! Rate limiting for log alerts. //! Rate limiting for log alerts.
use crate::{concurrent_map::LazyMap, lazy_map_cleaner::{LazyMapCleaner, TsSecs}, log_message::LogMessage}; use crate::{
concurrent_map::LazyMap,
lazy_map_cleaner::{LazyMapCleaner, TsSecs},
log_message::LogMessage,
};
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{
str::FromStr, str::FromStr,
@@ -127,6 +131,15 @@ impl Limiter {
} }
} }
impl TsSecs for Limiter {
fn ts_secs(&self) -> i64 {
match self {
Self::Multi(m) => m.get_latest(),
Self::SourceLocation(s) => s.get_latest(),
}
}
}
/// A rate limiter containing a single counter, and a minimum time window for the next event to pass /// A rate limiter containing a single counter, and a minimum time window for the next event to pass
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug, Default)] #[derive(Debug, Default)]
@@ -165,6 +178,8 @@ pub struct SourceLocationRateLimiter {
limiters: LazyMap<(Box<str>, Box<str>), MultiRateLimiter>, limiters: LazyMap<(Box<str>, Box<str>), MultiRateLimiter>,
/// Manages cleanup of the lazy map /// Manages cleanup of the lazy map
lazy_map_cleaner: LazyMapCleaner, lazy_map_cleaner: LazyMapCleaner,
/// Max evaluated timestamp
latest_ts_sec: AtomicI64,
} }
impl SourceLocationRateLimiter { impl SourceLocationRateLimiter {
@@ -172,6 +187,7 @@ impl SourceLocationRateLimiter {
Self { Self {
limiters: LazyMap::with_capacity(8, move |_key| MultiRateLimiter::from(threshold)), limiters: LazyMap::with_capacity(8, move |_key| MultiRateLimiter::from(threshold)),
lazy_map_cleaner: LazyMapCleaner::new(threshold.duration.as_secs() as i64), lazy_map_cleaner: LazyMapCleaner::new(threshold.duration.as_secs() as i64),
latest_ts_sec: Default::default(),
} }
} }
@@ -184,6 +200,8 @@ impl SourceLocationRateLimiter {
line.to_owned().into_boxed_str(), line.to_owned().into_boxed_str(),
); );
self.latest_ts_sec.fetch_max(ts_sec, Ordering::SeqCst);
let result = self.limiters.get(&key, |limiter| limiter.evaluate(ts_sec)); let result = self.limiters.get(&key, |limiter| limiter.evaluate(ts_sec));
// Maybe prune the lazy map // Maybe prune the lazy map
@@ -191,6 +209,10 @@ impl SourceLocationRateLimiter {
result result
} }
pub fn get_latest(&self) -> i64 {
self.latest_ts_sec.load(Ordering::SeqCst)
}
} }
/// Implements rate-limiting criteria such as 'at least n in the last w seconds' /// Implements rate-limiting criteria such as 'at least n in the last w seconds'