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);
}
/// Returns the back element of the buffer
pub fn back(&self) -> Option<&T> {
self.buf.back()
}
/// Returns the number of elements in the buffer.
#[allow(unused)]
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.
use crate::{circular_buffer::CircularBuffer, log_message::LogMessage};
use crate::{circular_buffer::CircularBuffer, lazy_map_cleaner::TsSecs, log_message::LogMessage};
use std::sync::Mutex;
/// A thread-safe circular buffer for log messages.
@@ -58,4 +58,17 @@ impl LogBuffer {
let mut iter = buf.iter().rev();
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::{
assistant::{Tool, ToolExecutor, ToolResult},
concurrent_map::LazyMap,
lazy_map_cleaner::LazyMapCleaner,
limiter_sequence::{Limit, LimiterSequence},
log_format::LogFormatConfig,
log_message::{LogMessage, Origin},
@@ -13,7 +14,7 @@ use crate::{
use async_trait::async_trait;
use chrono::Utc;
use conf::Conf;
use std::fmt;
use std::{fmt, time::Duration};
use tokio::sync::mpsc::UnboundedSender;
use tracing::{error, info};
@@ -57,6 +58,9 @@ pub struct LogHandlerConfig {
/// Number of recent log messages to buffer per origin
#[conf(long, env, default_value = "64")]
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.
/// 0 = no logging, 1 = log only overall limiter, 2 = log routes + overall, 3 = log all.
#[conf(long, env, default_value = "0")]
@@ -86,6 +90,8 @@ pub struct LogHandler {
signal_alert_mq_tx: UnboundedSender<SignalAlertMessage>,
/// Log buffers keyed by origin (app + host). Lazily created.
log_buffers: LazyMap<Origin, LogBuffer>,
/// Clean up log buffers as origins get old
lazy_map_cleaner: LazyMapCleaner,
/// Routes with their associated limiter sets.
routes: Vec<(Route, LimiterSet)>,
/// Overall rate limiters applied after route checks pass.
@@ -101,17 +107,19 @@ impl LogHandler {
let routes = config
.routes
.iter()
.map(|route| (route.clone(), route.make_limiter_set()))
.map(|route| (route.clone(), route.make_limiter_set(config.max_origin_age)))
.collect();
let overall_limits = config.overall_limits.iter().collect();
let buffer_size = config.log_buffer_size;
let max_age = config.max_origin_age.as_secs() as i64;
Self {
config,
signal_alert_mq_tx,
log_buffers: LazyMap::new(move |_key| LogBuffer::new(buffer_size)),
lazy_map_cleaner: LazyMapCleaner::new(max_age),
routes,
overall_limits,
}
@@ -175,6 +183,8 @@ impl LogHandler {
}
}
let now = Utc::now();
// Get or create the buffer for this origin, then record the message
let formatted_text = self.log_buffers.get(&origin, |buffer| {
if rate_limit_result.is_err() {
@@ -186,7 +196,6 @@ impl LogHandler {
let mut text = String::with_capacity(4096);
let mut first_msg_len = 0;
let mut is_first = true;
let now = Utc::now();
buffer.push_back_and_drain(log_msg, |log_msg| {
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.
@@ -2,9 +2,11 @@
use crate::{
concurrent_map::LazyMap,
lazy_map_cleaner::LazyMapCleaner,
limiter_sequence::LimiterSequence,
log_message::{LogMessage, Origin},
};
use std::time::Duration;
/// Result of evaluating a limiter set.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -23,6 +25,8 @@ pub struct LimiterSet {
limiters: LazyMap<Origin, LimiterSequence>,
/// Global rate limiters (shared across all origins).
global_limiters: LimiterSequence,
/// Lazy map cleaner
lazy_map_cleaner: LazyMapCleaner,
}
impl LimiterSet {
@@ -30,10 +34,12 @@ impl LimiterSet {
pub fn new(
make_limiters: impl Fn() -> LimiterSequence + Send + Sync + 'static,
global_limiters: LimiterSequence,
max_origin_age: Duration,
) -> Self {
Self {
limiters: LazyMap::new(move |_key| make_limiters()),
global_limiters,
lazy_map_cleaner: LazyMapCleaner::new(max_origin_age.as_secs() as i64),
}
}
@@ -55,6 +61,9 @@ impl LimiterSet {
.map(LimitResult::Limiter)
});
self.lazy_map_cleaner
.maybe_clean(log_msg.get_timestamp_or_fallback(), &self.limiters);
if let Some(result) = origin_result {
return result;
}
+7 -2
View File
@@ -9,6 +9,7 @@ use crate::{
log_message::{Level, LogFilter},
};
use serde::Deserialize;
use std::time::Duration;
/// A route configuration for processing log messages.
///
@@ -51,10 +52,14 @@ fn default_alert_level() -> Level {
impl Route {
/// 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 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,
)
}
}
+8 -6
View File
@@ -1,5 +1,5 @@
use crate::concurrent_map::LazyMap;
use std::{sync::Mutex};
use std::sync::Mutex;
/// Trait for types that expose a timestamp in seconds.
/// For containers, this should be the oldest timestamp in the container.
@@ -27,11 +27,11 @@ impl LazyMapCleaner {
pub fn new(max_age: i64) -> Self {
Self {
max_age,
min_checkup_period: 3600,
min_checkup_period: 3600,
state: Default::default(),
}
}
/// Set the min checkup period (in seconds)
/// Defaults to 1 hour if unset
#[allow(dead_code)]
@@ -45,15 +45,17 @@ impl LazyMapCleaner {
/// 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>)
where K: Eq + std::hash::Hash + Clone,
V: TsSecs
where
K: Eq + std::hash::Hash + Clone,
V: TsSecs,
{
// Only lock if we can do so without contention, if someone else is checking to clean,
// we don't have to do so ourselves.
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 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 new_cleanup_size = map.retain(|_key, val| val.ts_secs() >= cutoff);
lk.last_cleanup_size = new_cleanup_size;
+7
View File
@@ -4,6 +4,7 @@
//! and the first one to do so is returned.
use crate::{
lazy_map_cleaner::TsSecs,
log_message::{LogFilter, LogMessage},
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 {
fn from_iter<T>(iter: T) -> Self
where
+23 -1
View File
@@ -1,6 +1,10 @@
//! 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 std::{
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
#[allow(dead_code)]
#[derive(Debug, Default)]
@@ -165,6 +178,8 @@ pub struct SourceLocationRateLimiter {
limiters: LazyMap<(Box<str>, Box<str>), MultiRateLimiter>,
/// Manages cleanup of the lazy map
lazy_map_cleaner: LazyMapCleaner,
/// Max evaluated timestamp
latest_ts_sec: AtomicI64,
}
impl SourceLocationRateLimiter {
@@ -172,6 +187,7 @@ impl SourceLocationRateLimiter {
Self {
limiters: LazyMap::with_capacity(8, move |_key| MultiRateLimiter::from(threshold)),
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(),
);
self.latest_ts_sec.fetch_max(ts_sec, Ordering::SeqCst);
let result = self.limiters.get(&key, |limiter| limiter.evaluate(ts_sec));
// Maybe prune the lazy map
@@ -191,6 +209,10 @@ impl SourceLocationRateLimiter {
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'