Split src/engine/mod.rs into handle/message/snapshot submodules

Pure code-motion refactor, no logic changes. Follows the repo convention
of one struct/enum + its impl per module file:

- engine/handle.rs: the left-right write/read plumbing (EngineOp,
  IndexWriter, IndexReadHandle, IndexReader, EngineHandle) and the actor
  loop (spawn/run) that owns EngineSnapshot.
- engine/message.rs: EngineQuery/EngineMessage/EngineResponse.
- engine/snapshot/: EngineSnapshot itself, split by concern into
  params.rs (declaration + auto-detect), metrics.rs (declare/apply/
  backfill), filter.rs (search-bar filter compilation/eval), sort.rs
  (sort-helper indexes), and query.rs (handle_query_inner + PromQL
  evaluation), with mod.rs holding the struct and the accessors shared
  across all of them.
- engine/tests.rs: the inline test module, kept as one file (per the
  task's own escape hatch) since most tests exercise ingest -> declare
  -> query across several of the new submodules at once rather than one
  in isolation.

Visibility was narrowed per call site: pub(super) where only sibling
snapshot submodules need an item, pub(crate) where engine::handle or
engine::tests (outside the snapshot subtree) also need it.

cargo build --all-targets and cargo test are green (291 passed, 1
ignored, same as before the split).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1ZJ3SrayzLzVndhVq9BmS
This commit is contained in:
Hugh Kaznowski
2026-09-06 18:36:53 +01:00
co-authored by Claude Sonnet 5
parent 3b3ace146a
commit f19fa50a25
10 changed files with 4517 additions and 4383 deletions
+301
View File
@@ -0,0 +1,301 @@
//! The left-right write/read plumbing around `EngineSnapshot`, the actor loop that owns it,
//! and the `EngineHandle` other tasks use to talk to that actor.
use crate::consts::{
INGEST_LAG_WATERMARK_MAX_ENTRIES, INGEST_LAG_WATERMARK_WINDOW_MS,
};
use crate::engine::alerts::{self, AlertRuntimeState};
use crate::engine::data_value::DataValue;
use crate::engine::index_key::IndexKey;
use crate::engine::message::EngineMessage;
use crate::engine::metric_primitive::{MetricPrimitive, Number};
use crate::engine::snapshot::params::{derive_log_stream_params_from_row, log_stream_header_transition};
use crate::engine::snapshot::{approx_log_bytes, EngineSnapshot};
use crate::engine::timestamp::Timestamp;
use crate::engine::{ClearParamsResult, IngestedLine};
use crate::config::SettingsHandle;
use left_right::Absorb;
use std::collections::{BTreeMap, HashMap};
use tokio::sync::{mpsc, oneshot};
struct EngineOp(Box<dyn FnMut(&mut EngineSnapshot) + Send + 'static>);
impl Absorb<EngineOp> for EngineSnapshot {
fn absorb_first(&mut self, operation: &mut EngineOp, _other: &Self) {
(operation.0)(self);
}
fn drop_first(self: Box<Self>) {}
fn sync_with(&mut self, first: &Self) {
*self = first.clone();
}
}
struct IndexWriter {
handle: left_right::WriteHandle<EngineSnapshot, EngineOp>,
}
impl IndexWriter {
fn mutate(&mut self, op: impl FnMut(&mut EngineSnapshot) + Send + 'static) {
self.handle.append(EngineOp(Box::new(op)));
self.handle.publish();
}
fn peek(&mut self) -> Option<left_right::ReadGuard<'_, EngineSnapshot>> {
self.handle.enter()
}
}
pub struct IndexReadHandle(left_right::ReadHandle<EngineSnapshot>);
impl IndexReadHandle {
pub(crate) fn enter(&self) -> Option<left_right::ReadGuard<'_, EngineSnapshot>> {
self.0.enter()
}
}
pub struct IndexReader {
factory: left_right::ReadHandleFactory<EngineSnapshot>,
}
impl Clone for IndexReader {
fn clone(&self) -> Self {
IndexReader { factory: self.factory.clone() }
}
}
impl IndexReader {
pub(crate) fn handle(&self) -> IndexReadHandle {
IndexReadHandle(self.factory.handle())
}
}
pub struct EngineHandle {
pub tx: mpsc::Sender<EngineMessage>,
pub reader: IndexReader,
}
impl Clone for EngineHandle {
fn clone(&self) -> Self {
EngineHandle { tx: self.tx.clone(), reader: self.reader.clone() }
}
}
impl EngineHandle {
pub async fn barrier(&self) {
let (respond_to, response) = oneshot::channel();
if self.tx.send(EngineMessage::Barrier(respond_to)).await.is_ok() {
let _ = response.await;
}
}
}
pub fn spawn(settings: SettingsHandle, channel_capacity: usize) -> EngineHandle {
let (tx, rx) = mpsc::channel(channel_capacity);
let (write, read) = left_right::new::<EngineSnapshot, EngineOp>();
let reader = IndexReader { factory: read.factory() };
tokio::spawn(run(rx, settings, IndexWriter { handle: write }));
EngineHandle { tx, reader }
}
#[cfg(test)]
pub(crate) fn test_handle(channel_capacity: usize) -> (EngineHandle, mpsc::Receiver<EngineMessage>) {
let (tx, rx) = mpsc::channel(channel_capacity);
let (_write, read) = left_right::new::<EngineSnapshot, EngineOp>();
(EngineHandle { tx, reader: IndexReader { factory: read.factory() } }, rx)
}
async fn run(mut rx: mpsc::Receiver<EngineMessage>, settings: SettingsHandle, mut write: IndexWriter) {
let mut max_logs: Option<usize> = None;
let mut metric_sample_sequence: u64 = 0;
let mut alert_states: HashMap<String, AlertRuntimeState> = HashMap::new();
let mut log_stream_header: Option<crate::engine::logfmt::LogStreamHeader> = None;
let mut ingest_lag_samples: Vec<(Timestamp, i64)> = Vec::new();
while let Some(message) = rx.recv().await {
match message {
EngineMessage::Ingest(line) => {
let IngestedLine { sequence, timestamp, text } = line;
let processed_at = Timestamp::now();
record_ingest_lag(
&mut ingest_lag_samples,
processed_at,
(processed_at.as_millis() - timestamp.as_millis()).max(0),
);
let ingest_lag_high_watermark_ms =
ingest_lag_high_watermark_ms(&ingest_lag_samples, processed_at);
let is_new_header = crate::engine::logfmt::LogStreamHeader::detect(&text).is_some();
log_stream_header = log_stream_header_transition(&log_stream_header, &text);
let row_header = if is_new_header { None } else { log_stream_header.clone() };
let (rules, ntfy_topic, ntfy_server) = {
let settings = settings.lock().unwrap();
(settings.alert_rules.clone(), settings.ntfy_topic.clone(), settings.ntfy_server.clone())
};
let text_for_op = text.clone();
write.mutate(move |snap: &mut EngineSnapshot| {
snap.approx_bytes_total += approx_log_bytes(&text_for_op);
snap.index.insert(
IndexKey::IngestedLog { sequence, timestamp },
DataValue::LogLiteral { text: text_for_op.clone() },
);
snap.log_count += 1;
if let Some(header) = row_header.as_ref() {
derive_log_stream_params_from_row(&mut snap.index, header, &text_for_op);
}
snap.auto_declare_params(&text_for_op);
snap.apply_all_metrics(sequence, timestamp, &text_for_op);
snap.update_sort_helper_indexes_on_ingest(sequence, timestamp, &text_for_op);
snap.purge_stale_sort_helper_indexes();
snap.evict_excess_logs(max_logs);
snap.ingest_lag_high_watermark_ms = ingest_lag_high_watermark_ms;
});
let fired = write
.peek()
.map(|guard| {
alerts::evaluate_alerts(&rules, &mut alert_states, &guard, sequence, timestamp, &text)
})
.unwrap_or_default();
for (rule, message) in fired {
tokio::spawn(alerts::deliver_for_rule(rule.name, message, ntfy_topic.clone(), ntfy_server.clone()));
}
}
EngineMessage::IngestMetricSample(sample) => {
let labels: BTreeMap<String, MetricPrimitive> = sample
.labels
.into_iter()
.map(|(name, value)| (name, MetricPrimitive::String(value)))
.collect();
let metric_name = sample.metric_name;
let sequence = metric_sample_sequence;
let timestamp = sample.timestamp;
let value = MetricPrimitive::Number(Number::Float(sample.value));
write.mutate(move |snap: &mut EngineSnapshot| {
snap.push_bucket_value(&metric_name, labels.clone(), sequence, timestamp, value.clone());
});
metric_sample_sequence += 1;
}
EngineMessage::SetMaxLogs(new_max) => {
max_logs = new_max;
write.mutate(move |snap: &mut EngineSnapshot| {
snap.evict_excess_logs(new_max);
});
}
EngineMessage::DeclareParam { request, respond_to } => {
let result = write
.peek()
.map(|guard| guard.validate_param_declaration(&request))
.unwrap_or_else(|| Err("engine is shutting down".to_string()));
let succeeded = result.is_ok();
let name = request.name.clone();
let request_for_op = request.clone();
write.mutate(move |snap: &mut EngineSnapshot| {
snap.sort_helper_indexes.remove(&name);
if succeeded {
snap.insert_param_declaration(request_for_op.clone());
}
});
let _ = respond_to.send(result);
}
EngineMessage::RemoveParamDeclaration { name, respond_to } => {
let existed = write
.peek()
.is_some_and(|guard| guard.index.contains_key(&IndexKey::ParamDeclaration { name: name.clone() }));
let name_for_op = name.clone();
write.mutate(move |snap: &mut EngineSnapshot| {
snap.index.remove(&IndexKey::ParamDeclaration { name: name_for_op.clone() });
snap.sort_helper_indexes.remove(&name_for_op);
});
let _ = respond_to.send(existed);
}
EngineMessage::ClearParams { respond_to } => {
let mut scratch = EngineSnapshot::from_index(
write.peek().map(|guard| guard.index.clone()).unwrap_or_default(),
);
let removed = scratch.clear_all_param_declarations();
let (reextracted, logs_considered) = scratch.reextract_params_from_retained_logs();
write.mutate(move |snap: &mut EngineSnapshot| {
snap.clear_all_param_declarations();
snap.reextract_params_from_retained_logs();
snap.sort_helper_indexes.clear();
});
let _ = respond_to.send(ClearParamsResult { removed, reextracted, logs_considered });
}
EngineMessage::DeclareMetric { request, respond_to } => {
let result = write
.peek()
.map(|guard| guard.validate_metric_request(&request).map(|_| ()))
.unwrap_or_else(|| Err("engine is shutting down".to_string()));
let succeeded = result.is_ok();
let request_for_op = request.clone();
write.mutate(move |snap: &mut EngineSnapshot| {
if succeeded {
let _ = snap.declare_metric(request_for_op.clone());
}
});
let _ = respond_to.send(result);
}
EngineMessage::UpdateMetric { name, request, respond_to } => {
let result = write
.peek()
.map(|guard| guard.validate_update_metric(&name, &request).map(|_| ()))
.unwrap_or_else(|| Err("engine is shutting down".to_string()));
let succeeded = result.is_ok();
let existing_name = name.clone();
let request_for_op = request.clone();
write.mutate(move |snap: &mut EngineSnapshot| {
if succeeded {
let _ = snap.update_metric(&existing_name, request_for_op.clone());
}
});
let _ = respond_to.send(result);
}
EngineMessage::RemoveMetricDeclaration { name, respond_to } => {
let existed = write
.peek()
.is_some_and(|guard| guard.index.contains_key(&IndexKey::MetricDeclaration { name: name.clone() }));
let name_for_op = name.clone();
write.mutate(move |snap: &mut EngineSnapshot| {
snap.remove_metric_declaration(&name_for_op);
});
let _ = respond_to.send(existed);
}
EngineMessage::EnsureSortHelperIndex { param_name, respond_to } => {
write.mutate(move |snap: &mut EngineSnapshot| {
snap.ensure_sort_helper_index_built(&param_name);
});
let _ = respond_to.send(());
}
EngineMessage::Barrier(respond_to) => {
let _ = respond_to.send(());
}
}
}
}
pub(crate) fn record_ingest_lag(samples: &mut Vec<(Timestamp, i64)>, now: Timestamp, lag_ms: i64) {
samples.retain(|(recorded_at, _)| now.as_millis() - recorded_at.as_millis() < INGEST_LAG_WATERMARK_WINDOW_MS);
if samples.len() < INGEST_LAG_WATERMARK_MAX_ENTRIES {
samples.push((now, lag_ms));
return;
}
let Some((min_index, &(_, min_lag))) = samples.iter().enumerate().min_by_key(|(_, (_, lag))| *lag) else {
return;
};
if lag_ms > min_lag {
samples[min_index] = (now, lag_ms);
}
}
pub(crate) fn ingest_lag_high_watermark_ms(samples: &[(Timestamp, i64)], now: Timestamp) -> i64 {
samples
.iter()
.filter(|(recorded_at, _)| now.as_millis() - recorded_at.as_millis() < INGEST_LAG_WATERMARK_WINDOW_MS)
.map(|(_, lag)| *lag)
.max()
.unwrap_or(0)
}