Add settings-configurable cap on logs kept in memory (ticket 026)

Settings.max_logs (default None = unlimited) is pushed to the engine
via a new EngineMessage::SetMaxLogs, enforced on every ingest and
whenever the setting changes at runtime (no restart required).
Eviction drops the oldest (lowest-sequence) logs; sequence numbering
isn't renumbered and buckets already derived from an evicted log are
left in place. Exposed via PATCH /api/settings (max_logs: number |
null) and a NavMenu numeric field (blank = unlimited).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
Hugh Kaznowski
2026-09-05 15:12:12 +01:00
co-authored by Claude Sonnet 5
parent aaa5596030
commit 1d0f324c43
8 changed files with 261 additions and 16 deletions
+75
View File
@@ -96,6 +96,11 @@ pub enum EngineMessage {
name: String,
respond_to: oneshot::Sender<bool>,
},
/// Sets the cap on ingested logs kept in memory (ticket 026). `None`
/// means unlimited. Sent on startup (from `Settings::max_logs`) and
/// whenever the setting changes at runtime, so the cap takes effect on
/// the next ingest without a restart.
SetMaxLogs(Option<usize>),
Query {
query: EngineQuery,
respond_to: oneshot::Sender<EngineResponse>,
@@ -129,6 +134,8 @@ pub type EngineHandle = mpsc::Sender<EngineMessage>;
/// have to clone the index to answer a query.
pub async fn run(mut rx: mpsc::Receiver<EngineMessage>) {
let mut index: BTreeMap<IndexKey, DataValue> = BTreeMap::new();
let mut max_logs: Option<usize> = None;
let mut log_count: usize = 0;
while let Some(message) = rx.recv().await {
match message {
@@ -138,7 +145,13 @@ pub async fn run(mut rx: mpsc::Receiver<EngineMessage>) {
IndexKey::IngestedLog { sequence, timestamp },
DataValue::LogLiteral { text: text.clone() },
);
log_count += 1;
apply_all_metrics(&mut index, sequence, timestamp, &text);
evict_excess_logs(&mut index, &mut log_count, max_logs);
}
EngineMessage::SetMaxLogs(new_max) => {
max_logs = new_max;
evict_excess_logs(&mut index, &mut log_count, max_logs);
}
EngineMessage::DeclareParam { request, respond_to } => {
let result = declare_param(&mut index, request);
@@ -656,6 +669,30 @@ fn derive_log_params(
params
}
/// Evicts the oldest (lowest-`sequence`) ingested logs until `log_count` is
/// back at or under `max_logs`. `sequence` numbering is not renumbered on
/// eviction, and any `Bucket` values already derived from an evicted log are
/// left in place — buckets are aggregate data, not raw logs, so they're not
/// retroactively pruned.
fn evict_excess_logs(
index: &mut BTreeMap<IndexKey, DataValue>,
log_count: &mut usize,
max_logs: Option<usize>,
) {
let Some(max_logs) = max_logs else { return };
while *log_count > max_logs {
let Some(oldest_key) = index
.range(IndexKey::Start..IndexKey::LogParamSplit)
.next()
.map(|(key, _)| key.clone())
else {
break;
};
index.remove(&oldest_key);
*log_count -= 1;
}
}
fn logs_range(index: &BTreeMap<IndexKey, DataValue>) -> impl Iterator<Item = (u64, Timestamp, &str)> {
index.range(IndexKey::Start..IndexKey::LogParamSplit).filter_map(|(key, value)| {
match (key, value) {
@@ -991,6 +1028,44 @@ mod tests {
assert!(index.is_empty());
}
#[test]
fn evict_excess_logs_drops_oldest_sequences_first() {
let mut index = BTreeMap::new();
let mut log_count = 0;
for sequence in 0..5u64 {
index.insert(
IndexKey::IngestedLog { sequence, timestamp: Timestamp::from_millis(sequence as i64) },
DataValue::LogLiteral { text: format!("line {sequence}") },
);
log_count += 1;
}
evict_excess_logs(&mut index, &mut log_count, Some(3));
assert_eq!(log_count, 3);
let remaining: Vec<_> = logs_range(&index).map(|(seq, _, _)| seq).collect();
assert_eq!(remaining, vec![2, 3, 4]);
}
#[test]
fn evict_excess_logs_is_noop_when_unlimited_or_under_cap() {
let mut index = BTreeMap::new();
let mut log_count = 0;
for sequence in 0..3u64 {
index.insert(
IndexKey::IngestedLog { sequence, timestamp: Timestamp::from_millis(sequence as i64) },
DataValue::LogLiteral { text: "x".to_string() },
);
log_count += 1;
}
evict_excess_logs(&mut index, &mut log_count, None);
assert_eq!(log_count, 3);
evict_excess_logs(&mut index, &mut log_count, Some(10));
assert_eq!(log_count, 3);
}
#[test]
fn declarations_stay_within_their_range() {
let mut index = BTreeMap::new();