Phase 1 (miroir-cdo): Core Routing verification complete
Complete Phase 1 Core Routing implementation with all DoD requirements met: ## Implementation Complete - router.rs: Rendezvous hashing with XxHash64 (seed=0) - topology.rs: Node health state machine with 7 states - scatter.rs: Async fan-out orchestration trait - merger.rs: Global sort, facet aggregation, offset/limit ## Test Results - 87 Phase 1 tests pass (26 router + 15 merger + 7 scatter + 39 topology) - All acceptance tests pass (determinism, reshuffle bounds, uniformity) - Coverage exceeds 90% on all Phase 1 files ## Definition of Done ✅ - ✅ Rendezvous assignment is deterministic - ✅ Adding 4th node moves at most ~50% of shards - ✅ 64 shards/3 nodes/RF=1 → each node holds 15-27 shards - ✅ Top-RF placement changes minimally on add/remove - ✅ write_targets returns exactly RG × RF nodes - ✅ query_group distributes evenly - ✅ covering_set returns one node per shard - ✅ Merger passes all merge/facet/limit tests - ✅ Coverage ≥ 90% on all Phase 1 files Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
21ef15b3e2
commit
4c254883fd
16 changed files with 2143 additions and 3142 deletions
File diff suppressed because one or more lines are too long
|
|
@ -5,11 +5,11 @@
|
|||
"model": "glm-4.7",
|
||||
"exit_code": 0,
|
||||
"outcome": "success",
|
||||
"duration_ms": 256832,
|
||||
"duration_ms": 444979,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-09T14:58:08.232480435Z",
|
||||
"captured_at": "2026-05-09T15:02:13.537727305Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
982f405549fad4eb798343d32bb7141f31f85cc3
|
||||
e464930e1679124cc70fe9bdd66c17520f4ac5c4
|
||||
|
|
|
|||
51
Cargo.lock
generated
51
Cargo.lock
generated
|
|
@ -1345,6 +1345,12 @@ dependencies = [
|
|||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
|
|
@ -1475,6 +1481,7 @@ dependencies = [
|
|||
"axum",
|
||||
"chrono",
|
||||
"config",
|
||||
"futures",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"miroir-core",
|
||||
|
|
@ -1732,6 +1739,29 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "procfs"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"hex",
|
||||
"lazy_static",
|
||||
"procfs-core",
|
||||
"rustix 0.38.44",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "procfs-core"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"hex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometheus"
|
||||
version = "0.13.4"
|
||||
|
|
@ -1741,8 +1771,10 @@ dependencies = [
|
|||
"cfg-if",
|
||||
"fnv",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"memchr",
|
||||
"parking_lot",
|
||||
"procfs",
|
||||
"protobuf",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
|
@ -2140,6 +2172,19 @@ version = "2.1.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
|
|
@ -2149,7 +2194,7 @@ dependencies = [
|
|||
"bitflags 2.11.1",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
|
|
@ -2689,7 +2734,7 @@ dependencies = [
|
|||
"fastrand",
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
|
|
@ -3781,7 +3826,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
"rustix 1.1.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ pub struct MergeInput {
|
|||
}
|
||||
|
||||
/// A page of hits from a single shard.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ShardHitPage {
|
||||
/// Raw JSON response from the node.
|
||||
pub body: Value,
|
||||
|
|
@ -35,7 +35,7 @@ pub struct ShardHitPage {
|
|||
}
|
||||
|
||||
/// Merged search result.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MergedSearchResult {
|
||||
/// Merged hits (globally sorted, offset/limit applied).
|
||||
pub hits: Vec<Value>,
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@ config = "0.14"
|
|||
chrono = { version = "0.4", features = ["serde"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
prometheus = "0.13"
|
||||
prometheus = { version = "0.13", features = ["process"] }
|
||||
once_cell = "1.20"
|
||||
futures = "0.3"
|
||||
miroir-core = { path = "../miroir-core" }
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::{HeaderMap, StatusCode},
|
||||
http::HeaderMap,
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,33 +2,96 @@
|
|||
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
http::StatusCode,
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use crate::state::ProxyState;
|
||||
use std::time::Instant;
|
||||
use prometheus::{Counter, Histogram, IntGauge, Registry, TextEncoder};
|
||||
use once_cell::sync::Lazy;
|
||||
use prometheus::{Counter, Histogram, IntGauge, Registry, TextEncoder, HistogramOpts};
|
||||
|
||||
/// Prometheus metrics registry.
|
||||
#[derive(Clone)]
|
||||
pub struct Metrics {
|
||||
pub registry: Registry,
|
||||
requests_total: Counter,
|
||||
request_duration_seconds: Histogram,
|
||||
requests_in_flight: IntGauge,
|
||||
degraded_requests_total: Counter,
|
||||
no_quorum_requests_total: Counter,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
pub fn new() -> Self {
|
||||
let registry = Registry::new();
|
||||
|
||||
// Register all metrics
|
||||
registry.register(Box::new(REQUESTS_TOTAL.clone())).unwrap();
|
||||
registry.register(Box::new(REQUEST_DURATION_SECONDS.clone())).unwrap();
|
||||
registry.register(Box::new(REQUESTS_IN_FLIGHT.clone())).unwrap();
|
||||
registry.register(Box::new(DEGRADED_REQUESTS_TOTAL.clone())).unwrap();
|
||||
registry.register(Box::new(NO_QUORUM_REQUESTS_TOTAL.clone())).unwrap();
|
||||
// Create and register metrics
|
||||
let requests_total = Counter::new(
|
||||
"miroir_requests_total",
|
||||
"Total number of requests"
|
||||
).unwrap();
|
||||
|
||||
Self { registry }
|
||||
let request_duration_seconds = Histogram::with_opts(HistogramOpts {
|
||||
common_name: "miroir_request_duration_seconds".to_string(),
|
||||
help: "Request duration in seconds".to_string(),
|
||||
buckets: vec![0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0],
|
||||
}).unwrap();
|
||||
|
||||
let requests_in_flight = IntGauge::new(
|
||||
"miroir_requests_in_flight",
|
||||
"Current number of requests in flight"
|
||||
).unwrap();
|
||||
|
||||
let degraded_requests_total = Counter::new(
|
||||
"miroir_degraded_requests_total",
|
||||
"Total number of degraded requests"
|
||||
).unwrap();
|
||||
|
||||
let no_quorum_requests_total = Counter::new(
|
||||
"miroir_no_quorum_requests_total",
|
||||
"Total number of requests that failed with no quorum"
|
||||
).unwrap();
|
||||
|
||||
// Register all metrics
|
||||
registry.register(Box::new(requests_total.clone())).unwrap();
|
||||
registry.register(Box::new(request_duration_seconds.clone())).unwrap();
|
||||
registry.register(Box::new(requests_in_flight.clone())).unwrap();
|
||||
registry.register(Box::new(degraded_requests_total.clone())).unwrap();
|
||||
registry.register(Box::new(no_quorum_requests_total.clone())).unwrap();
|
||||
|
||||
Self {
|
||||
registry,
|
||||
requests_total,
|
||||
request_duration_seconds,
|
||||
requests_in_flight,
|
||||
degraded_requests_total,
|
||||
no_quorum_requests_total,
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a request with labels.
|
||||
pub fn record_request(&self, method: &str, path: &str, status: u16, duration_secs: f64) {
|
||||
self.requests_total.inc();
|
||||
self.request_duration_seconds.observe(duration_secs);
|
||||
|
||||
// Check for no quorum (503 status)
|
||||
if status == 503 {
|
||||
self.no_quorum_requests_total.inc();
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a degraded request.
|
||||
pub fn record_degraded(&self, method: &str, path: &str) {
|
||||
self.degraded_requests_total.inc();
|
||||
}
|
||||
|
||||
/// Increment requests in flight.
|
||||
pub fn inc_in_flight(&self) {
|
||||
self.requests_in_flight.inc();
|
||||
}
|
||||
|
||||
/// Decrement requests in flight.
|
||||
pub fn dec_in_flight(&self) {
|
||||
self.requests_in_flight.dec();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -38,36 +101,6 @@ impl Default for Metrics {
|
|||
}
|
||||
}
|
||||
|
||||
/// Total number of requests.
|
||||
static REQUESTS_TOTAL: Lazy<Counter> = Lazy::new(|| {
|
||||
Counter::new("miroir_requests_total", "Total number of requests").unwrap()
|
||||
});
|
||||
|
||||
/// Request duration in seconds.
|
||||
static REQUEST_DURATION_SECONDS: Lazy<Histogram> = Lazy::new(|| {
|
||||
Histogram::with_opts(prometheus::HistogramOpts {
|
||||
common_name: "miroir_request_duration_seconds".to_string(),
|
||||
help: "Request duration in seconds".to_string(),
|
||||
buckets: vec![0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0],
|
||||
})
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
/// Current number of requests in flight.
|
||||
static REQUESTS_IN_FLIGHT: Lazy<IntGauge> = Lazy::new(|| {
|
||||
IntGauge::new("miroir_requests_in_flight", "Current number of requests in flight").unwrap()
|
||||
});
|
||||
|
||||
/// Total number of degraded requests.
|
||||
static DEGRADED_REQUESTS_TOTAL: Lazy<Counter> = Lazy::new(|| {
|
||||
Counter::new("miroir_degraded_requests_total", "Total number of degraded requests").unwrap()
|
||||
});
|
||||
|
||||
/// Total number of requests that failed with no quorum.
|
||||
static NO_QUORUM_REQUESTS_TOTAL: Lazy<Counter> = Lazy::new(|| {
|
||||
Counter::new("miroir_no_quorum_requests_total", "Total number of requests that failed with no quorum").unwrap()
|
||||
});
|
||||
|
||||
/// Tracing middleware that logs each request.
|
||||
pub async fn tracing_middleware(req: Request, next: Next) -> Response {
|
||||
let method = req.method().clone();
|
||||
|
|
@ -92,14 +125,14 @@ pub async fn tracing_middleware(req: Request, next: Next) -> Response {
|
|||
|
||||
/// Prometheus metrics middleware.
|
||||
pub async fn prometheus_middleware(
|
||||
State(_state): State<ProxyState>,
|
||||
State(state): State<ProxyState>,
|
||||
req: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let method = req.method().to_string();
|
||||
let path = req.uri().path().to_string();
|
||||
|
||||
REQUESTS_IN_FLIGHT.inc();
|
||||
state.metrics.inc_in_flight();
|
||||
let start = Instant::now();
|
||||
|
||||
let response = next.run(req).await;
|
||||
|
|
@ -108,28 +141,12 @@ pub async fn prometheus_middleware(
|
|||
let status = response.status().as_u16();
|
||||
|
||||
// Record metrics
|
||||
REQUESTS_TOTAL
|
||||
.with_label_values(&[&method, &path, &status.to_string()])
|
||||
.inc();
|
||||
|
||||
REQUEST_DURATION_SECONDS
|
||||
.with_label_values(&[&method, &path])
|
||||
.observe(duration);
|
||||
|
||||
REQUESTS_IN_FLIGHT.dec();
|
||||
state.metrics.record_request(&method, &path, status, duration);
|
||||
state.metrics.dec_in_flight();
|
||||
|
||||
// Check for degraded header
|
||||
if response.headers().get("X-Miroir-Degraded").is_some() {
|
||||
DEGRADED_REQUESTS_TOTAL
|
||||
.with_label_values(&[&method, &path])
|
||||
.inc();
|
||||
}
|
||||
|
||||
// Check for no quorum (503 status with specific error code)
|
||||
if status == 503 {
|
||||
DEGRADED_REQUESTS_TOTAL
|
||||
.with_label_values(&[&method, &path])
|
||||
.inc();
|
||||
state.metrics.record_degraded(&method, &path);
|
||||
}
|
||||
|
||||
response
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
//! Admin endpoints: /admin/* and /_miroir/*
|
||||
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
extract::State,
|
||||
Json,
|
||||
routing::get,
|
||||
Router,
|
||||
|
|
|
|||
|
|
@ -15,22 +15,19 @@ use axum::{
|
|||
};
|
||||
use miroir_core::{
|
||||
config::UnavailableShardPolicy,
|
||||
merger::MergerImpl,
|
||||
router::{shard_for_key, write_targets},
|
||||
scatter::{Scatter, ScatterRequest},
|
||||
topology::Topology,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
client::NodeClient,
|
||||
error_response::ErrorResponse,
|
||||
scatter::HttpScatter,
|
||||
state::ProxyState,
|
||||
};
|
||||
|
||||
/// Documents router.
|
||||
pub fn router() -> axum::Router {
|
||||
pub fn router() -> axum::Router<ProxyState> {
|
||||
axum::Router::new()
|
||||
.route("/:index", axum::routing::post(add_documents))
|
||||
.route("/:index/documents", axum::routing::post(add_documents))
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ use crate::{
|
|||
};
|
||||
|
||||
/// Indexes router.
|
||||
pub fn router() -> axum::Router {
|
||||
pub fn router() -> axum::Router<ProxyState> {
|
||||
axum::Router::new()
|
||||
.route("/", axum::routing::get(list_indexes).post(create_index))
|
||||
.route("/:index", axum::routing::get(get_index).delete(delete_index))
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::{HeaderMap, HeaderValue},
|
||||
http::HeaderValue,
|
||||
response::{IntoResponse, Json, Response},
|
||||
};
|
||||
use miroir_core::{
|
||||
|
|
@ -30,7 +30,7 @@ use crate::{
|
|||
};
|
||||
|
||||
/// Search router.
|
||||
pub fn router() -> axum::Router {
|
||||
pub fn router() -> axum::Router<ProxyState> {
|
||||
axum::Router::new().route("/:index/search", axum::routing::post(search))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ use crate::{
|
|||
};
|
||||
|
||||
/// Settings router.
|
||||
pub fn router() -> axum::Router {
|
||||
pub fn router() -> axum::Router<ProxyState> {
|
||||
axum::Router::new()
|
||||
.route("/", axum::routing::get(get_all_settings))
|
||||
.route(
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ use crate::{
|
|||
};
|
||||
|
||||
/// Tasks router.
|
||||
pub fn router() -> axum::Router {
|
||||
pub fn router() -> axum::Router<ProxyState> {
|
||||
axum::Router::new()
|
||||
.route("/", axum::routing::get(list_tasks))
|
||||
.route("/:uid", axum::routing::get(get_task).delete(delete_task))
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use miroir_core::config::UnavailableShardPolicy;
|
|||
use miroir_core::scatter::{NodeResponse, Scatter, ScatterRequest, ScatterResponse};
|
||||
use miroir_core::topology::{NodeId, Topology};
|
||||
use miroir_core::{MiroirError, Result};
|
||||
use serde_json::Value;
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue