Move metric declarations and bucket computation to the profile actor (ticket 104)
Metric declare/update/remove, backfill, and per-line apply_all_metrics now run on the profile actor (engine::profile) instead of inline in the primary's ingest closure, per idea 0001's resolved decision that the primary handles params only. The profile actor reuses EngineSnapshot as its own snapshot type -- declare_metric/update_metric/apply_all_metrics work unchanged -- and mirrors the primary's param declarations into its own snapshot on every wake (and synchronously before handling a DeclareMetric/UpdateMetric message, since there's no ordering guarantee between the watch-wake and message branches in tokio::select!) so metric label/value resolution keeps working without storing a second copy of raw log text; backfill reads lines through its cloned primary IndexReader instead. EngineHandle gained a profile_reader field so MetricDeclarations/ BucketValues/Promql queries route to the profile actor's snapshot instead of the primary's -- without it, metrics could be declared but never queried, since the primary no longer computes or stores them. DeclareMetric/UpdateMetric/RemoveMetricDeclaration messages are still sent to the primary's mpsc channel and proxied to the profile actor by handle.rs's run() loop; EngineHandle's write-side API is otherwise unchanged (ticket 106 gives API handlers a direct route). Alert evaluation still reads the primary's (now metric-less) snapshot directly -- an accepted gap until ticket 105 moves it, per idea 0001's resolved decision that alerts move with metric computation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
033e8bbab5
commit
ce3e741cb0
+69
-50
@@ -61,7 +61,7 @@ impl IndexReadHandle {
|
||||
}
|
||||
|
||||
pub struct IndexReader {
|
||||
factory: left_right::ReadHandleFactory<EngineSnapshot>,
|
||||
pub(crate) factory: left_right::ReadHandleFactory<EngineSnapshot>,
|
||||
}
|
||||
|
||||
impl Clone for IndexReader {
|
||||
@@ -81,6 +81,11 @@ impl IndexReader {
|
||||
pub struct EngineHandle {
|
||||
pub tx: mpsc::Sender<EngineMessage>,
|
||||
pub reader: IndexReader,
|
||||
/// Read-only view of the profile actor's snapshot (`super::profile`), which owns metric
|
||||
/// declarations and bucket data (ticket 104) — `MetricDeclarations`/`BucketValues`/`Promql`
|
||||
/// queries read through this instead of `reader`. Not part of the write path; the profile
|
||||
/// actor's own `mpsc` channel (proxied through `EngineMessage`'s metric variants) is that.
|
||||
pub(crate) profile_reader: IndexReader,
|
||||
}
|
||||
|
||||
impl Clone for EngineHandle {
|
||||
@@ -88,6 +93,7 @@ impl Clone for EngineHandle {
|
||||
EngineHandle {
|
||||
tx: self.tx.clone(),
|
||||
reader: self.reader.clone(),
|
||||
profile_reader: self.profile_reader.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,13 +112,16 @@ impl EngineHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the handle/receiver/writer/version-receiver quadruple shared by `spawn` and, in
|
||||
/// tests, by callers that want the raw receiver without an actor loop draining it.
|
||||
/// Builds the primary actor's sender/receiver/reader/writer/version-receiver, shared by `spawn`
|
||||
/// and, in tests, by callers that want the raw receiver without an actor loop draining it.
|
||||
/// Stops short of building `EngineHandle` itself, since that also needs a profile-actor reader
|
||||
/// that doesn't exist until the caller spawns (or fakes, see `test_handle`) the profile actor.
|
||||
fn new_channel_and_handle(
|
||||
channel_capacity: usize,
|
||||
) -> (
|
||||
EngineHandle,
|
||||
mpsc::Sender<EngineMessage>,
|
||||
mpsc::Receiver<EngineMessage>,
|
||||
IndexReader,
|
||||
IndexWriter,
|
||||
watch::Receiver<u64>,
|
||||
) {
|
||||
@@ -123,8 +132,9 @@ fn new_channel_and_handle(
|
||||
};
|
||||
let (version_tx, version_rx) = watch::channel(0u64);
|
||||
(
|
||||
EngineHandle { tx, reader },
|
||||
tx,
|
||||
rx,
|
||||
reader,
|
||||
IndexWriter {
|
||||
handle: write,
|
||||
version: 0,
|
||||
@@ -135,9 +145,14 @@ fn new_channel_and_handle(
|
||||
}
|
||||
|
||||
pub fn spawn(settings: SettingsHandle, channel_capacity: usize) -> EngineHandle {
|
||||
let (handle, rx, write, version_rx) = new_channel_and_handle(channel_capacity);
|
||||
crate::engine::profile::spawn(handle.reader.clone(), version_rx);
|
||||
tokio::spawn(run(rx, settings, write));
|
||||
let (tx, rx, reader, write, version_rx) = new_channel_and_handle(channel_capacity);
|
||||
let profile = crate::engine::profile::spawn(reader.clone(), version_rx);
|
||||
let handle = EngineHandle {
|
||||
tx,
|
||||
reader,
|
||||
profile_reader: profile.reader.clone(),
|
||||
};
|
||||
tokio::spawn(run(rx, settings, write, profile));
|
||||
handle
|
||||
}
|
||||
|
||||
@@ -145,7 +160,14 @@ pub fn spawn(settings: SettingsHandle, channel_capacity: usize) -> EngineHandle
|
||||
pub(crate) fn test_handle(
|
||||
channel_capacity: usize,
|
||||
) -> (EngineHandle, mpsc::Receiver<EngineMessage>) {
|
||||
let (handle, rx, _write, _version_rx) = new_channel_and_handle(channel_capacity);
|
||||
let (tx, rx, reader, _write, _version_rx) = new_channel_and_handle(channel_capacity);
|
||||
let handle = EngineHandle {
|
||||
tx,
|
||||
reader,
|
||||
// No actor loop is spawned here at all, so nothing ever publishes -- an inert reader
|
||||
// over an always-empty snapshot is all a query through this handle could see anyway.
|
||||
profile_reader: crate::engine::profile::inert_reader(),
|
||||
};
|
||||
(handle, rx)
|
||||
}
|
||||
|
||||
@@ -156,9 +178,14 @@ pub(crate) fn spawn_with_profile_observer(
|
||||
settings: SettingsHandle,
|
||||
channel_capacity: usize,
|
||||
) -> (EngineHandle, tokio::sync::mpsc::UnboundedReceiver<u64>) {
|
||||
let (handle, rx, write, version_rx) = new_channel_and_handle(channel_capacity);
|
||||
let observed = crate::engine::profile::spawn_observed(handle.reader.clone(), version_rx);
|
||||
tokio::spawn(run(rx, settings, write));
|
||||
let (tx, rx, reader, write, version_rx) = new_channel_and_handle(channel_capacity);
|
||||
let (profile, observed) = crate::engine::profile::spawn_observed(reader.clone(), version_rx);
|
||||
let handle = EngineHandle {
|
||||
tx,
|
||||
reader,
|
||||
profile_reader: profile.reader.clone(),
|
||||
};
|
||||
tokio::spawn(run(rx, settings, write, profile));
|
||||
(handle, observed)
|
||||
}
|
||||
|
||||
@@ -166,6 +193,7 @@ async fn run(
|
||||
mut rx: mpsc::Receiver<EngineMessage>,
|
||||
settings: SettingsHandle,
|
||||
mut write: IndexWriter,
|
||||
profile: crate::engine::profile::ProfileHandle,
|
||||
) {
|
||||
let mut max_logs: Option<usize> = None;
|
||||
let mut metric_sample_sequence: u64 = 0;
|
||||
@@ -209,10 +237,10 @@ async fn run(
|
||||
|
||||
let text_for_op = Arc::clone(&text);
|
||||
write.mutate(move |snap: &mut EngineSnapshot| {
|
||||
// Two independently-callable stages (ticket 101): primary/param
|
||||
// extraction, then profile-scoped metric computation.
|
||||
// Primary/param extraction only (ticket 101) -- metric computation
|
||||
// (ticket 104) now happens on the profile actor, which wakes on this
|
||||
// publish and reads the newly-visible line itself.
|
||||
snap.ingest_params(sequence, timestamp, &text_for_op, row_header.as_ref());
|
||||
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);
|
||||
@@ -327,49 +355,40 @@ async fn run(
|
||||
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);
|
||||
// Metric declarations live on the profile actor (ticket 104), not here --
|
||||
// proxy the message rather than mutating the primary's snapshot.
|
||||
let _ = profile
|
||||
.tx
|
||||
.send(crate::engine::profile::ProfileMessage::DeclareMetric {
|
||||
request,
|
||||
respond_to,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
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);
|
||||
let _ = profile
|
||||
.tx
|
||||
.send(crate::engine::profile::ProfileMessage::UpdateMetric {
|
||||
name,
|
||||
request,
|
||||
respond_to,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
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);
|
||||
let _ = profile
|
||||
.tx
|
||||
.send(
|
||||
crate::engine::profile::ProfileMessage::RemoveMetricDeclaration {
|
||||
name,
|
||||
respond_to,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
EngineMessage::EnsureSortHelperIndex {
|
||||
param_name,
|
||||
|
||||
Reference in New Issue
Block a user