mod alert;
mod api;
mod config;
#[cfg(test)]
mod e2e;
mod json;
mod mcp;
mod pipeline;
mod proxy;
mod receiver;
mod telemetry;
mod term;
mod tui;
mod ui;
mod update;
use std::io::IsTerminal;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::sync::atomic::Ordering::Relaxed;
use std::time::Duration;
use axum::Router;
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use clap::{Arg, ArgAction, ArgMatches, value_parser};
use config::Config;
fn cli() -> clap::Command {
clap::Command::new("mira")
.version(env!("CARGO_PKG_VERSION"))
.about("OTLP in, immutable Arrow blocks out, one binary.")
.after_help(
"Flags override the config file, which overrides the defaults. Every value\n\
can also come from the file via ${env:VAR} — see https://miradb.dev/config/.",
)
.arg(config_arg())
.arg(
Arg::new("node")
.long("node")
.value_name("NAME")
.help("this replica's name, hashed into its block directory names"),
)
.arg(
Arg::new("grpc")
.long("grpc")
.value_name("ADDR")
.value_parser(value_parser!(SocketAddr))
.help("where OTLP/gRPC listens"),
)
.arg(http_arg())
.arg(data_dir_arg())
.arg(
Arg::new("retention")
.long("retention")
.value_name("DURATION")
.value_parser(config::duration)
.help("how long a block is kept before it is deleted"),
)
.arg(offload_arg())
.arg(max_request_bytes_arg())
.arg(
Arg::new("queue")
.long("queue")
.value_name("N")
.value_parser(config::positive)
.help("exports queued for one signal before the next has to wait"),
)
.arg(
Arg::new("shards")
.long("shards")
.value_name("N")
.value_parser(config::whole)
.help("flushers per signal, or 0 for one per two cores"),
)
.arg(
Arg::new("telemetry-interval")
.long("telemetry-interval")
.value_name("DURATION")
.value_parser(config::duration)
.help("how often --self-telemetry samples this node's counters"),
)
.arg(
Arg::new("alerts")
.long("alerts")
.value_name("FILE")
.value_parser(value_parser!(PathBuf))
.help("KYAML alerting rules; absent means alerting is off"),
)
.arg(
Arg::new("wal")
.long("wal")
.action(ArgAction::SetTrue)
.help("acknowledge on the write-ahead log rather than on the block"),
)
.arg(
Arg::new("self-telemetry")
.long("self-telemetry")
.action(ArgAction::SetTrue)
.help("store this node's own counters in this node"),
)
.subcommand(
clap::Command::new("mira")
.visible_alias("tui")
.about("open the terminal UI over the same blocks")
.after_help(
"With --data-dir it reads a block directory in-process and needs no\n\
server running; with --addr it queries one over HTTP.",
)
.arg(config_arg())
.arg(data_dir_arg())
.arg(
Arg::new("addr")
.long("addr")
.value_name("HOST[:PORT]")
.value_parser(tui::parse_addr)
.help("query a running node instead of reading a directory"),
),
)
.subcommand(
clap::Command::new("proxy")
.about("one OTLP and query surface in front of N storage nodes")
.after_help(
"It stores nothing: exports are split by entity and forwarded, and\n\
/api/v1/query is answered by merging every replica's page on the\n\
cursor order. The reads it cannot merge — correlate, map, metrics\n\
and entities — answer 501 naming themselves rather than returning\n\
one node's share of the answer.",
)
.arg(config_arg())
.arg(http_arg())
.arg(max_request_bytes_arg())
.arg(
Arg::new("replica")
.long("replica")
.value_name("URL")
.action(ArgAction::Append)
.value_parser(config::replicas)
.help("a storage node to forward to; repeatable"),
),
)
.subcommand(
clap::Command::new("offload")
.about("move blocks between a data directory and an object store")
.after_help(
"--offload sends a block to an object store just before retention\n\
deletes it, under the same directory name it had locally — so the\n\
store's own listing is the catalog and there is nothing else to keep\n\
in sync.\n\n\
`push` is the same copy in the other direction and deletes nothing.\n\
It is how a volume a scale-down left behind is re-homed: push it,\n\
then restore it into a node that is still running. Give it a URI of\n\
its own — file:///archive/${node} — so the restore pulls back one\n\
node's blocks rather than the whole archive.",
)
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(offload_verb("list", "list what the store holds"))
.subcommand(
offload_verb(
"restore",
"copy every block the store has and this node does not into --data-dir",
)
.arg(data_dir_arg()),
)
.subcommand(
offload_verb("push", "copy this node's blocks into the store")
.arg(data_dir_arg()),
),
)
.subcommand(update::cli())
.subcommand(
clap::Command::new("completion")
.about("print a shell completion script")
.after_help(
"The script is generated from this binary's own command tree, so it\n\
completes exactly the flags this version accepts:\n\n \
mira completion bash > /etc/bash_completion.d/mira\n \
mira completion zsh > \"${fpath[1]}/_mira\"\n \
mira completion fish > ~/.config/fish/completions/mira.fish",
)
.arg(
Arg::new("shell")
.required(true)
.value_name("SHELL")
.value_parser(value_parser!(clap_complete::Shell)),
),
)
}
fn config_arg() -> Arg {
Arg::new("config")
.long("config")
.value_name("FILE")
.value_parser(value_parser!(PathBuf))
.help("KYAML configuration file; flags override what it sets")
}
fn data_dir_arg() -> Arg {
Arg::new("data-dir")
.long("data-dir")
.value_name("PATH")
.value_parser(value_parser!(PathBuf))
.help("the block directory, which is the whole manifest")
}
fn http_arg() -> Arg {
Arg::new("http")
.long("http")
.value_name("ADDR")
.value_parser(value_parser!(SocketAddr))
.help("where OTLP/HTTP, the query API, MCP and the UI listen")
}
fn max_request_bytes_arg() -> Arg {
Arg::new("max-request-bytes")
.long("max-request-bytes")
.value_name("SIZE")
.value_parser(config::bytes)
.help("the largest export either listener will decode")
}
fn offload_arg() -> Arg {
Arg::new("offload")
.long("offload")
.value_name("URI")
.help("where a block goes before retention unlinks it")
}
fn offload_verb(name: &'static str, about: &'static str) -> clap::Command {
clap::Command::new(name)
.about(about)
.arg(config_arg())
.arg(offload_arg())
}
fn opt<T: Clone + Send + Sync + 'static>(m: &ArgMatches, id: &str) -> Option<T> {
m.try_get_one::<T>(id).ok().flatten().cloned()
}
fn many(m: &ArgMatches, id: &str) -> Vec<String> {
m.try_get_many::<Vec<String>>(id)
.ok()
.flatten()
.into_iter()
.flatten()
.flatten()
.cloned()
.collect()
}
fn load_from(m: &ArgMatches) -> Result<Config, String> {
let mut cfg = match opt::<PathBuf>(m, "config") {
Some(p) => Config::load(&p)?,
None => Config::default(),
};
if let Some(v) = opt::<String>(m, "node") {
cfg.node = v;
}
if let Some(v) = opt::<SocketAddr>(m, "grpc") {
cfg.grpc = v;
}
if let Some(v) = opt::<SocketAddr>(m, "http") {
cfg.http = v;
}
if let Some(v) = opt::<PathBuf>(m, "data-dir") {
cfg.data_dir = v;
}
if let Some(v) = opt::<Duration>(m, "retention") {
cfg.retention = v;
}
if let Some(v) = opt::<String>(m, "offload") {
cfg.offload = Some(v);
}
if let Some(v) = opt::<usize>(m, "max-request-bytes") {
cfg.max_request_bytes = v;
}
if let Some(v) = opt::<usize>(m, "queue") {
cfg.queue = v;
}
if let Some(v) = opt::<usize>(m, "shards") {
cfg.shards = v;
}
if let Some(v) = opt::<Duration>(m, "telemetry-interval") {
cfg.telemetry_interval = v;
}
if let Some(v) = opt::<PathBuf>(m, "alerts") {
cfg.alerts = Some(v);
}
cfg.replicas.extend(many(m, "replica"));
cfg.wal |= opt::<bool>(m, "wal").unwrap_or_default();
cfg.self_telemetry |= opt::<bool>(m, "self-telemetry").unwrap_or_default();
Ok(cfg)
}
fn offload_cmd(verb: &str, m: &ArgMatches) -> Result<(), String> {
let cfg = load_from(m)?;
let uri = cfg
.offload
.as_deref()
.ok_or("mira offload needs --offload URI, or storage.offload in --config")?;
let target = mira_core::offload::Target::parse(uri).map_err(|e| e.to_string())?;
let node = mira_core::block::node_id(&cfg.node);
if verb == "restore" {
mira_core::block::check_filesystem(&cfg.data_dir).map_err(|e| e.to_string())?;
}
let (mut blocks, mut bytes) = (0u64, 0u64);
for signal in pipeline::SIGNALS {
let source = match verb {
"push" => mira_core::block::scan(&cfg.data_dir, signal),
_ => target.list(signal),
};
for b in source.map_err(|e| e.to_string())? {
let name = b.dir.file_name().unwrap_or_default().to_string_lossy();
let size = block_bytes(&b.dir);
blocks += 1;
bytes += size;
let what = match verb {
"restore" => match target
.pull(signal, &b, &cfg.data_dir, node)
.map_err(|e| e.to_string())?
{
true => "restored",
false => "present",
},
"push" => match target.push(signal, &b).map_err(|e| e.to_string())? {
true => "pushed",
false => "present",
},
_ => "",
};
println!(
"{signal:<8} {} .. {} {:>10} {name} {what}",
tui::stamp(b.min_ts),
tui::stamp(b.max_ts),
size,
);
}
}
println!("{blocks} blocks, {bytes} bytes");
Ok(())
}
fn block_bytes(dir: &Path) -> u64 {
std::fs::read_dir(dir)
.into_iter()
.flatten()
.flatten()
.filter_map(|e| e.metadata().ok())
.filter(|m| m.is_file())
.map(|m| m.len())
.sum()
}
fn spawn_probes() {
tokio::spawn(async {
loop {
let t = std::time::Instant::now();
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
mira_core::diag::RUNTIME_LAG
.record(t.elapsed().as_nanos().saturating_sub(50_000_000) as u64);
}
});
tokio::spawn(async {
let mut t = tokio::time::interval(std::time::Duration::from_secs(5));
loop {
t.tick().await;
tracing::debug!(target: "mira::probe", "{}", mira_core::diag::dump());
}
});
}
fn tui_source(m: &ArgMatches) -> Result<tui::Source, String> {
let cfg = match opt::<PathBuf>(m, "config") {
Some(p) => Config::load(&p)?,
None => Config::default(),
};
Ok(match opt::<String>(m, "addr") {
Some(a) => tui::Source::Remote(a),
None => tui::Source::Local(opt::<PathBuf>(m, "data-dir").unwrap_or(cfg.data_dir)),
})
}
fn check_source(src: &tui::Source, on_a_tty: bool) -> Result<(), Box<dyn std::error::Error>> {
let tui::Source::Local(dir) = src else {
return Ok(());
};
if !on_a_tty {
return Ok(());
}
if !dir.is_dir() {
return Err(format!(
"{} is not a directory. `mira mira --data-dir` reads an existing block \
directory in place and creates nothing, so this is a volume that never \
mounted, a typo, or the path a different replica writes to. An empty \
but real directory is fine and shows no rows.",
dir.display()
)
.into());
}
let to_stderr = tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_ansi(std::io::stderr().is_terminal())
.without_time()
.finish();
tracing::subscriber::with_default(to_stderr, || mira_core::block::check_filesystem(dir))?;
Ok(())
}
fn main() {
if let Err(e) = run() {
eprintln!("mira: {e}");
std::process::exit(1);
}
}
fn clap_error(e: &clap::Error) -> Box<dyn std::error::Error> {
if !e.use_stderr() {
e.exit();
}
e.to_string()
.trim_start_matches("error: ")
.trim_end()
.to_owned()
.into()
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let m = cli().try_get_matches().map_err(|e| clap_error(&e))?;
let string = |e: String| -> Box<dyn std::error::Error> { e.into() };
if let Some(("update", sub)) = m.subcommand() {
return update::run(sub).map_err(string);
}
if let Some(("completion", sub)) = m.subcommand() {
let shell = sub
.get_one::<clap_complete::Shell>("shell")
.copied()
.expect("required");
clap_complete::generate(shell, &mut cli(), "mira", &mut std::io::stdout());
return Ok(());
}
if let Some(("offload", sub)) = m.subcommand() {
let (verb, args) = sub.subcommand().expect("subcommand_required");
return offload_cmd(verb, args).map_err(string);
}
if let Some(("mira", sub)) = m.subcommand() {
let src = tui_source(sub).map_err(string)?;
let on_a_tty = std::io::stdin().is_terminal() && std::io::stdout().is_terminal();
check_source(&src, on_a_tty)?;
return tui::run(src).map_err(Into::into);
}
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "mira=info,mira_core=info".into()),
)
.with_ansi(std::io::stdout().is_terminal())
.init();
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
match m.subcommand() {
Some(("proxy", sub)) => rt.block_on(proxy_cmd(sub)),
_ => rt.block_on(serve(&m)),
}
}
async fn proxy_cmd(m: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let cfg = load_from(m).map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
let p = proxy::Proxy::new(cfg.replicas.clone(), cfg.max_request_bytes)
.map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
let socket = tokio::net::TcpListener::bind(cfg.http).await?;
let addr = socket.local_addr()?;
tracing::info!(
http = %addr,
replicas = %cfg.replicas.join(" "),
max_request_bytes = cfg.max_request_bytes,
"mira proxy listening"
);
axum::serve(socket, proxy::router(p))
.with_graceful_shutdown(shutdown())
.await?;
Ok(())
}
async fn serve(m: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let cfg = load_from(m).map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
serve_with(cfg, shutdown()).await
}
async fn serve_with(
cfg: Config,
stop_signal: impl std::future::Future<Output = ()>,
) -> Result<(), Box<dyn std::error::Error>> {
if !cfg.replicas.is_empty() {
return Err(
"--replica / proxy.replicas is read by `mira proxy`; this is a storage node".into(),
);
}
let rules = match &cfg.alerts {
Some(p) => alert::Rules::load(p)?,
None => alert::Rules::off(),
};
std::fs::create_dir_all(&cfg.data_dir).map_err(|e| {
format!(
"cannot create data directory {}: {e}. Mira makes this path on first \
start, so this is a parent that is not writable or something that is \
not a directory already sitting there (in Kubernetes: a subPath that \
names a file, or a volume mounted readOnly).",
cfg.data_dir.display()
)
})?;
mira_core::block::check_filesystem(&cfg.data_dir)?;
mira_core::block::check_writable(&cfg.data_dir)?;
let node = mira_core::block::node_id(&cfg.node);
let taken = |addr: std::net::SocketAddr, what: &str, e: std::io::Error| {
format!(
"cannot bind {addr} for {what}: {e}. Nothing has started yet, so this \
is another process on the port — most often the previous instance \
still draining (in Kubernetes: a terminationGracePeriodSeconds \
shorter than the drain takes, or two replicas sharing a hostPort)."
)
};
let grpc_socket = tonic::transport::server::TcpIncoming::bind(cfg.grpc)
.map_err(|e| taken(cfg.grpc, "OTLP/gRPC", e))?
.with_nodelay(Some(true));
let http_socket = tokio::net::TcpListener::bind(cfg.http)
.await
.map_err(|e| taken(cfg.http, "OTLP/HTTP and the query API", e))?;
let (grpc_addr, http_addr) = (grpc_socket.local_addr()?, http_socket.local_addr()?);
let data_dir = std::sync::Arc::new(cfg.data_dir.clone());
let _ = *START;
let wal = match cfg.wal {
true => Some(std::sync::Arc::new(mira_core::wal::Wal::open(
&cfg.data_dir,
node,
)?)),
false => None,
};
let node_name = cfg.node.clone();
let pcfg = std::sync::Arc::new(pipeline::Config {
data_dir: cfg.data_dir,
node,
retention: cfg.retention,
offload: cfg
.offload
.as_deref()
.map(mira_core::offload::Target::parse)
.transpose()?,
queue: cfg.queue,
shards: pipeline::shard_count(
cfg.shards,
std::thread::available_parallelism().map_or(1, |n| n.get()),
),
wal: wal.clone(),
..Default::default()
});
let (logs, o_logs, h_logs) = pipeline::spawn::<mira_core::logs::LogsBuilder>(&pcfg);
let (traces, o_traces, h_traces) = pipeline::spawn::<mira_core::traces::TracesBuilder>(&pcfg);
let (metrics, o_metrics, h_metrics) =
pipeline::spawn::<mira_core::metrics::MetricsBuilder>(&pcfg);
let flushers = [h_logs, h_traces, h_metrics];
let open_blocks = [o_logs, o_traces, o_metrics];
if wal.is_some() {
replay(
&pcfg.data_dir,
node,
logs.clone(),
traces.clone(),
metrics.clone(),
)
.await?;
}
let sampler = cfg.self_telemetry.then(|| {
tracing::info!(
interval_s = cfg.telemetry_interval.as_secs(),
"storing this node's own telemetry in this node"
);
tokio::spawn(telemetry::run(
node_name,
pcfg.data_dir.clone(),
cfg.telemetry_interval,
metrics.clone(),
))
});
let recv = receiver::Receivers {
logs,
traces,
metrics,
max_request_bytes: cfg.max_request_bytes,
};
pipeline::spawn_retention(pcfg);
let (stop, stop_rx) = tokio::sync::watch::channel(());
let stopped = |mut rx: tokio::sync::watch::Receiver<()>| async move {
let _ = rx.changed().await;
};
let grpc = tokio::spawn(
tonic::transport::Server::builder()
.add_service(recv.logs_server())
.add_service(recv.traces_server())
.add_service(recv.metrics_server())
.serve_with_incoming_shutdown(grpc_socket, stopped(stop_rx.clone())),
);
let api = api::Api {
data_dir: std::sync::Arc::clone(&data_dir),
open: open_blocks,
alerts: std::sync::Arc::new(alert::Engine::new(rules)),
};
alert::spawn(api.clone());
if tracing::enabled!(target: "mira::probe", tracing::Level::DEBUG) {
spawn_probes();
}
let serve = axum::serve(
http_socket,
receiver::http_router(recv)
.merge(api::router(api.clone()).layer(axum::middleware::from_fn(timed)))
.merge(mcp::router(api.clone()))
.merge(alert::router(api))
.merge(ops_router(std::sync::Arc::clone(&data_dir)))
.merge(ui::router()),
)
.with_graceful_shutdown(stopped(stop_rx));
let http = tokio::spawn(async move { serve.await });
tracing::info!(
grpc = %grpc_addr, http = %http_addr,
ui = %format!("http://{http_addr}/"),
node = %cfg.node, node_id = format!("{node:08x}"),
data_dir = %data_dir.display(),
retention = %format!("{}s", cfg.retention.as_secs()),
max_request_bytes = cfg.max_request_bytes,
"mira listening"
);
let (mut grpc, mut http) = (grpc, http);
let mut flushers = flushers;
let mut wedged = false;
tokio::select! {
r = &mut grpc => r??,
r = &mut http => r??,
_ = first_stopped(&mut flushers) => wedged = true,
_ = stop_signal => tracing::info!("draining"),
}
if let Some(s) = sampler {
s.abort();
let _ = s.await;
}
drain(stop, grpc, http, flushers, DRAIN_GRACE).await;
tracing::info!("stopped");
if wedged {
return Err("a flusher stopped, so one signal can no longer be stored; \
exiting for the supervisor to restart (the cause is logged above)"
.into());
}
Ok(())
}
const DRAIN_GRACE: std::time::Duration = std::time::Duration::from_secs(15);
async fn drain<G, H>(
stop: tokio::sync::watch::Sender<()>,
grpc: tokio::task::JoinHandle<G>,
http: tokio::task::JoinHandle<H>,
flushers: [pipeline::Flushers; 3],
grace: std::time::Duration,
) {
let _ = stop.send(());
let landed = async move {
let _ = grpc.await;
let _ = http.await;
for h in flushers {
let _ = h.await;
}
};
if tokio::time::timeout(grace, landed).await.is_err() {
tracing::warn!(?grace, "did not drain in time; exiting anyway");
}
}
async fn replay(
dir: &Path,
node: u32,
logs: pipeline::Ingest<mira_proto::collector::logs::v1::ExportLogsServiceRequest>,
traces: pipeline::Ingest<mira_proto::collector::trace::v1::ExportTraceServiceRequest>,
metrics: pipeline::Ingest<mira_proto::collector::metrics::v1::ExportMetricsServiceRequest>,
) -> Result<(), Box<dyn std::error::Error>> {
use mira_core::wal::Signal;
let dir = dir.to_path_buf();
let started = std::time::Instant::now();
let done = tokio::task::spawn_blocking(move || {
let watermarks = mira_core::block::wal_watermarks(&dir, node)?;
let mut undecodable = 0u64;
let out = mira_core::wal::Wal::replay(&dir, node, watermarks, |signal, seq, body| {
let pushed = match signal {
Signal::Logs => logs.replay(body, seq),
Signal::Traces => traces.replay(body, seq),
Signal::Metrics => metrics.replay(body, seq),
};
match pushed {
Ok(()) | Err(pipeline::Rejected::Failed(_)) => {
undecodable += u64::from(pushed.is_err());
Ok(())
}
Err(_) => Err(mira_core::Error::WalCorrupt {
path: dir.clone(),
why: "the flusher for this signal stopped during replay",
}),
}
})?;
Ok::<_, mira_core::Error>((out, undecodable))
})
.await??;
let (out, undecodable) = done;
if undecodable > 0 {
tracing::error!(
frames = undecodable,
"write-ahead log frames passed their checksum and would not decode as OTLP; \
those exports are gone"
);
}
if out.torn_segments > 0 {
tracing::info!(
segments = out.torn_segments,
"write-ahead log segments ended in a torn frame; that is what a crash looks like"
);
}
if out.replayed > 0 || out.skipped > 0 {
tracing::info!(
replayed = out.replayed,
skipped = out.skipped,
bytes = out.bytes,
elapsed_ms = started.elapsed().as_millis() as u64,
"recovered from the write-ahead log"
);
}
Ok(())
}
async fn first_stopped(flushers: &mut [pipeline::Flushers; 3]) {
let [logs, traces, metrics] = flushers;
tokio::select! {
_ = logs => {}
_ = traces => {}
_ = metrics => {}
}
}
fn ops_router(data_dir: std::sync::Arc<PathBuf>) -> Router {
Router::new()
.route("/health", get(health))
.route("/readyz", get(readyz))
.route("/api/v1/stats", get(stats))
.with_state(data_dir)
}
async fn health() -> Response {
let mut j = mira_core::json::Json::new();
j.obj(|j| {
j.key("status");
j.str("ok");
for r in &pipeline::REJECTS {
j.key(r.signal);
j.obj(|j| {
j.key("shed");
j.u64(r.shed.load(Relaxed));
j.key("failed");
j.u64(r.failed.load(Relaxed));
});
}
});
json_ok(j.into_string())
}
async fn readyz() -> Response {
ready(pipeline::stalled())
}
fn ready(stalled: Option<(&'static str, u64)>) -> Response {
let mut j = mira_core::json::Json::new();
j.obj(|j| match stalled {
None => {
j.key("status");
j.str("ok");
}
Some((signal, secs)) => {
j.key("status");
j.str("unavailable");
j.key("signal");
j.str(signal);
j.key("stalled_s");
j.u64(secs);
j.key("reason");
j.str(
"this node has not been able to store an export for this signal; \
the usual cause is a full or unwritable volume",
);
}
});
let code = match stalled {
None => axum::http::StatusCode::OK,
Some(_) => axum::http::StatusCode::SERVICE_UNAVAILABLE,
};
(
code,
[(axum::http::header::CONTENT_TYPE, "application/json")],
j.into_string(),
)
.into_response()
}
static QUERIES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static QUERY_NANOS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static QUERY_MAX_NANOS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static START: std::sync::LazyLock<std::time::Instant> =
std::sync::LazyLock::new(std::time::Instant::now);
async fn timed(req: axum::extract::Request, next: axum::middleware::Next) -> Response {
let t = std::time::Instant::now();
let res = next.run(req).await;
let ns = t.elapsed().as_nanos() as u64;
QUERIES.fetch_add(1, Relaxed);
QUERY_NANOS.fetch_add(ns, Relaxed);
QUERY_MAX_NANOS.fetch_max(ns, Relaxed);
res
}
async fn stats(
axum::extract::State(dir): axum::extract::State<std::sync::Arc<PathBuf>>,
) -> Response {
let disk = tokio::task::spawn_blocking(move || {
(
mira_core::block::free_fraction(&dir).ok(),
pipeline::SIGNALS.map(|s| mira_core::block::scan(&dir, s).ok().map(|b| b.len() as u64)),
)
})
.await;
let (free, on_disk) = disk.unwrap_or((None, [None; 3]));
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let age = |since: u64| (since != 0).then_some(now.saturating_sub(since));
let queries = QUERIES.load(Relaxed);
let mut j = mira_core::json::Json::new();
j.obj(|j| {
j.key("uptime_s");
j.u64(START.elapsed().as_secs());
j.key("peak_rss_bytes");
j.u64(peak_rss());
j.key("free_fraction");
match free {
Some(f) => j.f64(f),
None => j.null(),
}
j.key("degraded_syncs");
j.u64(mira_core::degraded_syncs());
j.key("queries");
j.obj(|j| {
j.key("count");
j.u64(queries);
j.key("mean_ms");
j.f64(QUERY_NANOS.load(Relaxed) as f64 / queries.max(1) as f64 / 1e6);
j.key("max_ms");
j.f64(QUERY_MAX_NANOS.load(Relaxed) as f64 / 1e6);
});
j.key("signals");
j.obj(|j| {
for (r, blocks) in pipeline::REJECTS.iter().zip(on_disk) {
j.key(r.signal);
j.obj(|j| {
for (k, v) in [
("shed", r.shed.load(Relaxed)),
("failed", r.failed.load(Relaxed)),
("refused", r.refused.load(Relaxed)),
("blocks_published", r.published.load(Relaxed)),
("rows", r.rows.load(Relaxed)),
("bytes", r.bytes.load(Relaxed)),
] {
j.key(k);
j.u64(v);
}
for (k, v) in [
("blocks_on_disk", blocks),
("open_block_age_s", age(r.open_since.load(Relaxed))),
("stalled_s", age(r.stalled_since.load(Relaxed))),
] {
j.key(k);
match v {
Some(v) => j.u64(v),
None => j.null(),
}
}
});
}
});
});
json_ok(j.into_string())
}
fn peak_rss() -> u64 {
let mut ru: libc::rusage = unsafe { std::mem::zeroed() };
unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut ru) };
#[cfg(target_os = "macos")]
const UNIT: u64 = 1;
#[cfg(not(target_os = "macos"))]
const UNIT: u64 = 1024;
ru.ru_maxrss.max(0) as u64 * UNIT
}
fn json_ok(body: String) -> Response {
(
[(axum::http::header::CONTENT_TYPE, "application/json")],
body,
)
.into_response()
}
#[cfg(unix)]
async fn shutdown() {
let mut term = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("a SIGTERM handler on the serving runtime");
tokio::select! {
_ = tokio::signal::ctrl_c() => {}
_ = term.recv() => {}
}
}
#[cfg(not(unix))]
async fn shutdown() {
let _ = tokio::signal::ctrl_c().await;
}
#[cfg(test)]
mod tests {
use super::*;
fn argv(s: &str) -> Result<ArgMatches, String> {
cli()
.try_get_matches_from(std::iter::once("mira").chain(s.split_whitespace()))
.map_err(|e| e.to_string())
}
fn load(s: &str) -> Result<Config, String> {
load_from(&argv(s)?)
}
fn offload(s: &str) -> Result<(), String> {
let m = argv(&format!("offload {s}"))?;
let (verb, sub) = m
.subcommand_matches("offload")
.and_then(ArgMatches::subcommand)
.expect("subcommand_required");
offload_cmd(verb, sub)
}
fn tmp(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("mira-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
async fn stop_edge(immediately: bool) {
if !immediately {
std::future::pending::<()>().await;
}
}
#[tokio::test]
async fn the_stop_edge_fires_when_it_is_told_to_and_never_otherwise() {
let zero = std::time::Duration::ZERO;
let never = tokio::time::timeout(zero, stop_edge(false));
assert!(never.await.is_err(), "the never-stop edge stopped");
tokio::time::timeout(zero, stop_edge(true))
.await
.expect("the immediate edge did not fire");
}
#[tokio::test(start_paused = true)]
async fn the_probe_tasks_sample_the_runtime_and_dump_it() {
use std::sync::atomic::Ordering::Relaxed;
let lag = &mira_core::diag::RUNTIME_LAG;
let before = lag.n.load(Relaxed);
spawn_probes();
tokio::time::sleep(std::time::Duration::from_millis(120)).await;
assert_eq!(
lag.n.load(Relaxed),
before + 2,
"the lag sampler did not run once per 50 ms period"
);
assert_eq!(
lag.max_ns.load(Relaxed),
0,
"a wake that cost no real time was reported as late"
);
}
#[test]
fn offload_list_and_restore_read_the_store_and_nothing_else() {
let dir = tmp("offload-cmd");
let (store, data) = (dir.join("cold"), dir.join("data"));
std::fs::create_dir_all(&data).unwrap();
let uri = format!("file://{}", store.display());
for (args, want) in [
("", "list"),
("sync --offload file:///x", "unrecognized subcommand 'sync'"),
("list", "--offload"),
("list --offload s3://bucket", "s3://bucket"),
] {
let e = offload(args).unwrap_err();
assert!(e.contains(want), "`mira offload {args}` said: {e}");
}
offload(&format!("list --offload {uri}")).unwrap();
let name = format!("{:020}-{:020}-{:08x}-{:012}-{:020}", 1_000, 2_000, 7, 1, 0);
let block = store.join("logs").join("p=0").join(&name);
std::fs::create_dir_all(&block).unwrap();
std::fs::write(block.join("logs.arrow"), b"bytes").unwrap();
offload(&format!("list --offload {uri}")).unwrap();
assert_eq!(
std::fs::read_dir(&data).unwrap().count(),
0,
"`list` copies nothing"
);
let restore = format!("restore --offload {uri} --data-dir {}", data.display());
offload(&restore).unwrap();
let local = data.join("logs").join("p=0").join(&name);
assert_eq!(std::fs::read(local.join("logs.arrow")).unwrap(), b"bytes");
std::fs::write(local.join("logs.arrow"), b"local edit").unwrap();
offload(&restore).unwrap();
assert_eq!(
std::fs::read(local.join("logs.arrow")).unwrap(),
b"local edit",
"a block already present is left alone"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn offload_push_copies_out_of_the_data_dir_and_unlinks_nothing() {
let dir = tmp("offload-push");
let (store, data) = (dir.join("cold"), dir.join("data"));
let uri = format!("file://{}", store.display());
let name = format!("{:020}-{:020}-{:08x}-{:012}-{:020}", 1_000, 2_000, 7, 1, 0);
let local = data.join("logs").join("p=0").join(&name);
std::fs::create_dir_all(&local).unwrap();
std::fs::write(local.join("logs.arrow"), b"bytes").unwrap();
let push = format!("push --offload {uri} --data-dir {}", data.display());
offload(&push).unwrap();
let remote = store.join("logs").join("p=0").join(&name);
assert_eq!(std::fs::read(remote.join("logs.arrow")).unwrap(), b"bytes");
assert!(local.exists(), "push copies the block, it does not move it");
offload(&push).unwrap();
std::fs::write(local.join("logs.arrow"), b"local edit").unwrap();
offload(&push).unwrap_err();
assert_eq!(
std::fs::read(remote.join("logs.arrow")).unwrap(),
b"bytes",
"a block already in the store is left alone"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn flags_override_the_file_which_overrides_the_defaults() {
let dir = tmp("load");
let file = dir.join("mira.yaml");
std::fs::write(
&file,
r#"{ "node": "from-file",
"listen": { "grpc": "127.0.0.1:1", "http": "127.0.0.1:2" },
"storage": { "dir": "/from/file", "retention": "3h" },
"ingest": { "max_request_bytes": "1MiB" } }"#,
)
.unwrap();
let f = file.display();
let c = load(&format!("--config {f}")).unwrap();
assert_eq!(c.node, "from-file");
assert_eq!(c.data_dir, PathBuf::from("/from/file"));
assert_eq!(c.retention, std::time::Duration::from_secs(3 * 3600));
assert_eq!(c.max_request_bytes, 1 << 20);
assert_eq!(c.http.port(), 2);
let c = load(&format!(
"--config {f} --node cli --grpc 127.0.0.1:3 --http 127.0.0.1:4 \
--data-dir /from/cli --retention 30s --max-request-bytes 2MiB"
))
.unwrap();
assert_eq!(c.node, "cli");
assert_eq!(c.grpc.port(), 3);
assert_eq!(c.http.port(), 4);
assert_eq!(c.data_dir, PathBuf::from("/from/cli"));
assert_eq!(c.retention, std::time::Duration::from_secs(30));
assert_eq!(c.max_request_bytes, 2 << 20);
let c = load(
"--queue 4096 --shards 8 --wal --alerts /from/cli.kyaml \
--self-telemetry --telemetry-interval 1m",
)
.unwrap();
assert_eq!(c.queue, 4096);
assert_eq!(c.shards, 8);
assert_eq!(c.alerts, Some(PathBuf::from("/from/cli.kyaml")));
assert!(c.self_telemetry);
assert!(c.wal);
assert_eq!(c.telemetry_interval, std::time::Duration::from_secs(60));
let c = load("--offload file:///srv/cold").unwrap();
assert_eq!(c.offload.as_deref(), Some("file:///srv/cold"));
let d = load("").unwrap();
assert_eq!(d.node, Config::default().node);
assert!(!d.self_telemetry, "self-telemetry is opt-in");
for (args, want) in [
("--nope", "unexpected argument '--nope'"),
("--peers a:1", "unexpected argument '--peers'"),
("--node", "a value is required for '--node <NAME>'"),
("--config", "a value is required for '--config <FILE>'"),
("--grpc nope", "invalid value 'nope' for '--grpc <ADDR>'"),
("--http nope", "invalid value 'nope' for '--http <ADDR>'"),
("--retention nope", "not a duration"),
("--max-request-bytes nope", "not a size"),
("--queue nope", "not a whole number"),
("--queue 0", "at least 1"),
("--shards nope", "not a whole number"),
("--shards -1", "unexpected argument '-1'"),
("--telemetry-interval nope", "not a duration"),
("--config /no/such/file.yaml", "/no/such/file.yaml"),
] {
let e = load(args).unwrap_err();
assert!(e.contains(want), "{args:?} said {e:?}");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn the_tui_reads_the_directory_its_config_writes_to() {
let dir = tmp("tui-src");
let file = dir.join("mira.yaml");
std::fs::write(&file, r#"{ "storage": { "dir": "/from/file" } }"#).unwrap();
let f = file.display();
let src = |a: &str| {
let out = argv(&format!("mira {a}")).and_then(|m| {
tui_source(m.subcommand_matches("mira").expect("the tui subcommand"))
});
match out {
Ok(tui::Source::Local(p)) => format!("local {}", p.display()),
Ok(tui::Source::Remote(a)) => format!("remote {a}"),
Err(e) => format!("error {}", e.lines().next().unwrap_or_default()),
}
};
let default_dir = Config::default().data_dir;
for (args, want) in [
(format!("--config {f}"), "local /from/file".to_owned()),
(
format!("--config {f} --data-dir /from/cli"),
"local /from/cli".to_owned(),
),
(String::new(), format!("local {}", default_dir.display())),
("--addr host:9999".into(), "remote host:9999".to_owned()),
(
"--nope".into(),
"error error: unexpected argument '--nope' found".to_owned(),
),
(
"--data-dir".into(),
"error error: a value is required for '--data-dir <PATH>' but none was supplied"
.to_owned(),
),
(
"--config".into(),
"error error: a value is required for '--config <FILE>' but none was supplied"
.to_owned(),
),
] {
assert_eq!(src(&args), want, "{args:?}");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn the_server_starts_from_a_config_and_drains_when_stopped() {
let dir = tmp("serve");
let cfg = Config {
data_dir: dir.join("data"),
grpc: "127.0.0.1:0".parse().unwrap(),
http: "127.0.0.1:0".parse().unwrap(),
self_telemetry: true,
telemetry_interval: std::time::Duration::from_secs(3600),
..Config::default()
};
let log = dir.join("start.log");
let _logging = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_writer(std::fs::File::create(&log).unwrap())
.without_time()
.with_ansi(false)
.finish(),
);
let t = std::time::Instant::now();
serve_with(cfg.clone(), stop_edge(true)).await.unwrap();
assert!(
t.elapsed() < std::time::Duration::from_secs(15),
"timed out"
);
assert!(cfg.data_dir.is_dir());
let logged = std::fs::read_to_string(&log).unwrap();
assert!(logged.contains("mira listening"), "{logged}");
assert!(!logged.contains("127.0.0.1:0"), "{logged}");
assert!(logged.contains("ui=http://127.0.0.1:"), "{logged}");
assert!(
logged.contains("storing this node's own telemetry"),
"{logged}"
);
assert!(
logged.contains(&format!("retention={}s", cfg.retention.as_secs())),
"{logged}"
);
let held = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = held.local_addr().unwrap();
for taken in [
Config {
http: addr,
..cfg.clone()
},
Config {
grpc: addr,
..cfg.clone()
},
] {
let e = serve_with(taken, stop_edge(false))
.await
.unwrap_err()
.to_string();
assert!(e.contains(&addr.to_string()), "{e}");
assert!(e.to_lowercase().contains("address"), "{e}");
}
let file = dir.join("a-file");
std::fs::write(&file, b"").unwrap();
let e = serve_with(
Config {
data_dir: file.clone(),
..cfg
},
stop_edge(false),
)
.await
.unwrap_err()
.to_string();
assert!(e.contains(&file.display().to_string()), "{e}");
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn a_node_refuses_to_start_on_a_rules_file_or_a_log_it_cannot_open() {
let dir = tmp("boot-guards");
let rules = dir.join("alerts.kyaml");
let cfg = |alerts: Option<PathBuf>, data_dir: PathBuf| Config {
data_dir,
grpc: "127.0.0.1:0".parse().unwrap(),
http: "127.0.0.1:0".parse().unwrap(),
alerts,
..Config::default()
};
std::fs::write(
&rules,
r#"{ "rules": [ { "name": "any-log", "over": "1m", "when": "count >= 1",
"query": { "signal": "logs" } } ] }"#,
)
.unwrap();
serve_with(
cfg(Some(rules.clone()), dir.join("ok")),
std::future::ready(()),
)
.await
.expect("a node with rules starts");
std::fs::write(&rules, "{ rules: nope }").unwrap();
let never_made = dir.join("not-made");
let e = serve_with(
cfg(Some(rules.clone()), never_made.clone()),
std::future::pending(),
)
.await
.unwrap_err()
.to_string();
assert!(e.contains("alerts.kyaml"), "{e}");
assert!(!never_made.exists(), "the boot got past the rules file");
let wal_blocked = dir.join("wal-blocked");
std::fs::create_dir_all(&wal_blocked).unwrap();
std::fs::write(wal_blocked.join(".wal"), b"not a directory").unwrap();
let cfg = Config {
wal: true,
..cfg(None, wal_blocked.clone())
};
let e = serve_with(cfg, std::future::pending())
.await
.unwrap_err()
.to_string();
assert!(e.contains(".wal"), "{e}");
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn a_flusher_that_cannot_start_stops_the_server() {
let dir = tmp("wedged");
std::fs::write(dir.join("logs"), b"not a directory").unwrap();
let cfg = Config {
data_dir: dir.clone(),
grpc: "127.0.0.1:0".parse().unwrap(),
http: "127.0.0.1:0".parse().unwrap(),
wal: false,
..Config::default()
};
let e = serve_with(cfg, stop_edge(false))
.await
.unwrap_err()
.to_string();
assert!(e.contains("flusher"), "{e}");
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn a_drain_that_never_lands_leaves_anyway() {
let (stop, rx) = tokio::sync::watch::channel(());
let wedged = || tokio::spawn(std::future::pending::<()>());
drain(
stop,
wedged(),
wedged(),
std::array::from_fn(|_| pipeline::Flushers::wedged()),
std::time::Duration::from_millis(1),
)
.await;
assert!(
rx.has_changed().is_err(),
"drain owns the sender to the end"
);
}
async fn text(r: Response) -> (axum::http::StatusCode, String) {
let (parts, body) = r.into_parts();
let body = axum::body::to_bytes(body, 1 << 20).await.unwrap();
(parts.status, String::from_utf8(body.to_vec()).unwrap())
}
#[tokio::test]
async fn health_reports_every_signals_rejections() {
let (status, body) = text(health().await).await;
assert_eq!(status, axum::http::StatusCode::OK);
assert!(body.starts_with(r#"{"status":"ok""#), "{body}");
for signal in pipeline::SIGNALS {
assert!(body.contains(&format!(r#""{signal}":{{"shed":"#)), "{body}");
}
}
#[tokio::test]
async fn readiness_fails_only_once_a_signal_has_been_unable_to_store() {
let (status, body) = text(ready(None)).await;
assert_eq!(status, axum::http::StatusCode::OK);
assert_eq!(body, r#"{"status":"ok"}"#);
let (status, body) = text(ready(Some(("logs", 300)))).await;
assert_eq!(status, axum::http::StatusCode::SERVICE_UNAVAILABLE);
assert!(body.contains(r#""signal":"logs""#), "{body}");
assert!(body.contains(r#""stalled_s":300"#), "{body}");
let (status, _) = text(readyz().await).await;
assert_eq!(status, axum::http::StatusCode::OK);
}
#[tokio::test]
async fn stats_reports_what_this_node_is_doing_with_its_disk() {
let dir = tmp("stats");
let (status, body) =
text(stats(axum::extract::State(std::sync::Arc::new(dir.clone()))).await).await;
assert_eq!(status, axum::http::StatusCode::OK);
for key in [
r#""uptime_s":"#,
r#""free_fraction":"#,
r#""queries":{"count":"#,
r#""mean_ms":"#,
r#""max_ms":"#,
] {
assert!(body.contains(key), "{key} missing from {body}");
}
for signal in pipeline::SIGNALS {
assert!(body.contains(&format!(r#""{signal}":{{"shed":"#)), "{body}");
}
for key in [
"refused",
"blocks_published",
"rows",
"bytes",
"blocks_on_disk",
"open_block_age_s",
"stalled_s",
] {
assert!(body.contains(&format!(r#""{key}":"#)), "{key}: {body}");
}
assert!(body.contains(r#""blocks_on_disk":0"#), "{body}");
assert!(body.contains(r#""open_block_age_s":null"#), "{body}");
let free = api::parse(&body).expect("the document is KYAML")["free_fraction"]
.as_f64()
.expect("a readable volume reports a fraction");
assert!(
free > 0.0 && free <= 1.0,
"free_fraction is a fraction of the volume: {free}"
);
let gone = std::sync::Arc::new(dir.join("no-such-volume"));
let (status, body) = text(stats(axum::extract::State(gone)).await).await;
assert_eq!(status, axum::http::StatusCode::OK);
assert!(body.contains(r#""free_fraction":null"#), "{body}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn the_peak_resident_set_is_reported_in_bytes() {
let rss = peak_rss();
assert!(rss > 1 << 20, "{rss} bytes is below a running process");
assert!(rss < 100 << 30, "{rss} bytes is a unit mistake, not an RSS");
}
#[test]
fn the_tui_refuses_a_data_directory_the_server_would_have_refused() {
let dir = tmp("tui-guard");
let local = |p: &Path| tui::Source::Local(p.to_path_buf());
check_source(&local(&dir), true).unwrap();
for bad in [dir.join("nope"), {
let f = dir.join("a-file");
std::fs::write(&f, b"").unwrap();
f
}] {
let e = check_source(&local(&bad), true).unwrap_err().to_string();
assert!(e.contains(&bad.display().to_string()), "{e}");
assert!(e.contains("not a directory"), "{e}");
check_source(&local(&bad), false).unwrap();
let remote = tui::Source::Remote(bad.display().to_string());
check_source(&remote, true).unwrap();
}
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn a_boot_replays_every_frame_no_block_claims() {
use mira_core::wal::Signal;
use mira_proto::collector::metrics::v1::ExportMetricsServiceRequest;
use mira_proto::collector::trace::v1::ExportTraceServiceRequest;
use mira_proto::metrics::v1::metric::Data;
use mira_proto::metrics::v1::number_data_point::Value as NumValue;
use mira_proto::metrics::v1::{
Gauge, Metric, NumberDataPoint, ResourceMetrics, ScopeMetrics,
};
use mira_proto::trace::v1::{ResourceSpans, ScopeSpans, Span};
use prost::Message as _;
let dir = tmp("replay");
let node = mira_core::block::node_id("replaynode");
let spans = ExportTraceServiceRequest {
resource_spans: vec![ResourceSpans {
scope_spans: vec![ScopeSpans {
spans: vec![Span {
trace_id: vec![0x11; 16].into(),
span_id: vec![0x22; 8].into(),
name: "GET /".into(),
start_time_unix_nano: 3_000,
end_time_unix_nano: 3_500,
..Default::default()
}],
..Default::default()
}],
..Default::default()
}],
};
let points = ExportMetricsServiceRequest {
resource_metrics: vec![ResourceMetrics {
scope_metrics: vec![ScopeMetrics {
metrics: vec![Metric {
name: "process.cpu".into(),
data: Some(Data::Gauge(Gauge {
data_points: vec![NumberDataPoint {
time_unix_nano: 4_000,
value: Some(NumValue::AsDouble(0.5)),
..Default::default()
}],
})),
..Default::default()
}],
..Default::default()
}],
..Default::default()
}],
};
{
let wal = mira_core::wal::Wal::open(&dir, node).unwrap();
let logs = e2e::logs_export("checkout", 2_000, 4).encode_to_vec();
wal.append(Signal::Logs, &logs).unwrap();
wal.append(Signal::Traces, &spans.encode_to_vec()).unwrap();
wal.append(Signal::Metrics, &points.encode_to_vec())
.unwrap();
wal.append(Signal::Logs, b"\x08").unwrap();
}
let wal = std::sync::Arc::new(mira_core::wal::Wal::open(&dir, node).unwrap());
let pcfg = std::sync::Arc::new(pipeline::Config {
data_dir: dir.clone(),
node,
wal: Some(wal),
max_block_age: std::time::Duration::from_millis(50),
..Default::default()
});
let (logs, _ol, h_logs) = pipeline::spawn::<mira_core::logs::LogsBuilder>(&pcfg);
let (traces, _ot, h_traces) = pipeline::spawn::<mira_core::traces::TracesBuilder>(&pcfg);
let (metrics, _om, h_metrics) =
pipeline::spawn::<mira_core::metrics::MetricsBuilder>(&pcfg);
replay(&dir, node, logs.clone(), traces.clone(), metrics.clone())
.await
.unwrap();
drop((logs, traces, metrics));
for h in [h_logs, h_traces, h_metrics] {
h.await.unwrap();
}
for signal in pipeline::SIGNALS {
let published = mira_core::block::scan(&dir, signal).unwrap();
assert_eq!(published.len(), 1, "{signal} did not store its frame");
}
assert_eq!(
mira_core::block::wal_watermarks(&dir, node).unwrap(),
[4, 4, 4]
);
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn a_boot_after_a_hard_kill_reports_the_torn_tail_and_what_it_recovered() {
use mira_core::wal::Signal;
use prost::Message as _;
let dir = tmp("torn-replay");
let node = mira_core::block::node_id("tornnode");
{
let wal = mira_core::wal::Wal::open(&dir, node).unwrap();
for i in 0..2 {
let body = e2e::logs_export("checkout", 2_000 + i, 1).encode_to_vec();
wal.append(Signal::Logs, &body).unwrap();
}
wal.sync().unwrap();
}
let seg = dir
.join(".wal")
.join(format!("{node:08x}-{:020}.wal", 0u64));
let len = std::fs::metadata(&seg).unwrap().len();
std::fs::OpenOptions::new()
.write(true)
.open(&seg)
.unwrap()
.set_len(len - 4)
.unwrap();
let pcfg = std::sync::Arc::new(pipeline::Config {
data_dir: dir.clone(),
node,
max_block_age: std::time::Duration::from_millis(50),
..Default::default()
});
let (logs, _ol, h_logs) = pipeline::spawn::<mira_core::logs::LogsBuilder>(&pcfg);
let (traces, _ot, h_traces) = pipeline::spawn::<mira_core::traces::TracesBuilder>(&pcfg);
let (metrics, _om, h_metrics) =
pipeline::spawn::<mira_core::metrics::MetricsBuilder>(&pcfg);
let (guard, log) = e2e::capture();
replay(&dir, node, logs.clone(), traces.clone(), metrics.clone())
.await
.expect("a torn tail is a recovery, not a refusal");
drop(guard);
let text = log.text();
assert!(text.contains("torn frame"), "{text}");
assert!(text.contains("segments=1"), "{text}");
assert!(
text.contains("recovered from the write-ahead log"),
"{text}"
);
assert!(text.contains("replayed=1"), "{text}");
assert!(text.contains("elapsed_ms="), "{text}");
drop((logs, traces, metrics));
for h in [h_logs, h_traces, h_metrics] {
h.await.unwrap();
}
assert_eq!(mira_core::block::scan(&dir, "logs").unwrap().len(), 1);
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn a_replay_with_nowhere_to_put_a_frame_refuses_to_finish_the_boot() {
use mira_core::wal::Signal;
use prost::Message as _;
let dir = tmp("replay-closed");
let node = mira_core::block::node_id("closednode");
{
let wal = mira_core::wal::Wal::open(&dir, node).unwrap();
wal.append(
Signal::Logs,
&e2e::logs_export("checkout", 2_000, 1).encode_to_vec(),
)
.unwrap();
wal.sync().unwrap();
}
let pcfg = std::sync::Arc::new(pipeline::Config {
data_dir: dir.clone(),
node,
..Default::default()
});
let (logs, _ol, mut h_logs) = pipeline::spawn::<mira_core::logs::LogsBuilder>(&pcfg);
let (traces, _ot, _ht) = pipeline::spawn::<mira_core::traces::TracesBuilder>(&pcfg);
let (metrics, _om, _hm) = pipeline::spawn::<mira_core::metrics::MetricsBuilder>(&pcfg);
h_logs.abort();
let _ = h_logs.await;
let e = replay(&dir, node, logs, traces, metrics)
.await
.expect_err("a frame with nowhere to go must stop the boot");
let e = e.to_string();
assert!(e.contains("flusher"), "{e}");
assert!(e.contains(&dir.display().to_string()), "{e}");
let _ = std::fs::remove_dir_all(&dir);
}
}