diff --git a/Cargo.lock b/Cargo.lock index 2969d38..0e73921 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1741,6 +1741,7 @@ dependencies = [ "criterion", "futures-util", "hex", + "indexmap 2.14.0", "proptest", "rand 0.8.6", "redis", diff --git a/crates/miroir-core/src/config/advanced.rs b/crates/miroir-core/src/config/advanced.rs index 66330e2..476470a 100644 --- a/crates/miroir-core/src/config/advanced.rs +++ b/crates/miroir-core/src/config/advanced.rs @@ -859,6 +859,42 @@ impl Default for SearchUiAnalyticsConfig { } } +// --------------------------------------------------------------------------- +// 13.22 Rebalancer (P4.1 background worker) +// --------------------------------------------------------------------------- + +/// Rebalancer configuration (plan §4 Phase 4.1). +/// +/// The rebalancer is a background Tokio task that orchestrates shard migration +/// during topology changes (node add/drain/fail/recover). Uses leader lease to +/// ensure only one pod runs the rebalancer at a time. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(default)] +pub struct RebalancerConfig { + /// Enable or disable the rebalancer background worker. + pub enabled: bool, + /// Maximum concurrent shard migrations (plan §14.2 memory budget). + pub max_concurrent_migrations: usize, + /// Check interval for topology changes (milliseconds). + pub check_interval_ms: u64, + /// Leader lease TTL (milliseconds) — must be longer than check_interval. + pub leader_lease_ttl_ms: u64, + /// Batch size for document migration pagination. + pub migration_batch_size: u32, +} + +impl Default for RebalancerConfig { + fn default() -> Self { + Self { + enabled: true, + max_concurrent_migrations: 4, + check_interval_ms: 5000, + leader_lease_ttl_ms: 15000, + migration_batch_size: 1000, + } + } +} + // --------------------------------------------------------------------------- // §10 OpenTelemetry tracing // --------------------------------------------------------------------------- diff --git a/crates/miroir-proxy/src/main.rs b/crates/miroir-proxy/src/main.rs index 97caa26..8809162 100644 --- a/crates/miroir-proxy/src/main.rs +++ b/crates/miroir-proxy/src/main.rs @@ -353,6 +353,27 @@ async fn main() -> anyhow::Result<()> { }); } + // Load aliases from task store on startup (plan §13.7) + // Aliases must be loaded before any request routing to ensure consistent resolution + if let Some(ref task_store) = state.admin.task_store { + let alias_registry = state.admin.alias_registry.clone(); + let store = task_store.clone(); + tokio::spawn(async move { + info!("loading aliases from task store"); + match alias_registry.sync_from_store(&*store).await { + Ok(()) => { + let count = alias_registry.list().await.len(); + info!(count, "aliases loaded successfully"); + } + Err(e) => { + error!(error = %e, "failed to load aliases from task store"); + } + } + }); + } else { + info!("alias loading skipped (no task store configured)"); + } + // Start drift reconciler background task (plan §13.5) // Uses the drift_reconciler from AppState which is already configured if let Some(ref drift_reconciler) = state.admin.drift_reconciler { @@ -534,8 +555,9 @@ async fn main() -> anyhow::Result<()> { // 1. csrf_middleware - runs first // 2. auth_middleware // 3. Extension layers - // 4. request_id_middleware - sets X-Request-Id header - // 5. telemetry_middleware - reads X-Request-Id, creates tracing span with request_id field + // 4. session_pinning_middleware - extracts X-Miroir-Session header + // 5. request_id_middleware - sets X-Request-Id header + // 6. telemetry_middleware - reads X-Request-Id, creates tracing span with request_id field // The span's request_id field propagates to all child log events via with_current_span(true) // // To achieve this order, we add layers in REVERSE (last call = outermost): @@ -549,6 +571,9 @@ async fn main() -> anyhow::Result<()> { .layer(axum::middleware::from_fn( middleware::request_id_middleware, )) + .layer(axum::middleware::from_fn( + middleware::session_pinning_middleware, + )) .layer(axum::extract::DefaultBodyLimit::max( config.server.max_body_bytes as usize, )) @@ -693,6 +718,20 @@ async fn run_health_checker(state: admin_endpoints::AppState) { // Update §14.9 resource-pressure metrics update_resource_pressure_metrics(&state.metrics); + + // Update §13.6 session pinning metrics + state.session_manager.update_metrics(|count| { + state.metrics.set_session_active_count(count as u64); + }); + + // Prune expired sessions (plan §13.6) + let pruned = state.session_manager.prune_expired().await; + if pruned > 0 { + info!( + pruned_count = pruned, + "pruned expired sessions" + ); + } } }