systematically use lazy map cleaner on lazy maps
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user