Left-right single-writer, multi-reader index

This commit is contained in:
Hugh Kaznowski
2026-09-06 16:05:04 +01:00
parent 9fc7540a5b
commit 1c635bc530
9 changed files with 684 additions and 203 deletions
+21 -15
View File
@@ -400,6 +400,7 @@ pub async fn load_into_engine(engine: &EngineHandle, profile: &Profile) {
let request: DeclareParamRequest = record.into();
let name = request.name.clone();
if engine
.tx
.send(EngineMessage::DeclareParam { request, respond_to })
.await
.is_err()
@@ -425,6 +426,7 @@ pub async fn load_into_engine(engine: &EngineHandle, profile: &Profile) {
let request: DeclareMetricRequest = record.into();
let name = request.name.clone();
if engine
.tx
.send(EngineMessage::DeclareMetric { request, respond_to })
.await
.is_err()
@@ -818,7 +820,6 @@ mod tests {
use crate::dto::LogsParams;
use crate::engine::{EngineMessage, EngineQuery, EngineResponse, IngestedLine};
use crate::timestamp::Timestamp;
use tokio::sync::mpsc;
fn param_request(name: &str, pattern: &str, capture_group: &str) -> DeclareParamRequest {
DeclareParamRequest {
@@ -844,10 +845,14 @@ mod tests {
}
}
/// Ticket 075: reads no longer go through `EngineHandle::tx` — this reads straight off
/// `EngineHandle::reader_factory`, same as the real API layer. Only used here for
/// `ParamDeclarations`/`MetricDeclarations` lookups, neither of which ever needs the
/// `EnsureSortHelperIndex` round-trip a sorted `Logs` query would (see `api.rs`'s own `ask`).
async fn ask(engine: &EngineHandle, query: EngineQuery) -> EngineResponse {
let (respond_to, response) = oneshot::channel();
engine.send(EngineMessage::Query { query, respond_to }).await.unwrap();
response.await.unwrap()
let read_handle = engine.reader_factory.handle();
let guard = read_handle.enter().expect("engine unavailable");
crate::engine::handle_query_ro(&guard, query)
}
#[test]
@@ -939,7 +944,6 @@ mod tests {
async fn seeded_metric_renders_real_error_vs_other_data() {
use crate::engine::{self, EngineMessage, EngineQuery, EngineResponse, IngestedLine};
use crate::timestamp::Timestamp;
use tokio::sync::mpsc;
let dir = tempfile::tempdir().unwrap();
let settings_path = dir.path().join("settings.json");
@@ -947,13 +951,13 @@ mod tests {
settings.ensure_default_profile(&settings_path).unwrap();
let default_profile = settings.profile(DEFAULT_PROFILE_NAME).unwrap().clone();
let (engine_tx, engine_rx) = mpsc::channel(128);
let settings_handle = std::sync::Arc::new(std::sync::Mutex::new(settings.clone()));
tokio::spawn(engine::run(engine_rx, settings_handle));
let engine_tx = engine::spawn(settings_handle, 128);
load_into_engine(&engine_tx, &default_profile).await;
for (sequence, text) in [(0u64, "all good"), (1, "an ERROR happened"), (2, "fine again")] {
engine_tx
.tx
.send(EngineMessage::Ingest(IngestedLine {
sequence,
timestamp: Timestamp::from_millis(sequence as i64),
@@ -962,6 +966,7 @@ mod tests {
.await
.unwrap();
}
engine_tx.barrier().await;
let response = ask(
&engine_tx,
@@ -1143,13 +1148,13 @@ mod tests {
let profile = settings.profile(&active_name).unwrap().clone();
// Spawn a fresh engine and replay the profile's declarations into it.
let (engine_tx, engine_rx) = mpsc::channel(16);
let settings_handle = std::sync::Arc::new(std::sync::Mutex::new(settings.clone()));
tokio::spawn(crate::engine::run(engine_rx, settings_handle));
let engine_tx = crate::engine::spawn(settings_handle, 16);
load_into_engine(&engine_tx, &profile).await;
// Ingest a line and confirm the engine works with the replayed declarations (no re-declaration needed).
engine_tx
.tx
.send(EngineMessage::Ingest(IngestedLine {
sequence: 0,
timestamp: Timestamp::from_millis(0),
@@ -1157,6 +1162,7 @@ mod tests {
}))
.await
.unwrap();
engine_tx.barrier().await;
let params = match ask(&engine_tx, EngineQuery::ParamDeclarations).await {
EngineResponse::ParamDeclarations(params) => params,
@@ -1308,12 +1314,12 @@ mod tests {
// Replaying into a fresh engine behaves identically to before migration: the metric still ingests and buckets.
let profile = profile.clone();
let (engine_tx, engine_rx) = mpsc::channel(16);
let settings_handle = std::sync::Arc::new(std::sync::Mutex::new(Settings::default()));
tokio::runtime::Runtime::new().unwrap().block_on(async {
tokio::spawn(crate::engine::run(engine_rx, settings_handle));
let engine_tx = crate::engine::spawn(settings_handle, 16);
load_into_engine(&engine_tx, &profile).await;
engine_tx
.tx
.send(EngineMessage::Ingest(IngestedLine {
sequence: 0,
timestamp: Timestamp::from_millis(0),
@@ -1489,10 +1495,10 @@ mod tests {
// Simulate the auto-derive path: ingest a line carrying a logfmt key before any param is
// manually declared, so `dx` only exists as engine-internal auto-derived state.
let (engine_tx, engine_rx) = mpsc::channel(16);
let settings_handle = std::sync::Arc::new(std::sync::Mutex::new(settings.clone()));
tokio::spawn(crate::engine::run(engine_rx, settings_handle));
let engine_tx = crate::engine::spawn(settings_handle, 16);
engine_tx
.tx
.send(EngineMessage::Ingest(IngestedLine {
sequence: 0,
timestamp: Timestamp::from_millis(0),
@@ -1506,6 +1512,7 @@ mod tests {
let metric_req = metric_request("direction_x", r"dx=(?P<dx>\S+) moved", "dx");
let (respond_to, response) = oneshot::channel();
engine_tx
.tx
.send(EngineMessage::DeclareMetric { request: metric_req.clone(), respond_to })
.await
.unwrap();
@@ -1536,9 +1543,8 @@ mod tests {
assert_eq!(profile.params.len(), 1);
assert_eq!(profile.params[0].name, "dx");
let (fresh_engine_tx, fresh_engine_rx) = mpsc::channel(16);
let fresh_settings_handle = std::sync::Arc::new(std::sync::Mutex::new(Settings::default()));
tokio::spawn(crate::engine::run(fresh_engine_rx, fresh_settings_handle));
let fresh_engine_tx = crate::engine::spawn(fresh_settings_handle, 16);
load_into_engine(&fresh_engine_tx, &profile).await;
let EngineResponse::MetricDeclarations(metrics) =