fix(clippy): apply auto-fixes for unused imports and variables

Apply cargo clippy --fix to remove unused imports, prefix unused
variables with underscore, and fix various clippy warnings across
miroir-core, miroir-proxy, and miroir-ctl.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-05-25 05:15:22 -04:00
parent 465075b5b3
commit 0b3552ee4f
73 changed files with 794 additions and 983 deletions

View file

@ -41,7 +41,7 @@ fn bench_shard_for_key_batch(c: &mut Criterion) {
/// Benchmark: assign_shard_in_group for a single shard.
fn bench_assign_shard_single(c: &mut Criterion) {
let nodes: Vec<NodeId> = (0..TARGET_NODES)
.map(|i| NodeId::new(format!("node-{}", i)))
.map(|i| NodeId::new(format!("node-{i}")))
.collect();
c.bench_function("assign_shard_in_group_single", |b| {
@ -58,7 +58,7 @@ fn bench_assign_shard_single(c: &mut Criterion) {
/// Benchmark: assign_shard_in_group for all shards.
fn bench_assign_shard_all(c: &mut Criterion) {
let nodes: Vec<NodeId> = (0..TARGET_NODES)
.map(|i| NodeId::new(format!("node-{}", i)))
.map(|i| NodeId::new(format!("node-{i}")))
.collect();
c.bench_function("assign_shard_in_group_64_shards", |b| {
@ -86,7 +86,7 @@ fn bench_full_routing_pipeline(c: &mut Criterion) {
.collect();
let nodes: Vec<NodeId> = (0..TARGET_NODES)
.map(|i| NodeId::new(format!("node-{}", i)))
.map(|i| NodeId::new(format!("node-{i}")))
.collect();
// Pre-compute shard assignments
@ -110,7 +110,7 @@ fn bench_full_routing_pipeline(c: &mut Criterion) {
/// Benchmark: Varying shard counts.
fn bench_varying_shard_count(c: &mut Criterion) {
let nodes: Vec<NodeId> = (0..TARGET_NODES)
.map(|i| NodeId::new(format!("node-{}", i)))
.map(|i| NodeId::new(format!("node-{i}")))
.collect();
let mut group = c.benchmark_group("varying_shard_count");
@ -141,7 +141,7 @@ fn bench_varying_node_count(c: &mut Criterion) {
let mut group = c.benchmark_group("varying_node_count");
for node_count in [2, 3, 4, 5, 8, 10].iter() {
let nodes: Vec<NodeId> = (0..*node_count)
.map(|i| NodeId::new(format!("node-{}", i)))
.map(|i| NodeId::new(format!("node-{i}")))
.collect();
group.bench_with_input(
@ -168,7 +168,7 @@ fn bench_varying_node_count(c: &mut Criterion) {
/// Benchmark: Varying replication factors.
fn bench_varying_rf(c: &mut Criterion) {
let nodes: Vec<NodeId> = (0..10)
.map(|i| NodeId::new(format!("node-{}", i)))
.map(|i| NodeId::new(format!("node-{i}")))
.collect();
let mut group = c.benchmark_group("varying_rf");

View file

@ -248,7 +248,7 @@ impl AliasRegistry {
let mut aliases = self.aliases.write().await;
let alias = aliases
.get_mut(name)
.ok_or_else(|| MiroirError::NotFound(format!("alias '{}' not found", name)))?;
.ok_or_else(|| MiroirError::NotFound(format!("alias '{name}' not found")))?;
alias.flip(new_target)?;
Ok(())
}
@ -258,7 +258,7 @@ impl AliasRegistry {
let mut aliases = self.aliases.write().await;
let alias = aliases
.get_mut(name)
.ok_or_else(|| MiroirError::NotFound(format!("alias '{}' not found", name)))?;
.ok_or_else(|| MiroirError::NotFound(format!("alias '{name}' not found")))?;
alias.update_targets(new_targets)?;
Ok(())
}
@ -301,7 +301,7 @@ impl AliasFlipCoordinator {
new_uid: String,
pod_id: String,
) -> Self {
let scope = format!("alias_flip:{}", alias_name);
let scope = format!("alias_flip:{alias_name}");
let extra_state = AliasFlipExtraState {
new_uid,

View file

@ -250,12 +250,12 @@ impl CanaryRunner {
// Parse query
let query: SearchQuery = serde_json::from_str(&canary.query_json)
.map_err(|e| MiroirError::InvalidRequest(format!("Invalid canary query: {}", e)))?;
.map_err(|e| MiroirError::InvalidRequest(format!("Invalid canary query: {e}")))?;
// Parse assertions
let assertions: Vec<CanaryAssertion> = serde_json::from_str(&canary.assertions_json)
.map_err(|e| {
MiroirError::InvalidRequest(format!("Invalid canary assertions: {}", e))
MiroirError::InvalidRequest(format!("Invalid canary assertions: {e}"))
})?;
// Execute the search query against the index
@ -269,7 +269,7 @@ impl CanaryRunner {
let mut failed_assertions = Vec::new();
for assertion in &assertions {
if let Some(failure) =
self.evaluate_assertion(&assertion, &search_response, latency_ms, &canary.index_uid)
self.evaluate_assertion(assertion, &search_response, latency_ms, &canary.index_uid)
{
failed_assertions.push(failure);
}
@ -346,7 +346,7 @@ impl CanaryRunner {
assertion_type: "top_hit_id".to_string(),
expected: serde_json::json!(value),
actual: serde_json::json!(actual),
message: format!("Top hit ID mismatch: expected {}, got {}", value, actual),
message: format!("Top hit ID mismatch: expected {value}, got {actual}"),
});
}
}
@ -369,7 +369,7 @@ impl CanaryRunner {
assertion_type: "top_k_contains".to_string(),
expected: serde_json::json!(ids),
actual: serde_json::json!(top_k_ids),
message: format!("Top {} missing IDs: {:?}", k, missing),
message: format!("Top {k} missing IDs: {missing:?}"),
});
}
}
@ -393,7 +393,7 @@ impl CanaryRunner {
assertion_type: "max_p95_ms".to_string(),
expected: serde_json::json!(value),
actual: serde_json::json!(latency_ms),
message: format!("Latency exceeded p95: {}ms > {}ms", latency_ms, value),
message: format!("Latency exceeded p95: {latency_ms}ms > {value}ms"),
});
}
}
@ -407,8 +407,7 @@ impl CanaryRunner {
expected: serde_json::json!(value),
actual: serde_json::json!(current_version),
message: format!(
"Settings version below minimum: {} < {}",
current_version, value
"Settings version below minimum: {current_version} < {value}"
),
});
}
@ -427,7 +426,7 @@ impl CanaryRunner {
assertion_type: "must_not_contain_id".to_string(),
expected: serde_json::json!(null),
actual: serde_json::json!(id),
message: format!("Results contain forbidden ID: {}", id),
message: format!("Results contain forbidden ID: {id}"),
});
}
}
@ -499,10 +498,10 @@ pub fn create_canary(
index_uid,
interval_s,
query_json: serde_json::to_string(&query).map_err(|e| {
MiroirError::InvalidRequest(format!("Failed to serialize query: {}", e))
MiroirError::InvalidRequest(format!("Failed to serialize query: {e}"))
})?,
assertions_json: serde_json::to_string(&assertions).map_err(|e| {
MiroirError::InvalidRequest(format!("Failed to serialize assertions: {}", e))
MiroirError::InvalidRequest(format!("Failed to serialize assertions: {e}"))
})?,
enabled: true,
created_at: now,

View file

@ -420,7 +420,7 @@ impl CdcInternalQueue {
// Convert analytics event to a CdcEvent for storage
let cdc_event = CdcEvent {
mtask_id: format!("analytics:{}", event_id),
mtask_id: format!("analytics:{event_id}"),
index: index.clone(),
operation: if event_type == "click_through" {
CdcOperation::ClickThrough

View file

@ -96,7 +96,7 @@ impl DriftReconciler {
}
_ = leader_election_interval.tick() => {
// Renew leader lease
let _ = self.renew_leader_lease();
self.renew_leader_lease();
}
}
}
@ -208,7 +208,7 @@ impl DriftReconciler {
)
.send()
.await
.map_err(|e| MiroirError::Task(format!("failed to list indexes: {}", e)))?;
.map_err(|e| MiroirError::Task(format!("failed to list indexes: {e}")))?;
if !response.status().is_success() {
return Err(MiroirError::Task(format!(
@ -220,7 +220,7 @@ impl DriftReconciler {
let json: Value = response
.json()
.await
.map_err(|e| MiroirError::Task(format!("failed to parse indexes: {}", e)))?;
.map_err(|e| MiroirError::Task(format!("failed to parse indexes: {e}")))?;
let results = json
.get("results")
@ -272,8 +272,7 @@ impl DriftReconciler {
}
Err(e) => {
return Ok(DriftCheckResult::Error(MiroirError::Task(format!(
"node {} request failed: {}",
node_id, e
"node {node_id} request failed: {e}"
))));
}
}
@ -337,7 +336,7 @@ impl DriftReconciler {
.send()
.await
.map_err(|e| {
MiroirError::Task(format!("failed to fetch settings for repair: {}", e))
MiroirError::Task(format!("failed to fetch settings for repair: {e}"))
})?;
if !response.status().is_success() {
@ -348,7 +347,7 @@ impl DriftReconciler {
}
let correct_settings: Value = response.json().await.map_err(|e| {
MiroirError::Task(format!("failed to parse settings for repair: {}", e))
MiroirError::Task(format!("failed to parse settings for repair: {e}"))
})?;
// PATCH the drifted node with correct settings
@ -367,7 +366,7 @@ impl DriftReconciler {
.json(&correct_settings)
.send()
.await
.map_err(|e| MiroirError::Task(format!("failed to repair settings: {}", e)))?;
.map_err(|e| MiroirError::Task(format!("failed to repair settings: {e}")))?;
if !patch_response.status().is_success() {
return Err(MiroirError::Task(format!(
@ -390,7 +389,7 @@ impl DriftReconciler {
.node_addresses
.iter()
.enumerate()
.map(|(i, addr)| (format!("node-{}", i), addr.clone()))
.map(|(i, addr)| (format!("node-{i}"), addr.clone()))
.collect()
}
}

View file

@ -3,6 +3,12 @@
/// Placeholder dump handler
pub struct DumpHandler;
impl Default for DumpHandler {
fn default() -> Self {
Self::new()
}
}
impl DumpHandler {
pub fn new() -> Self {
Self

View file

@ -59,7 +59,7 @@ pub fn split_dump_into_chunks(data: &[u8], chunk_size_bytes: u64) -> Vec<DumpChu
let reader = BufReader::new(cursor);
// Track line boundaries for chunking
let mut line_start = 0u64;
let line_start = 0u64;
let mut last_line_end = 0u64;
for line_result in reader.lines() {

View file

@ -236,7 +236,7 @@ impl<C: NodeClient + Send + Sync + 'static> DumpImportManager<C> {
// Parse NDJSON and route documents
let data_str = std::str::from_utf8(&dump_data)
.map_err(|e| MiroirError::InvalidRequest(format!("invalid UTF-8 in dump: {}", e)))?;
.map_err(|e| MiroirError::InvalidRequest(format!("invalid UTF-8 in dump: {e}")))?;
// Per-target buffers: (node_id, shard_id) -> Vec<documents>
let mut per_target_buffers: HashMap<(NodeId, u32), Vec<Value>> = HashMap::new();
@ -252,7 +252,7 @@ impl<C: NodeClient + Send + Sync + 'static> DumpImportManager<C> {
continue;
}
let mut doc: Value = serde_json::from_str(line)
.map_err(|e| MiroirError::InvalidRequest(format!("invalid JSON in dump: {}", e)))?;
.map_err(|e| MiroirError::InvalidRequest(format!("invalid JSON in dump: {e}")))?;
// Extract primary key value
let pk_value = doc
@ -260,8 +260,7 @@ impl<C: NodeClient + Send + Sync + 'static> DumpImportManager<C> {
.and_then(|v| v.as_str())
.ok_or_else(|| {
MiroirError::InvalidRequest(format!(
"missing or invalid primary key field: {}",
primary_key
"missing or invalid primary key field: {primary_key}"
))
})?;
@ -279,8 +278,7 @@ impl<C: NodeClient + Send + Sync + 'static> DumpImportManager<C> {
if target_nodes.is_empty() {
return Err(MiroirError::Topology(format!(
"no nodes for shard {}",
shard_id
"no nodes for shard {shard_id}"
)));
}
@ -288,7 +286,7 @@ impl<C: NodeClient + Send + Sync + 'static> DumpImportManager<C> {
for node in &target_nodes {
per_target_buffers
.entry((node.clone(), shard_id))
.or_insert_with(Vec::new)
.or_default()
.push(doc.clone());
}

View file

@ -182,7 +182,7 @@ impl Explainer {
let coalescing_eligible = self.config.query_coalescing.enabled;
// Check cache candidate
let cache_candidate = !query.filter.is_some() && query.q.is_some();
let cache_candidate = query.filter.is_none() && query.q.is_some();
// Estimate p95 latency
let estimated_p95_ms = self.estimate_latency(topology, chosen_group.id, &target_shards);
@ -250,7 +250,7 @@ impl Explainer {
let group_id = self.hash_tenant_to_group(tenant, topology);
return ChosenGroup {
id: group_id,
reason: format!("tenant affinity: {}", tenant),
reason: format!("tenant affinity: {tenant}"),
};
}
"explicit" => {
@ -258,7 +258,7 @@ impl Explainer {
{
return ChosenGroup {
id: group_id,
reason: format!("explicit tenant mapping: {}", tenant),
reason: format!("explicit tenant mapping: {tenant}"),
};
}
}

View file

@ -173,22 +173,21 @@ impl<C: SyncNodeClient> GroupSyncWorker<C> {
// Get source group
let source = topology.group(source_group).ok_or_else(|| {
crate::error::MiroirError::Topology(format!("source group {} not found", source_group))
crate::error::MiroirError::Topology(format!("source group {source_group} not found"))
})?;
// Get target group (the new group being added)
let target_group_id = {
let coord = self.coordinator.read().await;
let state = coord.get_state(addition_id).ok_or_else(|| {
crate::error::MiroirError::Topology(format!("addition {} not found", addition_id))
crate::error::MiroirError::Topology(format!("addition {addition_id} not found"))
})?;
state.group_id
};
let target = topology.group(target_group_id).ok_or_else(|| {
crate::error::MiroirError::Topology(format!(
"target group {} not found",
target_group_id
"target group {target_group_id} not found"
))
})?;
@ -199,15 +198,13 @@ impl<C: SyncNodeClient> GroupSyncWorker<C> {
let source_node = source_healthy.first().ok_or_else(|| {
crate::error::MiroirError::Topology(format!(
"no healthy nodes in source group {}",
source_group
"no healthy nodes in source group {source_group}"
))
})?;
let target_node = target_healthy.first().ok_or_else(|| {
crate::error::MiroirError::Topology(format!(
"no healthy nodes in target group {}",
target_group_id
"no healthy nodes in target group {target_group_id}"
))
})?;
@ -242,14 +239,12 @@ impl<C: SyncNodeClient> GroupSyncWorker<C> {
.await
.map_err(|_| {
crate::error::MiroirError::Routing(format!(
"fetch timeout for shard {} from group {}",
shard_id, source_group
"fetch timeout for shard {shard_id} from group {source_group}"
))
})?
.map_err(|e| {
crate::error::MiroirError::Routing(format!(
"fetch failed for shard {}: {}",
shard_id, e
"fetch failed for shard {shard_id}: {e}"
))
})?;
@ -281,8 +276,7 @@ impl<C: SyncNodeClient> GroupSyncWorker<C> {
.await
.map_err(|e| {
crate::error::MiroirError::Routing(format!(
"write failed for shard {}: {}",
shard_id, e
"write failed for shard {shard_id}: {e}"
))
})?;
@ -309,7 +303,7 @@ impl<C: SyncNodeClient> GroupSyncWorker<C> {
coord
.shard_sync_complete(addition_id, shard_id, total_copied)
.map_err(|e| {
crate::error::MiroirError::Topology(format!("failed to mark shard complete: {}", e))
crate::error::MiroirError::Topology(format!("failed to mark shard complete: {e}"))
})?;
info!(
@ -349,7 +343,7 @@ impl<C: SyncNodeClient> GroupSyncWorker<C> {
let mut coord = self.coordinator.write().await;
let _ = coord
.fail_addition(addition_id, format!("sync timeout after {:?}", timeout));
.fail_addition(addition_id, format!("sync timeout after {timeout:?}"));
}
}
}

View file

@ -175,16 +175,12 @@ struct NodeIndexStats {
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Default)]
struct NodeStatsDetail {
#[serde(rename = "databaseSize", default)]
pub database_size: u64,
}
impl Default for NodeStatsDetail {
fn default() -> Self {
Self { database_size: 0 }
}
}
/// Aggregated index stats across all nodes.
#[derive(Debug, Clone)]
@ -293,7 +289,7 @@ impl IlmManager {
let policy_rows = task_store
.list_rollover_policies()
.map_err(|e| IlmError::CoordinatorError(format!("failed to load policies: {}", e)))?;
.map_err(|e| IlmError::CoordinatorError(format!("failed to load policies: {e}")))?;
let policies: Vec<RolloverPolicy> = policy_rows
.into_iter()
@ -314,13 +310,13 @@ impl IlmManager {
/// Convert a task store row to a RolloverPolicy.
fn row_to_policy(row: RolloverPolicyRow) -> std::result::Result<RolloverPolicy, IlmError> {
let triggers: RolloverTriggers = serde_json::from_str(&row.triggers_json)
.map_err(|e| IlmError::CoordinatorError(format!("invalid triggers JSON: {}", e)))?;
.map_err(|e| IlmError::CoordinatorError(format!("invalid triggers JSON: {e}")))?;
let retention: RetentionPolicy = serde_json::from_str(&row.retention_json)
.map_err(|e| IlmError::CoordinatorError(format!("invalid retention JSON: {}", e)))?;
.map_err(|e| IlmError::CoordinatorError(format!("invalid retention JSON: {e}")))?;
let template: IndexTemplate = serde_json::from_str(&row.template_json)
.map_err(|e| IlmError::CoordinatorError(format!("invalid template JSON: {}", e)))?;
.map_err(|e| IlmError::CoordinatorError(format!("invalid template JSON: {e}")))?;
Ok(RolloverPolicy {
name: row.name,
@ -439,7 +435,7 @@ impl IlmManager {
.iter()
.take(config.max_rollovers_per_check as usize)
{
if let Err(e) = Self::evaluate_policy(&state, &policy, &config).await {
if let Err(e) = Self::evaluate_policy(&state, policy, &config).await {
error!("ILM: error evaluating policy '{}': {}", policy.name, e);
}
}
@ -590,7 +586,7 @@ impl IlmWorker {
let policy_rows = self
.task_store
.list_rollover_policies()
.map_err(|e| IlmError::CoordinatorError(format!("failed to list policies: {}", e)))?;
.map_err(|e| IlmError::CoordinatorError(format!("failed to list policies: {e}")))?;
let mut rollover_count = 0;
@ -635,7 +631,7 @@ impl IlmWorker {
// Mark the rollover as failed in the coordinator
let _ = self
.coordinator
.fail(format!("rollover failed: {}", e))
.fail(format!("rollover failed: {e}"))
.await;
}
}
@ -695,8 +691,7 @@ impl IlmWorker {
let mut fired_triggers = Vec::new();
if age_triggered {
fired_triggers.push(format!(
"max_age ({}s >= {}s)",
age_seconds, max_age_seconds
"max_age ({age_seconds}s >= {max_age_seconds}s)"
));
}
if docs_triggered {
@ -794,13 +789,13 @@ impl IlmWorker {
.header("Authorization", format!("Bearer {}", &*self.master_key))
.send()
.await
.map_err(|e| IlmError::CoordinatorError(format!("request failed: {}", e)))?;
.map_err(|e| IlmError::CoordinatorError(format!("request failed: {e}")))?;
let status = response.status();
let body_text = response
.text()
.await
.map_err(|e| IlmError::CoordinatorError(format!("failed to read response: {}", e)))?;
.map_err(|e| IlmError::CoordinatorError(format!("failed to read response: {e}")))?;
if status.as_u16() == 404 {
// Index doesn't exist on this node
@ -819,7 +814,7 @@ impl IlmWorker {
}
serde_json::from_str(&body_text)
.map_err(|e| IlmError::CoordinatorError(format!("failed to parse stats: {}", e)))
.map_err(|e| IlmError::CoordinatorError(format!("failed to parse stats: {e}")))
}
/// Execute a rollover operation for a policy.
@ -911,14 +906,14 @@ impl IlmWorker {
.send()
.await
.map_err(|e| {
IlmError::RolloverFailed(format!("request to {} failed: {}", url, e))
IlmError::RolloverFailed(format!("request to {url} failed: {e}"))
})?;
let status = response.status();
let body_text = response
.text()
.await
.map_err(|e| IlmError::RolloverFailed(format!("failed to read response: {}", e)))?;
.map_err(|e| IlmError::RolloverFailed(format!("failed to read response: {e}")))?;
if status.as_u16() == 409 {
// Index already exists - this is ok for ILM (might have been partially created)
@ -956,12 +951,12 @@ impl IlmWorker {
.flip(alias_name, new_index.to_string())
.await
.map_err(|e| {
IlmError::AliasError(format!("failed to flip alias '{}': {}", alias_name, e))
IlmError::AliasError(format!("failed to flip alias '{alias_name}': {e}"))
})?;
// Persist to task store
let alias = self.alias_registry.get(alias_name).await.ok_or_else(|| {
IlmError::AliasError(format!("alias '{}' not found in registry", alias_name))
IlmError::AliasError(format!("alias '{alias_name}' not found in registry"))
})?;
// Update task store
@ -977,7 +972,7 @@ impl IlmWorker {
self.task_store
.create_alias(&new_alias)
.map_err(|e| IlmError::AliasError(format!("failed to persist alias flip: {}", e)))?;
.map_err(|e| IlmError::AliasError(format!("failed to persist alias flip: {e}")))?;
info!(
"ILM: flipped write alias '{}' to '{}'",
@ -1019,14 +1014,13 @@ impl IlmWorker {
.await
.map_err(|e| {
IlmError::AliasError(format!(
"failed to update multi-target alias '{}': {}",
alias_name, e
"failed to update multi-target alias '{alias_name}': {e}"
))
})?;
// Persist to task store
let alias = self.alias_registry.get(alias_name).await.ok_or_else(|| {
IlmError::AliasError(format!("alias '{}' not found in registry", alias_name))
IlmError::AliasError(format!("alias '{alias_name}' not found in registry"))
})?;
let new_alias = crate::task_store::NewAlias {
@ -1040,7 +1034,7 @@ impl IlmWorker {
};
self.task_store.create_alias(&new_alias).map_err(|e| {
IlmError::AliasError(format!("failed to persist read alias update: {}", e))
IlmError::AliasError(format!("failed to persist read alias update: {e}"))
})?;
info!(
@ -1140,7 +1134,7 @@ impl IlmWorker {
.header("Authorization", format!("Bearer {}", &*self.master_key))
.send()
.await
.map_err(|e| IlmError::RolloverFailed(format!("delete request failed: {}", e)))?;
.map_err(|e| IlmError::RolloverFailed(format!("delete request failed: {e}")))?;
let status = response.status();
@ -1179,7 +1173,7 @@ fn parse_index_date(date_str: &str) -> std::result::Result<u64, IlmError> {
use chrono::NaiveDate;
let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
.map_err(|_| IlmError::CoordinatorError(format!("invalid date format: {}", date_str)))?;
.map_err(|_| IlmError::CoordinatorError(format!("invalid date format: {date_str}")))?;
let datetime = date
.and_hms_opt(0, 0, 0)
@ -1196,23 +1190,23 @@ fn parse_duration(duration: &str) -> std::result::Result<u64, IlmError> {
if duration.ends_with('d') {
let days = duration[..duration.len() - 1]
.parse::<u64>()
.map_err(|_| IlmError::CoordinatorError(format!("invalid duration: {}", duration)))?;
.map_err(|_| IlmError::CoordinatorError(format!("invalid duration: {duration}")))?;
Ok(days * 86400)
} else if duration.ends_with('h') {
let hours = duration[..duration.len() - 1]
.parse::<u64>()
.map_err(|_| IlmError::CoordinatorError(format!("invalid duration: {}", duration)))?;
.map_err(|_| IlmError::CoordinatorError(format!("invalid duration: {duration}")))?;
Ok(hours * 3600)
} else if duration.ends_with('m') {
let minutes = duration[..duration.len() - 1]
.parse::<u64>()
.map_err(|_| IlmError::CoordinatorError(format!("invalid duration: {}", duration)))?;
.map_err(|_| IlmError::CoordinatorError(format!("invalid duration: {duration}")))?;
Ok(minutes * 60)
} else {
// Assume seconds if no unit
duration
.parse::<u64>()
.map_err(|_| IlmError::CoordinatorError(format!("invalid duration: {}", duration)))
.map_err(|_| IlmError::CoordinatorError(format!("invalid duration: {duration}")))
}
}

View file

@ -325,7 +325,7 @@ impl LeaderElection {
let current = self.task_store.get_leader_lease(scope)?;
let held = current
.as_ref()
.map(|l| &l.holder == &self.pod_id && l.expires_at > now_ms)
.map(|l| l.holder == self.pod_id && l.expires_at > now_ms)
.unwrap_or(false);
if held {

View file

@ -8,7 +8,7 @@ use crate::vector::{VectorHit, VectorMerger, VectorSearchConfig};
use crate::Result;
use serde_json::{Map, Value};
use std::cmp::Ordering;
use std::collections::{BTreeMap, HashMap};
use std::collections::BTreeMap;
/// Input to the merge operation.
#[derive(Debug, Clone)]

View file

@ -129,6 +129,7 @@ impl JobType {
/// Job progress tracking.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Default)]
pub struct JobProgress {
/// Bytes processed so far (for dump import).
pub bytes_processed: u64,
@ -140,16 +141,6 @@ pub struct JobProgress {
pub error: Option<String>,
}
impl Default for JobProgress {
fn default() -> Self {
Self {
bytes_processed: 0,
docs_routed: 0,
last_cursor: String::new(),
error: None,
}
}
}
/// Chunk specification for a job.
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -243,10 +234,10 @@ impl ModeCCoordinator {
pub fn enqueue_job(&self, type_: JobType, params: JobParams) -> Result<String> {
let job_id = format!("{}-{}", type_.as_str(), uuid::Uuid::new_v4());
let params_json = serde_json::to_string(&params)
.map_err(|e| MiroirError::TaskStore(format!("failed to serialize params: {}", e)))?;
.map_err(|e| MiroirError::TaskStore(format!("failed to serialize params: {e}")))?;
let progress = JobProgress::default();
let progress_json = serde_json::to_string(&progress)
.map_err(|e| MiroirError::TaskStore(format!("failed to serialize progress: {}", e)))?;
.map_err(|e| MiroirError::TaskStore(format!("failed to serialize progress: {e}")))?;
let new_job = NewJob {
id: job_id.clone(),
@ -346,7 +337,7 @@ impl ModeCCoordinator {
state: JobState,
) -> Result<()> {
let progress_json = serde_json::to_string(progress)
.map_err(|e| MiroirError::TaskStore(format!("failed to serialize progress: {}", e)))?;
.map_err(|e| MiroirError::TaskStore(format!("failed to serialize progress: {e}")))?;
self.task_store
.update_job_progress(job_id, state.as_str(), &progress_json)?;
@ -379,7 +370,7 @@ impl ModeCCoordinator {
failed_progress.error = Some(error.clone());
let progress_json = serde_json::to_string(&failed_progress)
.map_err(|e| MiroirError::TaskStore(format!("failed to serialize progress: {}", e)))?;
.map_err(|e| MiroirError::TaskStore(format!("failed to serialize progress: {e}")))?;
self.task_store
.update_job_progress(job_id, JobState::Failed.as_str(), &progress_json)?;
@ -403,7 +394,7 @@ impl ModeCCoordinator {
chunk_specs: Vec<JobChunk>,
) -> Result<Vec<String>> {
let params: JobParams = serde_json::from_str(&job.params)
.map_err(|e| MiroirError::TaskStore(format!("failed to deserialize params: {}", e)))?;
.map_err(|e| MiroirError::TaskStore(format!("failed to deserialize params: {e}")))?;
let total_chunks = chunk_specs.len() as u32;
let mut chunk_job_ids = Vec::new();
@ -424,11 +415,11 @@ impl ModeCCoordinator {
let chunk_job_id = format!("{}-chunk-{}", job.id, idx);
let params_json = serde_json::to_string(&chunk_params).map_err(|e| {
MiroirError::TaskStore(format!("failed to serialize chunk params: {}", e))
MiroirError::TaskStore(format!("failed to serialize chunk params: {e}"))
})?;
let progress = JobProgress::default();
let progress_json = serde_json::to_string(&progress).map_err(|e| {
MiroirError::TaskStore(format!("failed to serialize progress: {}", e))
MiroirError::TaskStore(format!("failed to serialize progress: {e}"))
})?;
let new_job = NewJob {
@ -555,13 +546,13 @@ impl ClaimedJob {
/// Parse the job parameters.
pub fn parse_params(&self) -> Result<JobParams> {
serde_json::from_str(&self.params)
.map_err(|e| MiroirError::TaskStore(format!("failed to deserialize params: {}", e)))
.map_err(|e| MiroirError::TaskStore(format!("failed to deserialize params: {e}")))
}
/// Parse the current progress.
pub fn parse_progress(&self) -> Result<JobProgress> {
serde_json::from_str(&self.progress)
.map_err(|e| MiroirError::TaskStore(format!("failed to deserialize progress: {}", e)))
.map_err(|e| MiroirError::TaskStore(format!("failed to deserialize progress: {e}")))
}
/// Check if this is a chunk job.

View file

@ -258,7 +258,7 @@ impl ModeCWorker {
}
// Calculate number of chunks (ceiling division)
let total_chunks = ((source_size + chunk_size_bytes - 1) / chunk_size_bytes) as u32;
let total_chunks = source_size.div_ceil(chunk_size_bytes) as u32;
(0..total_chunks)
.map(|i| {
@ -461,7 +461,7 @@ impl ModeCWorker {
// If this is a chunk job, process the shard range
if let Some(chunk) = &params.chunk {
let (start_shard, end_shard) = reshard_chunking::parse_reshard_chunk(chunk)
.map_err(|e| MiroirError::InvalidRequest(format!("invalid chunk spec: {}", e)))?;
.map_err(|e| MiroirError::InvalidRequest(format!("invalid chunk spec: {e}")))?;
info!(
"Processing reshard chunk {}/{} (shards {}-{})",
@ -560,7 +560,7 @@ impl ModeCWorker {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.map_err(|e| MiroirError::Task(format!("failed to create HTTP client: {}", e)))?;
.map_err(|e| MiroirError::Task(format!("failed to create HTTP client: {e}")))?;
// Get node addresses from environment or topology
// For now, use a placeholder - in production this would come from Topology
@ -577,7 +577,7 @@ impl ModeCWorker {
// Pagination through documents in this shard
loop {
// Fetch documents from live index with _miroir_shard filter
let filter = format!("_miroir_shard={}", shard_id);
let filter = format!("_miroir_shard={shard_id}");
let url = format!(
"{}/indexes/{}/documents?filter={}&limit={}&offset={}",
node_addresses.trim_end_matches('/'),
@ -589,10 +589,10 @@ impl ModeCWorker {
let response = client
.get(&url)
.header("Authorization", format!("Bearer {}", node_master_key))
.header("Authorization", format!("Bearer {node_master_key}"))
.send()
.await
.map_err(|e| MiroirError::Task(format!("fetch failed: {}", e)))?;
.map_err(|e| MiroirError::Task(format!("fetch failed: {e}")))?;
if !response.status().is_success() {
let status = response.status();
@ -601,15 +601,14 @@ impl ModeCWorker {
.await
.unwrap_or_else(|_| "unable to read error".to_string());
return Err(MiroirError::Task(format!(
"failed to fetch documents: HTTP {} - {}",
status, body
"failed to fetch documents: HTTP {status} - {body}"
)));
}
let json_body: serde_json::Value = response
.json()
.await
.map_err(|e| MiroirError::Task(format!("parse response failed: {}", e)))?;
.map_err(|e| MiroirError::Task(format!("parse response failed: {e}")))?;
let results = json_body
.get("results")
@ -657,11 +656,11 @@ impl ModeCWorker {
let response = client
.post(&write_url)
.header("Authorization", format!("Bearer {}", node_master_key))
.header("Authorization", format!("Bearer {node_master_key}"))
.json(&shadow_documents)
.send()
.await
.map_err(|e| MiroirError::Task(format!("write failed: {}", e)))?;
.map_err(|e| MiroirError::Task(format!("write failed: {e}")))?;
if !response.status().is_success() {
let status = response.status();
@ -670,8 +669,7 @@ impl ModeCWorker {
.await
.unwrap_or_else(|_| "unable to read error".to_string());
return Err(MiroirError::Task(format!(
"failed to write to shadow index: HTTP {} - {}",
status, body
"failed to write to shadow index: HTTP {status} - {body}"
)));
}
@ -691,7 +689,7 @@ impl ModeCWorker {
let progress = JobProgress {
bytes_processed: 0,
docs_routed: docs_backfilled,
last_cursor: format!("{}:{}", shard_id, offset),
last_cursor: format!("{shard_id}:{offset}"),
error: None,
};
coordinator.update_progress(

View file

@ -140,15 +140,15 @@ impl PeerDiscovery {
// Use system resolver config from /etc/resolv.conf (plan §14.5)
let resolver = Resolver::new(ResolverConfig::default(), ResolverOpts::default())
.map_err(|e| {
MiroirError::Discovery(format!("failed to create DNS resolver: {}", e))
MiroirError::Discovery(format!("failed to create DNS resolver: {e}"))
})?;
resolver.srv_lookup(&srv_name).map_err(|e| {
MiroirError::Discovery(format!("SRV lookup failed for {}: {}", srv_name, e))
MiroirError::Discovery(format!("SRV lookup failed for {srv_name}: {e}"))
})
})
.await
.map_err(|e| MiroirError::Discovery(format!("SRV lookup task failed: {}", e)))??;
.map_err(|e| MiroirError::Discovery(format!("SRV lookup task failed: {e}")))??;
// Extract pod names from SRV targets
// Each SRV record has a target like "miroir-miroir-0.miroir-headless.default.svc.cluster.local"

View file

@ -136,7 +136,7 @@ impl QueryPlanner {
let shard_id = shard_for_key(&literal, shard_count);
QueryPlan {
narrowed: true,
reason: format!("PK equality: {} = {}", pk_field, literal),
reason: format!("PK equality: {pk_field} = {literal}"),
target_shards: vec![shard_id],
warnings: vec![],
filter: Some(filter.clone()),
@ -176,7 +176,7 @@ impl QueryPlanner {
}
Err(e) => QueryPlan {
narrowed: false,
reason: format!("filter not narrowable: {}", e),
reason: format!("filter not narrowable: {e}"),
target_shards: vec![],
warnings: vec![],
filter: Some(filter.clone()),
@ -201,8 +201,8 @@ impl QueryPlanner {
));
}
if filter.contains(&format!("{} != ", pk_field))
|| filter.contains(&format!("{}<>", pk_field))
if filter.contains(&format!("{pk_field} != "))
|| filter.contains(&format!("{pk_field}<>"))
{
return Err(MiroirError::InvalidState(
"PK negation is not narrowable".to_string(),
@ -210,8 +210,8 @@ impl QueryPlanner {
}
// Try equality: pk = "literal"
let eq_pattern = format!(r#"{}\s*=\s*["']([^"']+)["']"#, pk_field);
if let Some(re) = regex::Regex::new(&eq_pattern).ok() {
let eq_pattern = format!(r#"{pk_field}\s*=\s*["']([^"']+)["']"#);
if let Ok(re) = regex::Regex::new(&eq_pattern) {
if let Some(caps) = re.captures(filter) {
if let Some(literal) = caps.get(1) {
return Ok(PkConstraint::Eq(literal.as_str().to_string()));
@ -220,8 +220,8 @@ impl QueryPlanner {
}
// Try IN list: pk IN ["literal1", "literal2", ...]
let in_pattern = format!(r#"{}\s+IN\s+\[(.+)\]"#, pk_field);
if let Some(re) = regex::Regex::new(&in_pattern).ok() {
let in_pattern = format!(r#"{pk_field}\s+IN\s+\[(.+)\]"#);
if let Ok(re) = regex::Regex::new(&in_pattern) {
if let Some(caps) = re.captures(filter) {
if let Some(list) = caps.get(1) {
let literals = self.parse_string_list(list.as_str())?;

View file

@ -586,7 +586,7 @@ impl Rebalancer {
for op in ops.values() {
for &mid in &op.migrations {
if let Some(state) = coordinator.get_state(mid) {
let key = format!("{}", mid);
let key = format!("{mid}");
let status = MigrationStatus {
id: mid.0,
new_node: state.new_node.to_string(),
@ -746,8 +746,7 @@ impl Rebalancer {
Ok(TopologyOperationResult {
id: op_id,
message: format!(
"Node {} addition started with {} shard migrations",
node_id_for_result, migrations_count
"Node {node_id_for_result} addition started with {migrations_count} shard migrations"
),
migrations_count,
})
@ -772,7 +771,7 @@ impl Rebalancer {
let group = topo
.groups()
.find(|g| g.id == node.replica_group)
.ok_or_else(|| RebalancerError::GroupNotFound(node.replica_group))?;
.ok_or(RebalancerError::GroupNotFound(node.replica_group))?;
if group.nodes().len() <= 1 {
return Err(RebalancerError::CannotRemoveLastNode);
@ -914,7 +913,7 @@ impl Rebalancer {
let group = topo
.groups()
.find(|g| g.id == node.replica_group)
.ok_or_else(|| RebalancerError::GroupNotFound(node.replica_group))?;
.ok_or(RebalancerError::GroupNotFound(node.replica_group))?;
if group.nodes().len() <= 1 {
return Err(RebalancerError::CannotRemoveLastNode);
@ -1211,7 +1210,7 @@ impl Rebalancer {
Ok(TopologyOperationResult {
id: op_id,
message: format!("Node {} marked as failed", node_id),
message: format!("Node {node_id} marked as failed"),
migrations_count: 0,
})
}
@ -1243,7 +1242,7 @@ impl Rebalancer {
// Check if RF needs to be restored (other healthy nodes exist in group)
let group = topo.groups().find(|g| g.id == replica_group);
let has_other_healthy = group.map_or(false, |g| {
let has_other_healthy = group.is_some_and(|g| {
g.nodes().iter().any(|nid| {
nid != &node_id_obj && topo.node(nid).map(|n| n.is_healthy()).unwrap_or(false)
})
@ -1263,8 +1262,7 @@ impl Rebalancer {
return Ok(TopologyOperationResult {
id: 0,
message: format!(
"Node {} recovered (no RF restore needed - no other healthy nodes in group)",
node_id
"Node {node_id} recovered (no RF restore needed - no other healthy nodes in group)"
),
migrations_count: 0,
});
@ -1371,8 +1369,7 @@ impl Rebalancer {
Ok(TopologyOperationResult {
id: op_id,
message: format!(
"Node {} recovered with RF restore ({} shards)",
node_id, migrations_count
"Node {node_id} recovered with RF restore ({migrations_count} shards)"
),
migrations_count,
})
@ -1397,7 +1394,7 @@ impl Rebalancer {
Ok(TopologyOperationResult {
id: op_id,
message: format!("Node {} recovered (no shards needed restoration)", node_id),
message: format!("Node {node_id} recovered (no shards needed restoration)"),
migrations_count: 0,
})
}
@ -1417,7 +1414,7 @@ impl Rebalancer {
let group = topo
.groups()
.find(|g| g.id == replica_group)
.ok_or_else(|| RebalancerError::GroupNotFound(replica_group))?;
.ok_or(RebalancerError::GroupNotFound(replica_group))?;
let mut shards_to_restore = Vec::new();
@ -1447,7 +1444,7 @@ impl Rebalancer {
let group = topo
.groups()
.find(|g| g.id == replica_group)
.ok_or_else(|| RebalancerError::GroupNotFound(replica_group))?;
.ok_or(RebalancerError::GroupNotFound(replica_group))?;
let assignment = assign_shard_in_group(shard.0, group.nodes(), topo.rf());
@ -1488,9 +1485,9 @@ impl Rebalancer {
let group = topo
.groups()
.find(|g| g.id == replica_group)
.ok_or_else(|| RebalancerError::GroupNotFound(replica_group))?;
.ok_or(RebalancerError::GroupNotFound(replica_group))?;
let existing_nodes: Vec<_> = group.nodes().iter().cloned().collect();
let existing_nodes: Vec<_> = group.nodes().to_vec();
let mut affected_shards = Vec::new();
// For each shard, check if the new node is in the new assignment
@ -1563,7 +1560,7 @@ impl Rebalancer {
let group = topo
.groups()
.find(|g| g.id == replica_group)
.ok_or_else(|| RebalancerError::GroupNotFound(replica_group))?;
.ok_or(RebalancerError::GroupNotFound(replica_group))?;
let other_nodes: Vec<_> = group
.nodes()
@ -1763,7 +1760,7 @@ async fn run_migration_task(
offset,
)
.await
.map_err(|e| RebalancerError::InvalidState(format!("fetch failed: {}", e)))?;
.map_err(|e| RebalancerError::InvalidState(format!("fetch failed: {e}")))?;
if docs.is_empty() {
break; // No more documents
@ -1772,7 +1769,7 @@ async fn run_migration_task(
// Write documents to target
exec.write_documents(&new_node, &new_node_address, &index_uid, docs.clone())
.await
.map_err(|e| RebalancerError::InvalidState(format!("write failed: {}", e)))?;
.map_err(|e| RebalancerError::InvalidState(format!("write failed: {e}")))?;
total_docs_copied += docs.len() as u64;
offset += limit;
@ -1828,14 +1825,14 @@ async fn run_migration_task(
0,
)
.await
.map_err(|e| RebalancerError::InvalidState(format!("delta fetch failed: {}", e)))?;
.map_err(|e| RebalancerError::InvalidState(format!("delta fetch failed: {e}")))?;
if !docs.is_empty() {
// Write any stragglers to target
exec.write_documents(&new_node, &new_node_address, &index_uid, docs)
.await
.map_err(|e| {
RebalancerError::InvalidState(format!("delta write failed: {}", e))
RebalancerError::InvalidState(format!("delta write failed: {e}"))
})?;
}
@ -2023,7 +2020,7 @@ async fn run_drain_task(
};
// For each shard being drained
for (shard_id, _old_node) in &old_owners {
for shard_id in old_owners.keys() {
info!(
migration_id = %mid,
shard_id = shard_id.0,
@ -2049,7 +2046,7 @@ async fn run_drain_task(
offset,
)
.await
.map_err(|e| RebalancerError::InvalidState(format!("fetch failed: {}", e)))?;
.map_err(|e| RebalancerError::InvalidState(format!("fetch failed: {e}")))?;
if docs.is_empty() {
break; // No more documents
@ -2058,7 +2055,7 @@ async fn run_drain_task(
// Write documents to new node
exec.write_documents(&new_node, &new_node_address, &index_uid, docs.clone())
.await
.map_err(|e| RebalancerError::InvalidState(format!("write failed: {}", e)))?;
.map_err(|e| RebalancerError::InvalidState(format!("write failed: {e}")))?;
total_docs_copied += docs.len() as u64;
offset += limit;
@ -2092,7 +2089,7 @@ async fn run_drain_task(
}
// Delta pass: re-read from draining node to catch stragglers
for (shard_id, _old_node) in &old_owners {
for shard_id in old_owners.keys() {
let (docs, _) = exec
.fetch_documents(
&drain_node_id,
@ -2103,14 +2100,14 @@ async fn run_drain_task(
0,
)
.await
.map_err(|e| RebalancerError::InvalidState(format!("delta fetch failed: {}", e)))?;
.map_err(|e| RebalancerError::InvalidState(format!("delta fetch failed: {e}")))?;
if !docs.is_empty() {
// Write any stragglers to new node
exec.write_documents(&new_node, &new_node_address, &index_uid, docs)
.await
.map_err(|e| {
RebalancerError::InvalidState(format!("delta write failed: {}", e))
RebalancerError::InvalidState(format!("delta write failed: {e}"))
})?;
}
@ -2127,7 +2124,7 @@ async fn run_drain_task(
}
// Delete drained shards from the draining node
for (shard_id, _old_node) in &old_owners {
for shard_id in old_owners.keys() {
if let Err(e) = exec
.delete_shard(&drain_node_id, &drain_node_address, &index_uid, shard_id.0)
.await
@ -2214,7 +2211,7 @@ impl HttpMigrationExecutor {
/// Build the filter string for fetching documents by shard.
fn shard_filter(&self, shard_id: u32) -> String {
format!("_miroir_shard = {}", shard_id)
format!("_miroir_shard = {shard_id}")
}
/// Make an authenticated GET request to a node.
@ -2238,7 +2235,7 @@ impl HttpMigrationExecutor {
.header("Authorization", format!("Bearer {}", self.node_master_key))
.send()
.await
.map_err(|e| format!("GET {} failed: {}", url, e))
.map_err(|e| format!("GET {url} failed: {e}"))
}
/// Make an authenticated POST request to a node.
@ -2264,7 +2261,7 @@ impl HttpMigrationExecutor {
.json(&body)
.send()
.await
.map_err(|e| format!("POST {} failed: {}", url, e))
.map_err(|e| format!("POST {url} failed: {e}"))
}
}
@ -2301,15 +2298,14 @@ impl MigrationExecutor for HttpMigrationExecutor {
.await
.unwrap_or_else(|_| "unable to read error".to_string());
return Err(format!(
"Failed to fetch documents from {}: HTTP {} - {}",
source_address, status, error_text
"Failed to fetch documents from {source_address}: HTTP {status} - {error_text}"
));
}
let json_body: serde_json::Value = response
.json()
.await
.map_err(|e| format!("Failed to parse response from {}: {}", source_address, e))?;
.map_err(|e| format!("Failed to parse response from {source_address}: {e}"))?;
// Meilisearch returns { results: [...], total: 123, limit: 20, offset: 0 }
let results = json_body
@ -2317,8 +2313,7 @@ impl MigrationExecutor for HttpMigrationExecutor {
.and_then(|v| v.as_array())
.ok_or_else(|| {
format!(
"Invalid response from {}: missing 'results' field",
source_address
"Invalid response from {source_address}: missing 'results' field"
)
})?;
@ -2342,7 +2337,7 @@ impl MigrationExecutor for HttpMigrationExecutor {
return Ok(());
}
let path = format!("indexes/{}/documents", index_uid);
let path = format!("indexes/{index_uid}/documents");
let response = self
.post_node(target_address, &path, serde_json::json!(documents))
@ -2380,7 +2375,7 @@ impl MigrationExecutor for HttpMigrationExecutor {
shard_id: u32,
) -> std::result::Result<(), String> {
let filter = self.shard_filter(shard_id);
let path = format!("indexes/{}/documents/delete", index_uid);
let path = format!("indexes/{index_uid}/documents/delete");
let body = serde_json::json!({
"filter": filter
@ -2395,8 +2390,7 @@ impl MigrationExecutor for HttpMigrationExecutor {
.await
.unwrap_or_else(|_| "unable to read error".to_string());
return Err(format!(
"Failed to delete shard {} from {}: HTTP {} - {}",
shard_id, node_address, status, error_text
"Failed to delete shard {shard_id} from {node_address}: HTTP {status} - {error_text}"
));
}

View file

@ -174,7 +174,7 @@ impl NodeClient for HttpNodeClient {
.json(&request.documents)
.send()
.await
.map_err(|e| NodeError::NetworkError(format!("write failed: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("write failed: {e}")))?;
if !response.status().is_success() {
let status = response.status();
@ -191,7 +191,7 @@ impl NodeClient for HttpNodeClient {
let json: Value = response
.json()
.await
.map_err(|e| NodeError::NetworkError(format!("parse response failed: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("parse response failed: {e}")))?;
let success = json
.get("success")
@ -252,7 +252,7 @@ impl NodeClient for HttpNodeClient {
.header("Authorization", format!("Bearer {}", self.node_master_key))
.send()
.await
.map_err(|e| NodeError::NetworkError(format!("fetch failed: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("fetch failed: {e}")))?;
if !response.status().is_success() {
let status = response.status();
@ -269,12 +269,11 @@ impl NodeClient for HttpNodeClient {
let json: Value = response
.json()
.await
.map_err(|e| NodeError::NetworkError(format!("parse failed: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("parse failed: {e}")))?;
let results = json
.get("results")
.and_then(|v| v.as_array())
.map(|v| v.clone())
.and_then(|v| v.as_array()).cloned()
.unwrap_or_default();
let total = json.get("total").and_then(|v| v.as_u64()).unwrap_or(0);
@ -306,7 +305,7 @@ impl NodeClient for HttpNodeClient {
.json(&request.body)
.send()
.await
.map_err(|e| NodeError::NetworkError(format!("search failed: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("search failed: {e}")))?;
if !response.status().is_success() {
let status = response.status();
@ -323,7 +322,7 @@ impl NodeClient for HttpNodeClient {
response
.json()
.await
.map_err(|e| NodeError::NetworkError(format!("parse response failed: {}", e)))
.map_err(|e| NodeError::NetworkError(format!("parse response failed: {e}")))
}
async fn preflight_node(
@ -350,7 +349,7 @@ impl NodeClient for HttpNodeClient {
.header("Authorization", format!("Bearer {}", self.node_master_key))
.send()
.await
.map_err(|e| NodeError::NetworkError(format!("preflight failed: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("preflight failed: {e}")))?;
if !response.status().is_success() {
let status = response.status();
@ -367,7 +366,7 @@ impl NodeClient for HttpNodeClient {
let json: Value = response
.json()
.await
.map_err(|e| NodeError::NetworkError(format!("parse response failed: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("parse response failed: {e}")))?;
let total_docs = json.get("total").and_then(|v| v.as_u64()).unwrap_or(0);
@ -412,7 +411,7 @@ impl NodeClient for HttpNodeClient {
.json(&body)
.send()
.await
.map_err(|e| NodeError::NetworkError(format!("delete failed: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("delete failed: {e}")))?;
if !response.status().is_success() {
let status = response.status();
@ -429,7 +428,7 @@ impl NodeClient for HttpNodeClient {
let json: Value = response
.json()
.await
.map_err(|e| NodeError::NetworkError(format!("parse response failed: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("parse response failed: {e}")))?;
Ok(crate::scatter::DeleteResponse {
success: json
@ -658,7 +657,7 @@ impl AntiEntropyWorker {
}
Err(e) => {
error!(scope = %scope, error = %e, "spawn_blocking task failed");
return Err(format!("spawn_blocking task failed: {}", e));
return Err(format!("spawn_blocking task failed: {e}"));
}
}
}
@ -710,7 +709,7 @@ impl AntiEntropyWorker {
Ok(())
}
Err(e) => Err(format!("anti-entropy pass failed: {}", e)),
Err(e) => Err(format!("anti-entropy pass failed: {e}")),
}
}
}

View file

@ -1887,15 +1887,15 @@ mod tests {
#[tokio::test]
async fn test_compute_affected_shards_for_add() {
let topo = Arc::new(RwLock::new(test_topology()));
let config = RebalancerWorkerConfig::default();
let _config = RebalancerWorkerConfig::default();
// Create a mock task store (in-memory for testing)
// Note: This would need a proper mock TaskStore implementation
// For now, we'll skip the full integration test
// Test that adding a node to group 0 affects some shards
let new_node_id = "node-new";
let replica_group = 0;
let _new_node_id = "node-new";
let _replica_group = 0;
// We'd need to instantiate the worker with a proper mock task store
// This is a placeholder for the actual test

View file

@ -8,7 +8,7 @@ use rand::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::time::Instant;
use tokio::sync::RwLock;
/// Replica selection strategy.
@ -270,7 +270,7 @@ impl ReplicaSelector {
/// Round-robin selection.
async fn select_round_robin(&self, candidates: &[NodeId], group_id: u64) -> Option<NodeId> {
let key = format!("group_{}", group_id);
let key = format!("group_{group_id}");
let mut counter = self.rr_counter.write().await;
let idx = *counter.entry(key.clone()).or_insert(0) as usize % candidates.len();
*counter.get_mut(&key).unwrap() += 1;

View file

@ -10,7 +10,7 @@
pub mod executor;
use crate::mode_b_coordinator::{ModeBOpLeader, PhaseState};
use crate::mode_b_coordinator::ModeBOpLeader;
use crate::router::{assign_shard_in_group, shard_for_key};
use crate::topology::{Group, NodeId};
use serde::{Deserialize, Serialize};
@ -778,7 +778,7 @@ impl ReshardOperation {
/// Create a new resharding operation.
pub fn new(index_uid: String, old_shards: u32, target_shards: u32) -> Self {
let id = format!("reshard-{}-{}", index_uid, uuid::Uuid::new_v4());
let shadow_index = format!("{}__reshard_{}", index_uid, target_shards);
let shadow_index = format!("{index_uid}__reshard_{target_shards}");
let now = millis_now();
Self {
id,
@ -897,8 +897,7 @@ impl ReshardingRegistry {
) -> Result<(), String> {
if self.active_operations.contains_key(&index_uid) {
return Err(format!(
"Resharding already in progress for index '{}'",
index_uid
"Resharding already in progress for index '{index_uid}'"
));
}
tracing::info!(
@ -923,7 +922,7 @@ impl ReshardingRegistry {
let op = self
.active_operations
.get_mut(index_uid)
.ok_or_else(|| format!("No resharding operation for index '{}'", index_uid))?;
.ok_or_else(|| format!("No resharding operation for index '{index_uid}'"))?;
op.phase = new_phase;
tracing::info!(
index_uid = %index_uid,
@ -936,7 +935,7 @@ impl ReshardingRegistry {
/// Remove a completed resharding operation.
pub fn remove(&mut self, index_uid: &str) -> Result<(), String> {
if self.active_operations.remove(index_uid).is_none() {
return Err(format!("No resharding operation for index '{}'", index_uid));
return Err(format!("No resharding operation for index '{index_uid}'"));
}
tracing::info!(
index_uid = %index_uid,
@ -1023,8 +1022,8 @@ impl<E> ReshardCoordinator<E> {
target_shards: u32,
pod_id: String,
) -> Self {
let scope = format!("reshard:{}", index_uid);
let shadow_index = format!("{}__reshard_{}", index_uid, target_shards);
let scope = format!("reshard:{index_uid}");
let shadow_index = format!("{index_uid}__reshard_{target_shards}");
let extra_state = ReshardExtraState {
index_uid,
@ -1222,9 +1221,9 @@ impl ReshardRegistry {
let op = self
.operations
.get(id)
.ok_or_else(|| format!("Operation '{}' not found", id))?;
.ok_or_else(|| format!("Operation '{id}' not found"))?;
if !op.is_terminal() {
return Err(format!("Operation '{}' is not in a terminal state", id));
return Err(format!("Operation '{id}' is not in a terminal state"));
}
self.index_ops.remove(&op.index_uid);
Ok(())
@ -1312,7 +1311,7 @@ pub async fn shadow_create_phase(
master_key: &str,
primary_key: Option<String>,
) -> Result<ShadowCreateResult, ShadowCreateError> {
let shadow_index = format!("{}__reshard_{}", live_index_uid, target_shards);
let shadow_index = format!("{live_index_uid}__reshard_{target_shards}");
tracing::info!(
live_index = %live_index_uid,
@ -1327,7 +1326,7 @@ pub async fn shadow_create_phase(
.build()
.map_err(|e| ShadowCreateError::NodeCreationFailed {
node: "client".to_string(),
error: format!("failed to create HTTP client: {}", e),
error: format!("failed to create HTTP client: {e}"),
})?;
// Step 1: Create shadow index on every node sequentially
@ -1358,8 +1357,7 @@ pub async fn shadow_create_phase(
return Err(match e {
ShadowCreateError::IndexAlreadyExists(_) => e,
other => ShadowCreateError::RollbackRequired(format!(
"creation failed on {}: {}",
address, other
"creation failed on {address}: {other}"
)),
});
}
@ -1387,8 +1385,7 @@ pub async fn shadow_create_phase(
Err(e) => {
rollback_shadow_index(&client, &shadow_index, &created_on, master_key).await;
return Err(ShadowCreateError::SettingsBroadcastFailed(format!(
"failed to fetch live index settings: {}",
e
"failed to fetch live index settings: {e}"
)));
}
};
@ -1419,8 +1416,7 @@ pub async fn shadow_create_phase(
// Settings broadcast failed - rollback shadow index creation
rollback_shadow_index(&client, &shadow_index, &created_on, master_key).await;
return Err(ShadowCreateError::SettingsBroadcastFailed(format!(
"two-phase broadcast failed: {}",
e
"two-phase broadcast failed: {e}"
)));
}
};
@ -1444,13 +1440,13 @@ async fn create_index_on_node(
) -> Result<Option<u64>, ShadowCreateError> {
let response = client
.post(url)
.header("Authorization", format!("Bearer {}", master_key))
.header("Authorization", format!("Bearer {master_key}"))
.json(body)
.send()
.await
.map_err(|e| ShadowCreateError::NodeCreationFailed {
node: address.to_string(),
error: format!("request failed: {}", e),
error: format!("request failed: {e}"),
})?;
let status = response.status();
@ -1459,7 +1455,7 @@ async fn create_index_on_node(
.await
.map_err(|e| ShadowCreateError::NodeCreationFailed {
node: address.to_string(),
error: format!("failed to read response: {}", e),
error: format!("failed to read response: {e}"),
})?;
if status.as_u16() == 409 {
@ -1497,22 +1493,22 @@ async fn fetch_index_settings(
let response = client
.get(&url)
.header("Authorization", format!("Bearer {}", master_key))
.header("Authorization", format!("Bearer {master_key}"))
.send()
.await
.map_err(|e| format!("request failed: {}", e))?;
.map_err(|e| format!("request failed: {e}"))?;
let status = response.status();
let body_text = response
.text()
.await
.map_err(|e| format!("failed to read response: {}", e))?;
.map_err(|e| format!("failed to read response: {e}"))?;
if !status.is_success() {
return Err(format!("HTTP {}: {}", status.as_u16(), body_text));
}
serde_json::from_str(&body_text).map_err(|e| format!("failed to parse settings JSON: {}", e))
serde_json::from_str(&body_text).map_err(|e| format!("failed to parse settings JSON: {e}"))
}
/// Ensure `_miroir_shard` is in filterableAttributes.
@ -1560,7 +1556,7 @@ async fn two_phase_broadcast_settings(
);
let result = client
.patch(&url)
.header("Authorization", format!("Bearer {}", key))
.header("Authorization", format!("Bearer {key}"))
.json(&settings)
.send()
.await;
@ -1578,7 +1574,7 @@ async fn two_phase_broadcast_settings(
let _text = resp.text().await.unwrap_or_default();
Err(format!("{}: HTTP {}", address, status.as_u16()))
}
Err(e) => Err(format!("{}: {}", address, e)),
Err(e) => Err(format!("{address}: {e}")),
}
}
})
@ -1598,7 +1594,7 @@ async fn two_phase_broadcast_settings(
node_task_uids.push((address, 0));
}
Err(e) => {
return Err(format!("Phase 1 propose failed: {}", e));
return Err(format!("Phase 1 propose failed: {e}"));
}
}
}
@ -1619,7 +1615,7 @@ async fn two_phase_broadcast_settings(
);
let result = client
.get(&url)
.header("Authorization", format!("Bearer {}", key))
.header("Authorization", format!("Bearer {key}"))
.send()
.await;
@ -1630,11 +1626,11 @@ async fn two_phase_broadcast_settings(
let hash = crate::settings::fingerprint_settings(&settings);
Ok((address, hash))
} else {
Err(format!("{}: failed to parse settings", address))
Err(format!("{address}: failed to parse settings"))
}
}
Ok(resp) => Err(format!("{}: HTTP {}", address, resp.status().as_u16())),
Err(e) => Err(format!("{}: {}", address, e)),
Err(e) => Err(format!("{address}: {e}")),
}
}
})
@ -1653,14 +1649,13 @@ async fn two_phase_broadcast_settings(
Ok((address, hash)) => {
if hash != expected_fingerprint {
return Err(format!(
"Phase 2 verify failed: hash mismatch on {}",
address
"Phase 2 verify failed: hash mismatch on {address}"
));
}
node_hashes.insert(address, hash);
}
Err(e) => {
return Err(format!("Phase 2 verify failed: {}", e));
return Err(format!("Phase 2 verify failed: {e}"));
}
}
}
@ -1689,7 +1684,7 @@ async fn rollback_shadow_index(
match client
.delete(&url)
.header("Authorization", format!("Bearer {}", master_key))
.header("Authorization", format!("Bearer {master_key}"))
.send()
.await
{
@ -2557,7 +2552,7 @@ pub async fn verify_phase(
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| VerifyPhaseError::NodeFetchFailed(format!("HTTP client: {}", e)))?;
.map_err(|e| VerifyPhaseError::NodeFetchFailed(format!("HTTP client: {e}")))?;
// Use the same node for all scans (first in list) - documents are identical
// across replicas within the same shard due to RF replication
@ -2770,16 +2765,16 @@ async fn scan_shard_to_pk_buckets(
let response = client
.get(&url)
.header("Authorization", format!("Bearer {}", master_key))
.header("Authorization", format!("Bearer {master_key}"))
.send()
.await
.map_err(|e| format!("request failed: {}", e))?;
.map_err(|e| format!("request failed: {e}"))?;
let status = response.status();
let body_text = response
.text()
.await
.map_err(|e| format!("failed to read response: {}", e))?;
.map_err(|e| format!("failed to read response: {e}"))?;
if !status.is_success() {
return Err(format!("HTTP {}: {}", status.as_u16(), body_text));
@ -2787,12 +2782,12 @@ async fn scan_shard_to_pk_buckets(
// Parse response
let docs_json: serde_json::Value =
serde_json::from_str(&body_text).map_err(|e| format!("JSON parse: {}", e))?;
serde_json::from_str(&body_text).map_err(|e| format!("JSON parse: {e}"))?;
let results = docs_json
.get("results")
.and_then(|v| v.as_array())
.ok_or_else(|| format!("missing results array"))?;
.ok_or_else(|| "missing results array".to_string())?;
if results.is_empty() {
break; // No more documents
@ -2803,7 +2798,7 @@ async fn scan_shard_to_pk_buckets(
let pk_value = doc.get(primary_key).or(doc.get("id")).or(doc.get("_id"));
let primary_key = pk_value
.and_then(|v| v.as_str())
.ok_or_else(|| format!("document missing primary key field"))?;
.ok_or_else(|| "document missing primary key field".to_string())?;
// Compute content hash (reuse anti-entropy logic)
let content_hash = compute_content_hash_for_verify(doc)?;
@ -2838,9 +2833,9 @@ fn compute_content_hash_for_verify(document: &serde_json::Value) -> Result<u64,
// Serialize with sorted keys for deterministic output
let canonical_json = if let Some(obj) = canonical.as_object() {
let sorted: BTreeMap<_, _> = obj.iter().collect();
serde_json::to_string(&sorted).map_err(|e| format!("JSON serialize: {}", e))?
serde_json::to_string(&sorted).map_err(|e| format!("JSON serialize: {e}"))?
} else {
serde_json::to_string(&canonical).map_err(|e| format!("JSON serialize: {}", e))?
serde_json::to_string(&canonical).map_err(|e| format!("JSON serialize: {e}"))?
};
// Hash using xxh3
@ -2923,7 +2918,7 @@ pub async fn alias_swap_phase(
// Step 1: Get the current alias state to capture old_target for rollback info
let existing = task_store
.get_alias(alias_name)
.map_err(|e| AliasSwapError::LookupFailed(format!("{}", e)))?
.map_err(|e| AliasSwapError::LookupFailed(format!("{e}")))?
.ok_or_else(|| AliasSwapError::AliasNotFound(alias_name.to_string()))?;
if existing.kind != "single" {
@ -2944,7 +2939,7 @@ pub async fn alias_swap_phase(
// Step 2: Perform the atomic alias flip via task store
let flipped = task_store
.flip_alias(alias_name, new_target_uid, history_retention)
.map_err(|e| AliasSwapError::FlipFailed(format!("{}", e)))?;
.map_err(|e| AliasSwapError::FlipFailed(format!("{e}")))?;
if !flipped {
return Err(AliasSwapError::FlipFailed(
@ -2955,7 +2950,7 @@ pub async fn alias_swap_phase(
// Step 3: Get the updated alias to capture new version
let updated = task_store
.get_alias(alias_name)
.map_err(|e| AliasSwapError::LookupFailed(format!("{}", e)))?
.map_err(|e| AliasSwapError::LookupFailed(format!("{e}")))?
.ok_or_else(|| AliasSwapError::LookupFailed("alias disappeared after flip".to_string()))?;
let flipped_at = SystemTime::now()
@ -3291,7 +3286,7 @@ pub async fn backfill_phase(
batch_size: usize,
progress_callback: Option<BackfillProgressCallback>,
) -> Result<BackfillResult, BackfillError> {
use std::time::{SystemTime, UNIX_EPOCH};
use std::time::SystemTime;
let start_time = SystemTime::now();
tracing::info!(
@ -3306,7 +3301,7 @@ pub async fn backfill_phase(
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| BackfillError::NodeFetchFailed(format!("HTTP client: {}", e)))?;
.map_err(|e| BackfillError::NodeFetchFailed(format!("HTTP client: {e}")))?;
// Use the first node for all operations (documents are identical across replicas)
let target_node = node_addresses
@ -3442,16 +3437,16 @@ async fn backfill_single_shard(
let response = client
.get(&url)
.header("Authorization", format!("Bearer {}", master_key))
.header("Authorization", format!("Bearer {master_key}"))
.send()
.await
.map_err(|e| format!("fetch failed: {}", e))?;
.map_err(|e| format!("fetch failed: {e}"))?;
let status = response.status();
let body_text = response
.text()
.await
.map_err(|e| format!("failed to read response: {}", e))?;
.map_err(|e| format!("failed to read response: {e}"))?;
if !status.is_success() {
return Err(format!("HTTP {}: {}", status.as_u16(), body_text));
@ -3459,12 +3454,12 @@ async fn backfill_single_shard(
// Parse response
let docs_json: serde_json::Value =
serde_json::from_str(&body_text).map_err(|e| format!("JSON parse: {}", e))?;
serde_json::from_str(&body_text).map_err(|e| format!("JSON parse: {e}"))?;
let results = docs_json
.get("results")
.and_then(|v| v.as_array())
.ok_or_else(|| format!("missing results array"))?;
.ok_or_else(|| "missing results array".to_string())?;
if results.is_empty() {
break; // No more documents
@ -3479,7 +3474,7 @@ async fn backfill_single_shard(
.or(doc.get("id"))
.or(doc.get("_id"))
.and_then(|v| v.as_str())
.ok_or_else(|| format!("document missing primary key field: {}", primary_key))?;
.ok_or_else(|| format!("document missing primary key field: {primary_key}"))?;
// Compute new shard assignment for shadow index
let new_shard_id = crate::router::shard_for_key(pk_value, new_shards);
@ -3525,11 +3520,11 @@ async fn write_backfill_batch(
let response = client
.post(&url)
.header("Authorization", format!("Bearer {}", master_key))
.header("Authorization", format!("Bearer {master_key}"))
.json(documents)
.send()
.await
.map_err(|e| format!("request failed: {}", e))?;
.map_err(|e| format!("request failed: {e}"))?;
let status = response.status();
let body_text = response.text().await.unwrap_or_default();
@ -3625,8 +3620,7 @@ pub async fn cleanup_phase(
"retention period not yet reached, skipping cleanup"
);
return Err(CleanupError::CleanupAborted(format!(
"retention period not reached: {} hours remaining",
remaining_hours
"retention period not reached: {remaining_hours} hours remaining"
)));
}
} else {
@ -3643,7 +3637,7 @@ pub async fn cleanup_phase(
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| CleanupError::CleanupAborted(format!("HTTP client: {}", e)))?;
.map_err(|e| CleanupError::CleanupAborted(format!("HTTP client: {e}")))?;
let mut nodes_deleted_from = Vec::new();
let mut errors = Vec::new();
@ -3657,7 +3651,7 @@ pub async fn cleanup_phase(
match client
.delete(&url)
.header("Authorization", format!("Bearer {}", master_key))
.header("Authorization", format!("Bearer {master_key}"))
.send()
.await
{
@ -3869,7 +3863,7 @@ pub struct ReshardOrchestratorResult {
pub async fn execute_reshard(
config: ReshardOrchestratorConfig,
) -> Result<ReshardOrchestratorResult, String> {
use std::time::{SystemTime, UNIX_EPOCH};
use std::time::SystemTime;
let start_time = SystemTime::now();
let old_shards = config.target_shards / 2; // Assume doubling for now
@ -3901,7 +3895,7 @@ pub async fn execute_reshard(
.await
.map_err(|e| {
// Phase 1 already handles rollback internally
format!("Phase 1 shadow create failed: {}", e)
format!("Phase 1 shadow create failed: {e}")
})?;
tracing::info!(
@ -3932,7 +3926,7 @@ pub async fn execute_reshard(
// Rollback: delete shadow index
tracing::error!(error = %e, "Phase 3 backfill failed, rolling back");
let _ = rollback_shadow_orchestrator(&shadow_index, &config);
format!("Phase 3 backfill failed: {}", e)
format!("Phase 3 backfill failed: {e}")
})?;
emit_phase(
@ -3963,7 +3957,7 @@ pub async fn execute_reshard(
// Rollback: delete shadow index
tracing::error!(error = %e, "Phase 4 verify failed, rolling back");
let _ = rollback_shadow_orchestrator(&shadow_index, &config);
format!("Phase 4 verify failed: {}", e)
format!("Phase 4 verify failed: {e}")
})?;
if !verify_result.passed {
@ -3991,7 +3985,7 @@ pub async fn execute_reshard(
config.alias_history_retention,
)
.await
.map_err(|e| format!("Phase 5 alias swap failed: {}", e))?
.map_err(|e| format!("Phase 5 alias swap failed: {e}"))?
} else {
// No task store - skip alias swap (for testing)
tracing::warn!("no task store, skipping alias swap");
@ -4051,7 +4045,7 @@ async fn rollback_shadow_orchestrator(
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| format!("HTTP client: {}", e))?;
.map_err(|e| format!("HTTP client: {e}"))?;
for address in &config.node_addresses {
let url = format!("{}/indexes/{}", address.trim_end_matches('/'), shadow_index);

View file

@ -315,9 +315,7 @@ impl<C: NodeClient> ReshardExecutor<C> {
// Get a healthy node from topology for verification
let topology = self.topology.read().await;
let node = topology
.nodes()
.filter(|n| n.is_healthy())
.next()
.nodes().find(|n| n.is_healthy())
.ok_or_else(|| {
MiroirError::Topology("No healthy nodes available for verification".to_string())
})?;
@ -433,7 +431,7 @@ impl<C: NodeClient> ReshardExecutor<C> {
let history_retention = 10; // Default history retention for rollback
let flipped = task_store
.flip_alias(&state.index_uid, shadow_name, history_retention)
.map_err(|e| MiroirError::AliasSwapFailed(format!("{}", e)))?;
.map_err(|e| MiroirError::AliasSwapFailed(format!("{e}")))?;
if !flipped {
return Err(MiroirError::AliasSwapFailed(format!(

View file

@ -3,7 +3,7 @@
//! Splits reshard backfill work by shard-id ranges.
//! Each chunk can process a range of old shards independently.
use crate::mode_c_coordinator::{JobChunk, JobParams};
use crate::mode_c_coordinator::JobChunk;
/// Chunk specification for a reshard backfill.
#[derive(Debug, Clone)]
@ -102,11 +102,11 @@ pub fn parse_reshard_chunk(chunk: &JobChunk) -> Result<(u32, u32), String> {
let start_shard = chunk
.start
.parse::<u32>()
.map_err(|e| format!("invalid start shard: {}", e))?;
.map_err(|e| format!("invalid start shard: {e}"))?;
let end_shard = chunk
.end
.parse::<u32>()
.map_err(|e| format!("invalid end shard: {}", e))?;
.map_err(|e| format!("invalid end shard: {e}"))?;
Ok((start_shard, end_shard))
}

View file

@ -12,7 +12,7 @@ use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
use tokio::time::{sleep, Duration};
use tokio::time::Duration;
use tracing::{info_span, instrument, Instrument};
/// Scatter plan: the exact shard→node mapping for a search query.

View file

@ -4,7 +4,7 @@
//! Uses leader-only singleton coordination (plan §14.5) to ensure only one pod
//! performs key rotation for a given index at a time.
use crate::error::{MiroirError, Result};
use crate::error::Result;
use crate::leader_election::LeaderElection;
use crate::mode_b_coordinator::ModeBOpLeader;
use crate::task_store::TaskStore;
@ -94,7 +94,7 @@ impl ScopedKeyRotationCoordinator {
drain_target_s: u64,
pod_id: String,
) -> Self {
let scope = format!("search_ui_key_rotation:{}", index_uid);
let scope = format!("search_ui_key_rotation:{index_uid}");
let extra_state = ScopedKeyRotationExtraState {
index_uid,

View file

@ -140,8 +140,7 @@ impl SettingsBroadcast {
if in_flight.contains_key(&index) {
return Err(MiroirError::InvalidState(format!(
"settings broadcast already in flight for index '{}'",
index
"settings broadcast already in flight for index '{index}'"
)));
}
@ -176,7 +175,7 @@ impl SettingsBroadcast {
let mut in_flight = self.in_flight.write().await;
let status = in_flight
.get_mut(index)
.ok_or_else(|| MiroirError::NotFound(format!("index '{}'", index)))?;
.ok_or_else(|| MiroirError::NotFound(format!("index '{index}'")))?;
if status.phase != BroadcastPhase::Propose {
return Err(MiroirError::InvalidState("expected Propose phase".into()));
@ -200,7 +199,7 @@ impl SettingsBroadcast {
let mut in_flight = self.in_flight.write().await;
let status = in_flight
.get_mut(index)
.ok_or_else(|| MiroirError::NotFound(format!("index '{}'", index)))?;
.ok_or_else(|| MiroirError::NotFound(format!("index '{index}'")))?;
if status.phase != BroadcastPhase::Verify {
return Err(MiroirError::InvalidState("expected Verify phase".into()));
@ -212,8 +211,7 @@ impl SettingsBroadcast {
for (node, hash) in &node_hashes {
if hash != expected_fingerprint {
status.error = Some(format!(
"node '{}' hash mismatch: expected {}, got {}",
node, expected_fingerprint, hash
"node '{node}' hash mismatch: expected {expected_fingerprint}, got {hash}"
));
status.verify_ok = false;
return Err(MiroirError::SettingsDivergence);
@ -231,7 +229,7 @@ impl SettingsBroadcast {
let mut in_flight = self.in_flight.write().await;
let status = in_flight
.get_mut(index)
.ok_or_else(|| MiroirError::NotFound(format!("index '{}'", index)))?;
.ok_or_else(|| MiroirError::NotFound(format!("index '{index}'")))?;
if status.phase != BroadcastPhase::Verify {
return Err(MiroirError::InvalidState("expected Verify phase".into()));
@ -271,7 +269,7 @@ impl SettingsBroadcast {
let mut in_flight = self.in_flight.write().await;
in_flight
.remove(index)
.ok_or_else(|| MiroirError::NotFound(format!("index '{}'", index)))?;
.ok_or_else(|| MiroirError::NotFound(format!("index '{index}'")))?;
Ok(())
}
@ -283,7 +281,7 @@ impl SettingsBroadcast {
}
in_flight
.remove(index)
.ok_or_else(|| MiroirError::NotFound(format!("index '{}'", index)))?;
.ok_or_else(|| MiroirError::NotFound(format!("index '{index}'")))?;
Ok(())
}
@ -336,7 +334,7 @@ impl SettingsBroadcastCoordinator {
index_uid: String,
pod_id: String,
) -> Self {
let scope = format!("settings_broadcast:{}", index_uid);
let scope = format!("settings_broadcast:{index_uid}");
let extra_state = SettingsBroadcastExtraState {
index_uid,
@ -396,7 +394,7 @@ impl SettingsBroadcastCoordinator {
/// Should be called after each phase boundary so that a new leader can
/// resume from the last committed phase.
pub async fn advance_phase(&mut self, new_phase: BroadcastPhase) -> Result<()> {
let phase_name = format!("{:?}", new_phase);
let phase_name = format!("{new_phase:?}");
self.leader.persist_phase(phase_name.to_lowercase()).await
}

View file

@ -189,7 +189,7 @@ impl ShadowManager {
// Build request with optional API key
let mut request_builder = self.client.post(&url).json(request_body);
if let Some(key) = &api_key {
request_builder = request_builder.header("Authorization", format!("Bearer {}", key));
request_builder = request_builder.header("Authorization", format!("Bearer {key}"));
}
// Send shadow request with timeout
@ -298,7 +298,7 @@ impl ShadowManager {
use sha2::{Digest, Sha256};
let json = serde_json::to_string(body).unwrap_or_default();
let hash = Sha256::digest(json.as_bytes());
format!("{:x}", hash)
format!("{hash:x}")
}
/// Compute symmetric diff and Kendall tau correlation.

View file

@ -60,7 +60,7 @@ impl<C: NodeClient> NodePoller for ClientNodePoller<C> {
.get_task_status(node_id, address, &req)
.await
.map(|resp| resp.to_node_status())
.map_err(|e| format!("{:?}", e))
.map_err(|e| format!("{e:?}"))
}
}
@ -88,7 +88,7 @@ impl InMemoryTaskRegistry {
let miroir_id = format!("mtask-{}", Uuid::new_v4());
let created_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| MiroirError::Task(format!("clock error: {}", e)))?
.map_err(|e| MiroirError::Task(format!("clock error: {e}")))?
.as_millis() as u64;
let mut tasks = HashMap::new();
@ -144,7 +144,7 @@ impl InMemoryTaskRegistry {
let miroir_id = format!("mtask-{}", Uuid::new_v4());
let created_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| MiroirError::Task(format!("clock error: {}", e)))?
.map_err(|e| MiroirError::Task(format!("clock error: {e}")))?
.as_millis() as u64;
let mut tasks = HashMap::new();
@ -218,7 +218,7 @@ impl InMemoryTaskRegistry {
task.started_at = Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| MiroirError::Task(format!("clock error: {}", e)))?
.map_err(|e| MiroirError::Task(format!("clock error: {e}")))?
.as_millis() as u64,
);
}
@ -231,7 +231,7 @@ impl InMemoryTaskRegistry {
task.finished_at = Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| MiroirError::Task(format!("clock error: {}", e)))?
.map_err(|e| MiroirError::Task(format!("clock error: {e}")))?
.as_millis() as u64,
);
}
@ -276,7 +276,7 @@ impl InMemoryTaskRegistry {
let mut any_failed = false;
let mut all_terminal = true;
for (_node_id, node_task) in &task.node_tasks {
for node_task in task.node_tasks.values() {
match node_task.status {
NodeTaskStatus::Enqueued | NodeTaskStatus::Processing => {
all_terminal = false;
@ -341,7 +341,7 @@ impl InMemoryTaskRegistry {
// Check each node task's status
let mut all_terminal = true;
for (_node_id, node_task) in &task.node_tasks {
for node_task in task.node_tasks.values() {
match node_task.status {
NodeTaskStatus::Enqueued | NodeTaskStatus::Processing => {
all_terminal = false;
@ -356,7 +356,7 @@ impl InMemoryTaskRegistry {
// Simulate completion for testing
let mut tasks = self.tasks.write().await;
if let Some(t) = tasks.get_mut(miroir_id) {
for (_node_id, node_task) in &mut t.node_tasks {
for node_task in t.node_tasks.values_mut() {
if matches!(
node_task.status,
NodeTaskStatus::Enqueued | NodeTaskStatus::Processing
@ -367,7 +367,7 @@ impl InMemoryTaskRegistry {
// Update overall status
let mut all_succeeded = true;
let mut any_failed = false;
for (_node_id, node_task) in &t.node_tasks {
for node_task in t.node_tasks.values() {
match node_task.status {
NodeTaskStatus::Succeeded => {}
NodeTaskStatus::Failed => any_failed = true,
@ -447,7 +447,7 @@ impl InMemoryTaskRegistry {
if let Some(t) = tasks.get_mut(miroir_id) {
let mut all_succeeded = true;
let mut any_failed = false;
for (_node_id, node_task) in &t.node_tasks {
for node_task in t.node_tasks.values() {
match node_task.status {
NodeTaskStatus::Succeeded => {}
NodeTaskStatus::Failed => any_failed = true,
@ -486,7 +486,7 @@ impl InMemoryTaskRegistry {
// For now, use a mock address - in production, this would come from the topology
let address = format!("http://{}", node_id.as_str());
match poller.poll_node_task(&node_id, &address, *task_uid).await {
match poller.poll_node_task(node_id, &address, *task_uid).await {
Ok(status) => {
node_statuses.insert(node_id.clone(), status);
}
@ -549,12 +549,12 @@ impl InMemoryTaskRegistry {
// Apply index_uid filter
if let Some(index_uid) = &filter.index_uid {
result.retain(|t| t.index_uid.as_ref().map_or(false, |uid| uid == index_uid));
result.retain(|t| t.index_uid.as_ref() == Some(index_uid));
}
// Apply task_type filter
if let Some(task_type) = &filter.task_type {
result.retain(|t| t.task_type.as_ref().map_or(false, |ty| ty == task_type));
result.retain(|t| t.task_type.as_ref() == Some(task_type));
}
// Apply offset
@ -651,7 +651,7 @@ impl crate::task::TaskRegistry for InMemoryTaskRegistry {
let registry = self.clone();
tokio::task::block_in_place(|| {
let rt = tokio::runtime::Handle::try_current()
.map_err(|e| MiroirError::Task(format!("runtime error: {}", e)))?;
.map_err(|e| MiroirError::Task(format!("runtime error: {e}")))?;
rt.block_on(async move {
registry
.register_async_with_metadata(node_tasks, index_uid, task_type)
@ -665,7 +665,7 @@ impl crate::task::TaskRegistry for InMemoryTaskRegistry {
let miroir_id = miroir_id.to_string();
tokio::task::block_in_place(|| {
let rt = tokio::runtime::Handle::try_current()
.map_err(|e| MiroirError::Task(format!("runtime error: {}", e)))?;
.map_err(|e| MiroirError::Task(format!("runtime error: {e}")))?;
rt.block_on(async move { Ok(registry.get_async(&miroir_id).await) })
})
}
@ -675,7 +675,7 @@ impl crate::task::TaskRegistry for InMemoryTaskRegistry {
let miroir_id = miroir_id.to_string();
tokio::task::block_in_place(|| {
let rt = tokio::runtime::Handle::try_current()
.map_err(|e| MiroirError::Task(format!("runtime error: {}", e)))?;
.map_err(|e| MiroirError::Task(format!("runtime error: {e}")))?;
rt.block_on(async move {
let mut tasks = registry.tasks.write().await;
if let Some(task) = tasks.get_mut(&miroir_id) {
@ -697,7 +697,7 @@ impl crate::task::TaskRegistry for InMemoryTaskRegistry {
let node_id = node_id.to_string();
tokio::task::block_in_place(|| {
let rt = tokio::runtime::Handle::try_current()
.map_err(|e| MiroirError::Task(format!("runtime error: {}", e)))?;
.map_err(|e| MiroirError::Task(format!("runtime error: {e}")))?;
rt.block_on(async move {
let mut tasks = registry.tasks.write().await;
if let Some(task) = tasks.get_mut(&miroir_id) {
@ -714,7 +714,7 @@ impl crate::task::TaskRegistry for InMemoryTaskRegistry {
let registry = self.clone();
tokio::task::block_in_place(|| {
let rt = tokio::runtime::Handle::try_current()
.map_err(|e| MiroirError::Task(format!("runtime error: {}", e)))?;
.map_err(|e| MiroirError::Task(format!("runtime error: {e}")))?;
rt.block_on(async move { registry.list_async(&filter).await })
})
}
@ -879,7 +879,7 @@ impl TaskRegistryImpl {
let miroir_id = format!("mtask-{}", Uuid::new_v4());
let created_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| MiroirError::Task(format!("clock error: {}", e)))?
.map_err(|e| MiroirError::Task(format!("clock error: {e}")))?
.as_millis() as i64;
let new_task = crate::task_store::NewTask {
@ -927,7 +927,7 @@ impl TaskRegistryImpl {
let miroir_id = format!("mtask-{}", Uuid::new_v4());
let created_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| MiroirError::Task(format!("clock error: {}", e)))?
.map_err(|e| MiroirError::Task(format!("clock error: {e}")))?
.as_millis() as i64;
let new_task = crate::task_store::NewTask {

View file

@ -248,7 +248,7 @@ impl TaskStore for RedisTaskStore {
fn migrate(&self) -> Result<()> {
let manager = self.pool.manager.clone();
let key_prefix = self.key_prefix.clone();
let version_key = format!("{}:schema_version", key_prefix);
let version_key = format!("{key_prefix}:schema_version");
self.block_on(async move {
let mut conn = manager.lock().await;
let current: Option<i64> = conn
@ -288,7 +288,7 @@ impl TaskStore for RedisTaskStore {
let key_prefix = self.key_prefix.clone();
let task = task.clone();
let key = format!("{}:tasks:{}", key_prefix, task.miroir_id);
let index_key = format!("{}:tasks:_index", key_prefix);
let index_key = format!("{key_prefix}:tasks:_index");
let created_at_str = task.created_at.to_string();
self.block_on(async move {
@ -443,7 +443,7 @@ impl TaskStore for RedisTaskStore {
let mut tasks = Vec::new();
for miroir_id in all_ids {
let key = format!("{}:tasks:{}", key_prefix, miroir_id);
let key = format!("{key_prefix}:tasks:{miroir_id}");
let fields: HashMap<String, Value> = conn
.hgetall(&key)
.await
@ -511,7 +511,7 @@ impl TaskStore for RedisTaskStore {
let mut to_delete = Vec::new();
for miroir_id in all_ids.into_iter().take(batch_size as usize) {
let key = format!("{}:tasks:{}", key_prefix, miroir_id);
let key = format!("{key_prefix}:tasks:{miroir_id}");
// Use a pipeline to get both fields atomically
let mut p = pipe();
@ -536,7 +536,7 @@ impl TaskStore for RedisTaskStore {
// Delete tasks and remove from index
let mut pipe = pipe();
for miroir_id in &to_delete {
let key = format!("{}:tasks:{}", key_prefix, miroir_id);
let key = format!("{key_prefix}:tasks:{miroir_id}");
pipe.del(&key);
pipe.srem(&index_key, miroir_id);
}
@ -559,7 +559,7 @@ impl TaskStore for RedisTaskStore {
self.block_on(async move {
let mut conn = manager.lock().await;
let index_key = format!("{}:tasks:_index", key_prefix);
let index_key = format!("{key_prefix}:tasks:_index");
let all_ids: Vec<String> = conn
.smembers(&index_key)
.await
@ -573,7 +573,7 @@ impl TaskStore for RedisTaskStore {
if added >= limit {
break;
}
let key = format!("{}:tasks:{}", key_prefix, miroir_id);
let key = format!("{key_prefix}:tasks:{miroir_id}");
// Get created_at and status
let mut p = pipe();
@ -654,13 +654,13 @@ impl TaskStore for RedisTaskStore {
fn delete_tasks_batch(&self, miroir_ids: &[&str]) -> Result<usize> {
let pool = self.pool.clone();
let key_prefix = self.key_prefix.clone();
let index_key = format!("{}:tasks:_index", key_prefix);
let index_key = format!("{key_prefix}:tasks:_index");
let ids: Vec<String> = miroir_ids.iter().map(|s| s.to_string()).collect();
self.block_on(async move {
let mut pipe = pipe();
for miroir_id in &ids {
let key = format!("{}:tasks:{}", key_prefix, miroir_id);
let key = format!("{key_prefix}:tasks:{miroir_id}");
pipe.del(&key);
pipe.srem(&index_key, miroir_id);
}
@ -698,15 +698,14 @@ impl TaskStore for RedisTaskStore {
let index_uid = index_uid.to_string();
let node_id = node_id.to_string();
let key = format!(
"{}:node_settings_version:{}:{}",
key_prefix, index_uid, node_id
"{key_prefix}:node_settings_version:{index_uid}:{node_id}"
);
let index_key = format!("{}:node_settings_version:_index", key_prefix);
let index_key = format!("{key_prefix}:node_settings_version:_index");
self.block_on(async move {
let version_str = version.to_string();
let updated_at_str = updated_at.to_string();
let index_value = format!("{}:{}", index_uid, node_id);
let index_value = format!("{index_uid}:{node_id}");
let mut pipe = pipe();
pipe.hset_multiple(
@ -734,8 +733,7 @@ impl TaskStore for RedisTaskStore {
let index_uid = index_uid.to_string();
let node_id = node_id.to_string();
let key = format!(
"{}:node_settings_version:{}:{}",
key_prefix, index_uid, node_id
"{key_prefix}:node_settings_version:{index_uid}:{node_id}"
);
self.block_on(async move {
@ -768,7 +766,7 @@ impl TaskStore for RedisTaskStore {
let target_uids_json = alias
.target_uids
.as_ref()
.map(|uids| serde_json::to_string(uids))
.map(serde_json::to_string)
.transpose()?
.unwrap_or_default();
let history_json = serde_json::to_string(&alias.history)?;
@ -776,8 +774,8 @@ impl TaskStore for RedisTaskStore {
let created_at_str = alias.created_at.to_string();
let current_uid = alias.current_uid.clone();
let has_target_uids = alias.target_uids.is_some();
let key = format!("{}:aliases:{}", key_prefix, name);
let index_key = format!("{}:aliases:_index", key_prefix);
let key = format!("{key_prefix}:aliases:{name}");
let index_key = format!("{key_prefix}:aliases:_index");
self.block_on(async move {
let mut pipe = pipe();
@ -807,7 +805,7 @@ impl TaskStore for RedisTaskStore {
let manager = self.pool.manager.clone();
let key_prefix = self.key_prefix.clone();
let name = name.to_string();
let key = format!("{}:aliases:{}", key_prefix, name);
let key = format!("{key_prefix}:aliases:{name}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -850,7 +848,7 @@ impl TaskStore for RedisTaskStore {
let key_prefix = self.key_prefix.clone();
let name = name.to_string();
let new_uid = new_uid.to_string();
let key = format!("{}:aliases:{}", key_prefix, name);
let key = format!("{key_prefix}:aliases:{name}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -897,8 +895,8 @@ impl TaskStore for RedisTaskStore {
let pool = self.pool.clone();
let key_prefix = self.key_prefix.clone();
let name = name.to_string();
let key = format!("{}:aliases:{}", key_prefix, name);
let index_key = format!("{}:aliases:_index", key_prefix);
let key = format!("{key_prefix}:aliases:{name}");
let index_key = format!("{key_prefix}:aliases:_index");
self.block_on(async move {
let mut conn = pool.manager.lock().await;
@ -924,7 +922,7 @@ impl TaskStore for RedisTaskStore {
fn list_aliases(&self) -> Result<Vec<AliasRow>> {
let pool = self.pool.clone();
let key_prefix = self.key_prefix.clone();
let index_key = format!("{}:aliases:_index", key_prefix);
let index_key = format!("{key_prefix}:aliases:_index");
self.block_on(async move {
let mut conn = pool.manager.lock().await;
@ -937,7 +935,7 @@ impl TaskStore for RedisTaskStore {
let mut result = Vec::new();
for name in names {
let key = format!("{}:aliases:{}", key_prefix, name);
let key = format!("{key_prefix}:aliases:{name}");
let fields: HashMap<String, Value> = conn
.hgetall(&key)
.await
@ -991,7 +989,7 @@ impl TaskStore for RedisTaskStore {
let manager = self.pool.manager.clone();
let key_prefix = self.key_prefix.clone();
let session_id = session_id.to_string();
let key = format!("{}:session:{}", key_prefix, session_id);
let key = format!("{key_prefix}:session:{session_id}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -1052,7 +1050,7 @@ impl TaskStore for RedisTaskStore {
let manager = self.pool.manager.clone();
let key_prefix = self.key_prefix.clone();
let key = key.to_string();
let redis_key = format!("{}:idemp:{}", key_prefix, key);
let redis_key = format!("{key_prefix}:idemp:{key}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -1090,8 +1088,8 @@ impl TaskStore for RedisTaskStore {
let key_prefix = self.key_prefix.clone();
let job = job.clone();
let key = format!("{}:jobs:{}", key_prefix, job.id);
let queued_key = format!("{}:jobs:_queued", key_prefix);
let index_key = format!("{}:jobs:_index", key_prefix);
let queued_key = format!("{key_prefix}:jobs:_queued");
let index_key = format!("{key_prefix}:jobs:_index");
self.block_on(async move {
let mut pipe = pipe();
@ -1139,7 +1137,7 @@ impl TaskStore for RedisTaskStore {
let manager = self.pool.manager.clone();
let key_prefix = self.key_prefix.clone();
let id = id.to_string();
let key = format!("{}:jobs:{}", key_prefix, id);
let key = format!("{key_prefix}:jobs:{id}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -1173,8 +1171,8 @@ impl TaskStore for RedisTaskStore {
let key_prefix = self.key_prefix.clone();
let id = id.to_string();
let claimed_by = claimed_by.to_string();
let key = format!("{}:jobs:{}", key_prefix, id);
let queued_key = format!("{}:jobs:_queued", key_prefix);
let key = format!("{key_prefix}:jobs:{id}");
let queued_key = format!("{key_prefix}:jobs:_queued");
self.block_on(async move {
let mut conn = pool.manager.lock().await;
@ -1206,7 +1204,7 @@ impl TaskStore for RedisTaskStore {
let id = id.to_string();
let state = state.to_string();
let progress = progress.to_string();
let key = format!("{}:jobs:{}", key_prefix, id);
let key = format!("{key_prefix}:jobs:{id}");
self.block_on(async move {
let mut conn = pool.manager.lock().await;
@ -1232,7 +1230,7 @@ impl TaskStore for RedisTaskStore {
let manager = self.pool.manager.clone();
let key_prefix = self.key_prefix.clone();
let id = id.to_string();
let key = format!("{}:jobs:{}", key_prefix, id);
let key = format!("{key_prefix}:jobs:{id}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -1264,14 +1262,14 @@ impl TaskStore for RedisTaskStore {
let mut conn = manager.lock().await;
// Use the _index set for O(cardinality) iteration (no SCAN).
let index_key = format!("{}:jobs:_index", key_prefix);
let index_key = format!("{key_prefix}:jobs:_index");
let ids: Vec<String> = conn
.smembers(&index_key)
.await
.map_err(|e| MiroirError::Redis(e.to_string()))?;
for id in ids {
let key = format!("{}:jobs:{}", key_prefix, id);
let key = format!("{key_prefix}:jobs:{id}");
let fields: HashMap<String, Value> = conn
.hgetall(&key)
.await
@ -1313,7 +1311,7 @@ impl TaskStore for RedisTaskStore {
// For queued state, use the _queued set for O(1) count
// This is used for HPA queue depth metric per plan §14.4
if state == "queued" {
let queued_key = format!("{}:jobs:_queued", key_prefix);
let queued_key = format!("{key_prefix}:jobs:_queued");
let count: u64 = conn
.scard(&queued_key)
.await
@ -1324,7 +1322,7 @@ impl TaskStore for RedisTaskStore {
// For other states, iterate through _index and count by state
// This is O(n) but acceptable for non-queued states which are
// typically few (only actively running jobs)
let index_key = format!("{}:jobs:_index", key_prefix);
let index_key = format!("{key_prefix}:jobs:_index");
let ids: Vec<String> = conn
.smembers(&index_key)
.await
@ -1332,7 +1330,7 @@ impl TaskStore for RedisTaskStore {
let mut count = 0u64;
for id in ids {
let key = format!("{}:jobs:{}", key_prefix, id);
let key = format!("{key_prefix}:jobs:{id}");
let job_state: Option<String> = conn
.hget(&key, "state")
.await
@ -1355,14 +1353,14 @@ impl TaskStore for RedisTaskStore {
let mut conn = manager.lock().await;
// Use the _index set for O(cardinality) iteration
let index_key = format!("{}:jobs:_index", key_prefix);
let index_key = format!("{key_prefix}:jobs:_index");
let ids: Vec<String> = conn
.smembers(&index_key)
.await
.map_err(|e| MiroirError::Redis(e.to_string()))?;
for id in ids {
let key = format!("{}:jobs:{}", key_prefix, id);
let key = format!("{key_prefix}:jobs:{id}");
let fields: HashMap<String, Value> = conn
.hgetall(&key)
.await
@ -1411,14 +1409,14 @@ impl TaskStore for RedisTaskStore {
let mut conn = manager.lock().await;
// Use the _index set for iteration
let index_key = format!("{}:jobs:_index", key_prefix);
let index_key = format!("{key_prefix}:jobs:_index");
let ids: Vec<String> = conn
.smembers(&index_key)
.await
.map_err(|e| MiroirError::Redis(e.to_string()))?;
for id in ids {
let key = format!("{}:jobs:{}", key_prefix, id);
let key = format!("{key_prefix}:jobs:{id}");
let fields: HashMap<String, Value> = conn
.hgetall(&key)
.await
@ -1454,11 +1452,11 @@ impl TaskStore for RedisTaskStore {
let id = id.to_string();
let state = state.to_string();
let progress = progress.to_string();
let key = format!("{}:jobs:{}", key_prefix, id);
let queued_key = format!("{}:jobs:_queued", key_prefix);
let key = format!("{key_prefix}:jobs:{id}");
let queued_key = format!("{key_prefix}:jobs:_queued");
self.block_on(async move {
let mut conn = pool.manager.lock().await;
let conn = pool.manager.lock().await;
let mut pipe = pipe();
pipe.hset(&key, "state", &state);
@ -1485,7 +1483,7 @@ impl TaskStore for RedisTaskStore {
let key_prefix = self.key_prefix.clone();
let scope = scope.to_string();
let holder = holder.to_string();
let key = format!("{}:lease:{}", key_prefix, scope);
let key = format!("{key_prefix}:lease:{scope}");
let ttl_seconds = ((expires_at - now_ms) / 1000).max(1) as u64;
self.block_on(async move {
@ -1564,7 +1562,7 @@ impl TaskStore for RedisTaskStore {
let key_prefix = self.key_prefix.clone();
let scope = scope.to_string();
let holder = holder.to_string();
let key = format!("{}:lease:{}", key_prefix, scope);
let key = format!("{key_prefix}:lease:{scope}");
let ttl_seconds = ((expires_at - now_ms()) / 1000).max(1) as u64;
self.block_on(async move {
@ -1587,7 +1585,7 @@ impl TaskStore for RedisTaskStore {
let manager = self.pool.manager.clone();
let key_prefix = self.key_prefix.clone();
let scope = scope.to_string();
let key = format!("{}:lease:{}", key_prefix, scope);
let key = format!("{key_prefix}:lease:{scope}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -1633,7 +1631,7 @@ impl TaskStore for RedisTaskStore {
let key_prefix = self.key_prefix.clone();
let canary = canary.clone();
let key = format!("{}:canary:{}", key_prefix, canary.id);
let index_key = format!("{}:canary:_index", key_prefix);
let index_key = format!("{key_prefix}:canary:_index");
let interval_s_str = canary.interval_s.to_string();
let enabled_str = (canary.enabled as i64).to_string();
@ -1664,7 +1662,7 @@ impl TaskStore for RedisTaskStore {
let manager = self.pool.manager.clone();
let key_prefix = self.key_prefix.clone();
let id = id.to_string();
let key = format!("{}:canary:{}", key_prefix, id);
let key = format!("{key_prefix}:canary:{id}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -1686,7 +1684,7 @@ impl TaskStore for RedisTaskStore {
let key_prefix = self.key_prefix.clone();
self.block_on(async move {
let index_key = format!("{}:canary:_index", key_prefix);
let index_key = format!("{key_prefix}:canary:_index");
let mut conn = manager.lock().await;
let ids: Vec<String> = conn
.smembers(&index_key)
@ -1695,7 +1693,7 @@ impl TaskStore for RedisTaskStore {
let mut result = Vec::new();
for id in ids {
let key = format!("{}:canary:{}", key_prefix, id);
let key = format!("{key_prefix}:canary:{id}");
let fields: HashMap<String, Value> = conn
.hgetall(&key)
.await
@ -1714,8 +1712,8 @@ impl TaskStore for RedisTaskStore {
let pool = self.pool.clone();
let key_prefix = self.key_prefix.clone();
let id = id.to_string();
let key = format!("{}:canary:{}", key_prefix, id);
let index_key = format!("{}:canary:_index", key_prefix);
let key = format!("{key_prefix}:canary:{id}");
let index_key = format!("{key_prefix}:canary:_index");
self.block_on(async move {
let mut conn = pool.manager.lock().await;
@ -1772,7 +1770,7 @@ impl TaskStore for RedisTaskStore {
let manager = self.pool.manager.clone();
let key_prefix = self.key_prefix.clone();
let canary_id = canary_id.to_string();
let key = format!("{}:canary_runs:{}", key_prefix, canary_id);
let key = format!("{key_prefix}:canary_runs:{canary_id}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -1835,7 +1833,7 @@ impl TaskStore for RedisTaskStore {
let key_prefix = self.key_prefix.clone();
let sink_name = sink_name.to_string();
let index_uid = index_uid.to_string();
let key = format!("{}:cdc_cursor:{}:{}", key_prefix, sink_name, index_uid);
let key = format!("{key_prefix}:cdc_cursor:{sink_name}:{index_uid}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -1861,7 +1859,7 @@ impl TaskStore for RedisTaskStore {
let pool = self.pool.clone();
let key_prefix = self.key_prefix.clone();
let sink_name = sink_name.to_string();
let index_key = format!("{}:cdc_cursor:_index:{}", key_prefix, sink_name);
let index_key = format!("{key_prefix}:cdc_cursor:_index:{sink_name}");
self.block_on(async move {
// Use the _index set for O(cardinality) iteration (no SCAN).
@ -1882,7 +1880,7 @@ impl TaskStore for RedisTaskStore {
Some(idx) => idx.to_string(),
None => continue,
};
let key = format!("{}:cdc_cursor:{}:{}", key_prefix, sink_name, idx);
let key = format!("{key_prefix}:cdc_cursor:{sink_name}:{idx}");
let fields: HashMap<String, Value> = conn
.hgetall(&key)
.await
@ -1911,7 +1909,7 @@ impl TaskStore for RedisTaskStore {
let tenant_id = mapping.tenant_id.clone();
let group_id = mapping.group_id;
let hex_hash = hex::encode(&api_key_hash);
let key = format!("{}:tenant_map:{}", key_prefix, hex_hash);
let key = format!("{key_prefix}:tenant_map:{hex_hash}");
self.block_on(async move {
let mut pipe = pipe();
@ -1930,7 +1928,7 @@ impl TaskStore for RedisTaskStore {
let key_prefix = self.key_prefix.clone();
let api_key_hash = api_key_hash.to_vec();
let hex_hash = hex::encode(&api_key_hash);
let key = format!("{}:tenant_map:{}", key_prefix, hex_hash);
let key = format!("{key_prefix}:tenant_map:{hex_hash}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -1956,7 +1954,7 @@ impl TaskStore for RedisTaskStore {
let key_prefix = self.key_prefix.clone();
let api_key_hash = api_key_hash.to_vec();
let hex_hash = hex::encode(&api_key_hash);
let key = format!("{}:tenant_map:{}", key_prefix, hex_hash);
let key = format!("{key_prefix}:tenant_map:{hex_hash}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -1986,7 +1984,7 @@ impl TaskStore for RedisTaskStore {
let key_prefix = self.key_prefix.clone();
let policy = policy.clone();
let key = format!("{}:rollover:{}", key_prefix, policy.name);
let index_key = format!("{}:rollover:_index", key_prefix);
let index_key = format!("{key_prefix}:rollover:_index");
let enabled_str = (policy.enabled as i64).to_string();
self.block_on(async move {
@ -2014,7 +2012,7 @@ impl TaskStore for RedisTaskStore {
let manager = self.pool.manager.clone();
let key_prefix = self.key_prefix.clone();
let name = name.to_string();
let key = format!("{}:rollover:{}", key_prefix, name);
let key = format!("{key_prefix}:rollover:{name}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -2045,7 +2043,7 @@ impl TaskStore for RedisTaskStore {
let key_prefix = self.key_prefix.clone();
self.block_on(async move {
let index_key = format!("{}:rollover:_index", key_prefix);
let index_key = format!("{key_prefix}:rollover:_index");
let mut conn = manager.lock().await;
let names: Vec<String> = conn
.smembers(&index_key)
@ -2054,7 +2052,7 @@ impl TaskStore for RedisTaskStore {
let mut result = Vec::new();
for name in names {
let key = format!("{}:rollover:{}", key_prefix, name);
let key = format!("{key_prefix}:rollover:{name}");
let fields: HashMap<String, Value> = conn
.hgetall(&key)
.await
@ -2082,8 +2080,8 @@ impl TaskStore for RedisTaskStore {
let pool = self.pool.clone();
let key_prefix = self.key_prefix.clone();
let name = name.to_string();
let key = format!("{}:rollover:{}", key_prefix, name);
let index_key = format!("{}:rollover:_index", key_prefix);
let key = format!("{key_prefix}:rollover:{name}");
let index_key = format!("{key_prefix}:rollover:_index");
self.block_on(async move {
let mut conn = pool.manager.lock().await;
@ -2131,7 +2129,7 @@ impl TaskStore for RedisTaskStore {
let manager = self.pool.manager.clone();
let key_prefix = self.key_prefix.clone();
let index_uid = index_uid.to_string();
let key = format!("{}:search_ui_config:{}", key_prefix, index_uid);
let key = format!("{key_prefix}:search_ui_config:{index_uid}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -2156,7 +2154,7 @@ impl TaskStore for RedisTaskStore {
let manager = self.pool.manager.clone();
let key_prefix = self.key_prefix.clone();
let index_uid = index_uid.to_string();
let key = format!("{}:search_ui_config:{}", key_prefix, index_uid);
let key = format!("{key_prefix}:search_ui_config:{index_uid}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -2225,7 +2223,7 @@ impl TaskStore for RedisTaskStore {
let manager = self.pool.manager.clone();
let key_prefix = self.key_prefix.clone();
let session_id = session_id.to_string();
let key = format!("{}:admin_session:{}", key_prefix, session_id);
let key = format!("{key_prefix}:admin_session:{session_id}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -2255,8 +2253,8 @@ impl TaskStore for RedisTaskStore {
let manager = self.pool.manager.clone();
let key_prefix = self.key_prefix.clone();
let session_id = session_id.to_string();
let key = format!("{}:admin_session:{}", key_prefix, session_id);
let channel = format!("{}:admin_session:revoked", key_prefix);
let key = format!("{key_prefix}:admin_session:{session_id}");
let channel = format!("{key_prefix}:admin_session:revoked");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -2337,13 +2335,13 @@ impl TaskStore for RedisTaskStore {
}
// Store the hash
conn.hset_multiple(&key, &items)
conn.hset_multiple::<_, _, _, ()>(&key, &items)
.await
.map_err(|e| MiroirError::Redis(e.to_string()))?;
// Add to scope index
let scope_key = format!("{}:mode_b_ops_scope:{}", key_prefix, op.scope);
conn.set(&scope_key, &op.operation_id)
conn.set::<_, _, ()>(&scope_key, &op.operation_id)
.await
.map_err(|e| MiroirError::Redis(e.to_string()))?;
@ -2358,7 +2356,7 @@ impl TaskStore for RedisTaskStore {
self.block_on(async move {
let mut conn = manager.lock().await;
let key = format!("{}:mode_b_ops:{}", key_prefix, id);
let key = format!("{key_prefix}:mode_b_ops:{id}");
// Check if key exists
let exists: bool = conn
@ -2412,7 +2410,7 @@ impl TaskStore for RedisTaskStore {
self.block_on(async move {
let mut conn = manager.lock().await;
let scope_key = format!("{}:mode_b_ops_scope:{}", key_prefix, scope);
let scope_key = format!("{key_prefix}:mode_b_ops_scope:{scope}");
// Get operation ID from scope index
let operation_id: Option<String> = conn
@ -2425,7 +2423,7 @@ impl TaskStore for RedisTaskStore {
};
// Get the operation
let key = format!("{}:mode_b_ops:{}", key_prefix, id);
let key = format!("{key_prefix}:mode_b_ops:{id}");
let exists: bool = conn
.exists(&key)
.await
@ -2479,7 +2477,7 @@ impl TaskStore for RedisTaskStore {
let mut conn = manager.lock().await;
// Scan for mode_b_ops keys
let pattern = format!("{}:mode_b_ops:*", key_prefix);
let pattern = format!("{key_prefix}:mode_b_ops:*");
let keys: Vec<String> = conn
.keys(&pattern)
.await
@ -2569,7 +2567,7 @@ impl TaskStore for RedisTaskStore {
self.block_on(async move {
let mut conn = manager.lock().await;
let key = format!("{}:mode_b_ops:{}", key_prefix, id);
let key = format!("{key_prefix}:mode_b_ops:{id}");
// Get scope for cleanup
let scope: Option<String> = conn
@ -2586,7 +2584,7 @@ impl TaskStore for RedisTaskStore {
// Clean up scope index
if let Some(s) = scope {
let scope_key = format!("{}:mode_b_ops_scope:{}", key_prefix, s);
let scope_key = format!("{key_prefix}:mode_b_ops_scope:{s}");
let _: () = conn
.del(&scope_key)
.await
@ -2605,7 +2603,7 @@ impl TaskStore for RedisTaskStore {
let mut conn = manager.lock().await;
// Scan for mode_b_ops keys
let pattern = format!("{}:mode_b_ops:*", key_prefix);
let pattern = format!("{key_prefix}:mode_b_ops:*");
let keys: Vec<String> = conn
.keys(&pattern)
.await
@ -2650,7 +2648,7 @@ impl TaskStore for RedisTaskStore {
let key_prefix = self.key_prefix.clone();
let index_uid = index_uid.to_string();
let event_id = event_id.to_string();
let key = format!("{}:search_ui_beacon:{}", key_prefix, index_uid);
let key = format!("{key_prefix}:search_ui_beacon:{index_uid}");
let field = event_id.clone();
self.block_on(async move {
@ -2703,7 +2701,7 @@ impl RedisTaskStore {
let manager = self.pool.manager.clone();
let key_prefix = self.key_prefix.clone();
let ip = ip.to_string();
let key = format!("{}:ratelimit:searchui:{}", key_prefix, ip);
let key = format!("{key_prefix}:ratelimit:searchui:{ip}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -2758,8 +2756,8 @@ impl RedisTaskStore {
let pool = self.pool.clone();
let key_prefix = self.key_prefix.clone();
let ip = ip.to_string();
let backoff_key = format!("{}:ratelimit:adminlogin:backoff:{}", key_prefix, ip);
let key = format!("{}:ratelimit:adminlogin:{}", key_prefix, ip);
let backoff_key = format!("{key_prefix}:ratelimit:adminlogin:backoff:{ip}");
let key = format!("{key_prefix}:ratelimit:adminlogin:{ip}");
self.block_on(async move {
let mut conn = pool.manager.lock().await;
@ -2819,7 +2817,7 @@ impl RedisTaskStore {
let pool = self.pool.clone();
let key_prefix = self.key_prefix.clone();
let ip = ip.to_string();
let backoff_key = format!("{}:ratelimit:adminlogin:backoff:{}", key_prefix, ip);
let backoff_key = format!("{key_prefix}:ratelimit:adminlogin:backoff:{ip}");
self.block_on(async move {
let mut conn = pool.manager.lock().await;
@ -2872,8 +2870,8 @@ impl RedisTaskStore {
let pool = self.pool.clone();
let key_prefix = self.key_prefix.clone();
let ip = ip.to_string();
let key = format!("{}:ratelimit:adminlogin:{}", key_prefix, ip);
let backoff_key = format!("{}:ratelimit:adminlogin:backoff:{}", key_prefix, ip);
let key = format!("{key_prefix}:ratelimit:adminlogin:{ip}");
let backoff_key = format!("{key_prefix}:ratelimit:adminlogin:backoff:{ip}");
self.block_on(async move {
let mut pipe = pipe();
@ -2898,7 +2896,7 @@ impl RedisTaskStore {
let pool = self.pool.clone();
let key_prefix = self.key_prefix.clone();
let ip = ip.to_string();
let key = format!("{}:ratelimit:searchui:{}", key_prefix, ip);
let key = format!("{key_prefix}:ratelimit:searchui:{ip}");
self.block_on(async move {
let mut conn = pool.manager.lock().await;
@ -2933,7 +2931,7 @@ impl RedisTaskStore {
let manager = self.pool.manager.clone();
let key_prefix = self.key_prefix.clone();
let index_uid = index_uid.to_string();
let key = format!("{}:search_ui_scoped_key:{}", key_prefix, index_uid);
let key = format!("{key_prefix}:search_ui_scoped_key:{index_uid}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -3008,8 +3006,7 @@ impl RedisTaskStore {
let pod_id = pod_id.to_string();
let index_uid = index_uid.to_string();
let key = format!(
"{}:search_ui_scoped_key_observed:{}:{}",
key_prefix, pod_id, index_uid
"{key_prefix}:search_ui_scoped_key_observed:{pod_id}:{index_uid}"
);
self.block_on(async move {
@ -3041,8 +3038,7 @@ impl RedisTaskStore {
for pod_id in &live_pods {
let key = format!(
"{}:search_ui_scoped_key_observed:{}:{}",
key_prefix, pod_id, index_uid
"{key_prefix}:search_ui_scoped_key_observed:{pod_id}:{index_uid}"
);
let fields: HashMap<String, Value> = conn
.hgetall(&key)
@ -3068,7 +3064,7 @@ impl RedisTaskStore {
let pool = self.pool.clone();
let key_prefix = self.key_prefix.clone();
let index_uid = index_uid.to_string();
let redis_key = format!("{}:search_ui_scoped_key:{}", key_prefix, index_uid);
let redis_key = format!("{key_prefix}:search_ui_scoped_key:{index_uid}");
self.block_on(async move {
let mut pipe = pipe();
@ -3085,7 +3081,7 @@ impl RedisTaskStore {
let pool = self.pool.clone();
let key_prefix = self.key_prefix.clone();
let pod_id = pod_id.to_string();
let key = format!("{}:live_pods", key_prefix);
let key = format!("{key_prefix}:live_pods");
let now = now_ms();
self.block_on(async move {
@ -3103,7 +3099,7 @@ impl RedisTaskStore {
pub fn get_live_pods(&self) -> Result<Vec<String>> {
let manager = self.pool.manager.clone();
let key_prefix = self.key_prefix.clone();
let key = format!("{}:live_pods", key_prefix);
let key = format!("{key_prefix}:live_pods");
let cutoff = now_ms() - 120_000; // 120 seconds ago
self.block_on(async move {
@ -3122,7 +3118,7 @@ impl RedisTaskStore {
let key_prefix = self.key_prefix.clone();
self.block_on(async move {
let pattern = format!("{}:search_ui_scoped_key:*", key_prefix);
let pattern = format!("{key_prefix}:search_ui_scoped_key:*");
let mut conn = pool.manager.lock().await;
let mut indexes = Vec::new();
@ -3170,8 +3166,8 @@ impl RedisTaskStore {
let key_prefix = self.key_prefix.clone();
let sink_name = sink_name.to_string();
let data = data.to_vec();
let key = format!("{}:cdc:overflow:{}", key_prefix, sink_name);
let bytes_key = format!("{}:cdc:overflow_bytes:{}", key_prefix, sink_name);
let key = format!("{key_prefix}:cdc:overflow:{sink_name}");
let bytes_key = format!("{key_prefix}:cdc:overflow_bytes:{sink_name}");
let data_len = data.len();
self.block_on(async move {
@ -3246,8 +3242,8 @@ impl RedisTaskStore {
let pool = self.pool.clone();
let key_prefix = self.key_prefix.clone();
let sink_name = sink_name.to_string();
let key = format!("{}:cdc:overflow:{}", key_prefix, sink_name);
let bytes_key = format!("{}:cdc:overflow_bytes:{}", key_prefix, sink_name);
let key = format!("{key_prefix}:cdc:overflow:{sink_name}");
let bytes_key = format!("{key_prefix}:cdc:overflow_bytes:{sink_name}");
self.block_on(async move {
let mut conn = pool.manager.lock().await;
@ -3275,7 +3271,7 @@ impl RedisTaskStore {
let manager = self.pool.manager.clone();
let key_prefix = self.key_prefix.clone();
let sink_name = sink_name.to_string();
let key = format!("{}:cdc:overflow:{}", key_prefix, sink_name);
let key = format!("{key_prefix}:cdc:overflow:{sink_name}");
self.block_on(async move {
let mut conn = manager.lock().await;
@ -3304,7 +3300,7 @@ impl RedisTaskStore {
.await
.map_err(|e| MiroirError::Redis(e.to_string()))?;
let channel = format!("{}:admin_session:revoked", key_prefix);
let channel = format!("{key_prefix}:admin_session:revoked");
conn.subscribe(&channel)
.await
.map_err(|e| MiroirError::Redis(e.to_string()))?;

View file

@ -9,7 +9,7 @@ use std::sync::Mutex;
fn registry() -> &'static MigrationRegistry {
use std::sync::OnceLock;
static REGISTRY: OnceLock<MigrationRegistry> = OnceLock::new();
REGISTRY.get_or_init(|| build_registry())
REGISTRY.get_or_init(build_registry)
}
pub struct SqliteTaskStore {
@ -304,7 +304,7 @@ impl TaskStore for SqliteTaskStore {
let target_uids_json = alias
.target_uids
.as_ref()
.map(|uids| serde_json::to_string(uids))
.map(serde_json::to_string)
.transpose()?;
let history_json = serde_json::to_string(&alias.history)?;
conn.execute(
@ -796,7 +796,7 @@ impl TaskStore for SqliteTaskStore {
let mut total_deleted = 0;
for miroir_id in miroir_ids {
let delete_sql = "DELETE FROM tasks WHERE miroir_id = ?1";
let rows = conn.execute(delete_sql, [&*miroir_id])?;
let rows = conn.execute(delete_sql, [miroir_id])?;
total_deleted += rows;
}
Ok(total_deleted)
@ -1394,10 +1394,10 @@ impl TaskStore for SqliteTaskStore {
query.push_str(" ORDER BY updated_at DESC");
if let Some(limit) = filter.limit {
query.push_str(&format!(" LIMIT {}", limit));
query.push_str(&format!(" LIMIT {limit}"));
}
if let Some(offset) = filter.offset {
query.push_str(&format!(" OFFSET {}", offset));
query.push_str(&format!(" OFFSET {offset}"));
}
let mut stmt = conn.prepare(&query)?;

View file

@ -121,8 +121,7 @@ impl NodeStatus {
Ok(target)
} else {
Err(MiroirError::Topology(format!(
"illegal state transition: {:?} → {:?}",
self, target
"illegal state transition: {self:?} → {target:?}"
)))
}
}

View file

@ -14,7 +14,7 @@
//! WriteRequest { ..., origin: Some(ORIGIN_TTL_EXPIRE.to_string()) }
//! ```
use crate::error::{MiroirError, Result};
use crate::error::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;

View file

@ -207,7 +207,7 @@ impl VectorMerger {
// First, sort each shard's hits by their original ranking score
let mut per_shard: HashMap<u32, Vec<VectorHit>> = HashMap::new();
for (shard_id, hit) in shard_hits {
per_shard.entry(shard_id).or_insert_with(Vec::new).push(hit);
per_shard.entry(shard_id).or_default().push(hit);
}
// Compute RRF scores

View file

@ -388,7 +388,7 @@ fn test_global_idf_single_shard() {
},
};
let global_idf = GlobalIdf::from_preflight_responses(&vec![response]);
let global_idf = GlobalIdf::from_preflight_responses(&[response]);
assert_eq!(global_idf.total_docs, 1000);
assert_eq!(global_idf.terms.get("test").unwrap().df, 50);

View file

@ -29,8 +29,7 @@ fn print_actual_hash_values() {
let hash = hash_for_key(key);
let shard = shard_for_key(key, shard_count);
println!(
"(\"{}\", {}, {}), // hash={}",
key, shard_count, shard, hash
"(\"{key}\", {shard_count}, {shard}), // hash={hash}"
);
}
println!("========================\n");

View file

@ -54,7 +54,7 @@ proptest! {
.map(|i| {
let id = shard_id * hits_per_shard + i;
let score = (hits_per_shard as f64 - i as f64) / hits_per_shard as f64;
make_hit(&format!("doc-{}", id), score)
make_hit(&format!("doc-{id}"), score)
})
.collect();
make_shard_response(hits, hits_per_shard as u64, 15)
@ -94,7 +94,7 @@ proptest! {
.map(|i| {
let id = shard_id * hits_per_shard + i;
let score = (hits_per_shard as f64 - i as f64) / hits_per_shard as f64;
make_hit(&format!("doc-{}", id), score)
make_hit(&format!("doc-{id}"), score)
})
.collect();
make_shard_response(hits, hits_per_shard as u64, 15)
@ -135,7 +135,7 @@ proptest! {
.map(|i| {
let id = shard_id * hits_per_shard + i;
let score = (hits_per_shard as f64 - i as f64) / hits_per_shard as f64;
make_hit(&format!("doc-{}", id), score)
make_hit(&format!("doc-{id}"), score)
})
.collect();
make_shard_response(hits, hits_per_shard as u64, 15)
@ -179,7 +179,7 @@ proptest! {
.map(|i| {
let id = shard_id * hits_per_shard + i;
let score = (hits_per_shard as f64 - i as f64) / hits_per_shard as f64;
make_hit(&format!("doc-{}", id), score)
make_hit(&format!("doc-{id}"), score)
})
.collect();
make_shard_response(hits, hits_per_shard as u64, 15)
@ -232,7 +232,7 @@ proptest! {
.map(|i| {
let id = shard_id * hits_per_shard + i;
let score = (hits_per_shard as f64 - i as f64) / hits_per_shard as f64;
make_hit(&format!("doc-{}", id), score)
make_hit(&format!("doc-{id}"), score)
})
.collect();
make_shard_response(hits, hits_per_shard as u64, 15)
@ -300,7 +300,7 @@ proptest! {
.map(|i| {
let id = shard_id * hits_per_shard + i;
let score = (hits_per_shard as f64 - i as f64) / hits_per_shard as f64;
make_hit(&format!("doc-{}", id), score)
make_hit(&format!("doc-{id}"), score)
})
.collect();
make_shard_response(hits, hits_per_shard as u64, 15)
@ -358,7 +358,7 @@ proptest! {
.map(|i| {
let id = shard_id * hits_per_shard + i;
let score = (hits_per_shard as f64 - i as f64) / hits_per_shard as f64;
make_hit(&format!("doc-{}", id), score)
make_hit(&format!("doc-{id}"), score)
})
.collect();
make_shard_response(hits, hits_per_shard as u64, 15)
@ -469,7 +469,7 @@ proptest! {
.map(|i| {
let id = shard_id * hits_per_shard + i;
let score = (hits_per_shard as f64 - i as f64) / hits_per_shard as f64;
make_hit(&format!("doc-{}", id), score)
make_hit(&format!("doc-{id}"), score)
})
.collect();
make_shard_response(hits, hits_per_shard as u64, 15)
@ -517,7 +517,7 @@ proptest! {
let id = shard_id * hits_per_shard + i;
// Use varying scores - RRF should sort by rank, not score
let score = rand::random::<f64>() * 0.5 + 0.5;
make_hit(&format!("doc-{}", id), score)
make_hit(&format!("doc-{id}"), score)
})
.collect();
make_shard_response(hits, hits_per_shard as u64, 15)

View file

@ -138,8 +138,7 @@ async fn p5_10_a3_hot_query_coalesces_scatters() {
// At least 90% should have coalesced (they all hit within the window)
assert!(
coalesced_count >= 900,
"expected at least 900 coalesced queries, got {}",
coalesced_count
"expected at least 900 coalesced queries, got {coalesced_count}"
);
}
@ -159,7 +158,7 @@ async fn p5_10_a3_multiple_scatters_across_windows() {
// First query in window: should miss and register
let rx = coalescer.try_coalesce(fp.clone()).await;
assert!(rx.is_none(), "first query in window {} should miss", window);
assert!(rx.is_none(), "first query in window {window} should miss");
let tx = coalescer.register(fp.clone()).await.unwrap();
scatter_count += 1;
@ -169,8 +168,7 @@ async fn p5_10_a3_multiple_scatters_across_windows() {
let rx = coalescer.try_coalesce(fp.clone()).await;
assert!(
rx.is_some(),
"subsequent queries in window {} should coalesce",
window
"subsequent queries in window {window} should coalesce"
);
}
@ -185,8 +183,7 @@ async fn p5_10_a3_multiple_scatters_across_windows() {
// We expect exactly 5 scatters (one per window)
assert_eq!(
scatter_count, 5,
"expected 5 scatters across 5 windows, got {}",
scatter_count
"expected 5 scatters across 5 windows, got {scatter_count}"
);
}
@ -304,10 +301,10 @@ async fn p5_10_a5_idempotency_cache_max_entries_enforcement() {
// Insert 3 entries (at capacity)
for i in 0..3 {
let key = format!("key-{}", i);
let key = format!("key-{i}");
let body = json!({"id": i});
let body_hash = compute_hash(&body);
cache.insert(key, body_hash, format!("mtask-{}", i)).await;
cache.insert(key, body_hash, format!("mtask-{i}")).await;
}
assert_eq!(cache.size().await, 3, "cache should have 3 entries");

View file

@ -8,7 +8,7 @@
//! - Canary CRUD operations
use miroir_core::{
canary::{CanaryAssertion, CanaryStatus, QueryCapture, SearchQuery, SearchResponse},
canary::{CanaryAssertion, QueryCapture, SearchQuery, SearchResponse},
task_store::{NewCanary, TaskStore},
};
use std::collections::HashMap;
@ -146,8 +146,7 @@ async fn ac3_assertion_failure_includes_actual_value() {
assert_eq!(failure["actual"], 2);
// Test multiple assertion types
let failures = vec![
serde_json::json!({
let failures = [serde_json::json!({
"assertion_type": "top_hit_id",
"expected": "product-123",
"actual": "product-456",
@ -158,8 +157,7 @@ async fn ac3_assertion_failure_includes_actual_value() {
"expected": 200,
"actual": 350,
"message": "Latency exceeded threshold"
}),
];
})];
assert_eq!(failures.len(), 2);
assert_eq!(failures[0]["assertion_type"], "top_hit_id");
@ -185,7 +183,7 @@ async fn ac4_capture_flow_records_queries() {
hits: vec![],
estimated_total_hits: 0,
processing_time_ms: 50,
query: format!("query {}", i),
query: format!("query {i}"),
},
)
.await;
@ -199,7 +197,7 @@ async fn ac4_capture_flow_records_queries() {
for (i, query) in captured.iter().enumerate() {
assert_eq!(query.index_uid, "products");
let q = query.query.params.get("q").and_then(|v| v.as_str());
assert_eq!(q, Some(format!("query {}", i).as_str()));
assert_eq!(q, Some(format!("query {i}").as_str()));
}
// Clear and verify
@ -362,8 +360,8 @@ async fn ac8_canary_list_can_be_retrieved() {
// Create multiple canaries
for i in 0..3 {
let canary = NewCanary {
id: format!("list-test-{}", i),
name: format!("List Test Canary {}", i),
id: format!("list-test-{i}"),
name: format!("List Test Canary {i}"),
index_uid: "products".to_string(),
interval_s: 60,
query_json: serde_json::to_string(&SearchQuery {

View file

@ -11,7 +11,6 @@ use std::sync::Arc;
use std::time::Duration;
use miroir_core::hedging::{HedgeOutcome, HedgingConfig, HedgingManager};
use miroir_core::router::assign_shard_in_group;
use miroir_core::scatter::{
execute_hedged_request, NodeClient, NodeError, SearchRequest, VectorMode,
};
@ -59,6 +58,7 @@ fn make_search_request() -> SearchRequest {
}
/// Mock node client that can simulate delays per node.
#[derive(Default)]
struct DelayedMockNodeClient {
/// Responses keyed by node ID.
responses: HashMap<NodeId, serde_json::Value>,
@ -66,14 +66,6 @@ struct DelayedMockNodeClient {
delays: HashMap<NodeId, Duration>,
}
impl Default for DelayedMockNodeClient {
fn default() -> Self {
Self {
responses: HashMap::new(),
delays: HashMap::new(),
}
}
}
impl NodeClient for DelayedMockNodeClient {
async fn search_node(
@ -171,8 +163,7 @@ async fn p5_2_a1_chaos_slow_node_avoided_via_hedging() {
// Hedge should have fired and won
assert!(
outcome == Some(HedgeOutcome::HedgeWon),
"Hedge should fire and win: got {:?}",
outcome
"Hedge should fire and win: got {outcome:?}"
);
// Total latency should be MUCH closer to fast replica (5ms + hedge overhead)
@ -184,8 +175,7 @@ async fn p5_2_a1_chaos_slow_node_avoided_via_hedging() {
// Definitely NOT 500ms.
assert!(
total_latency < Duration::from_millis(100),
"Total latency {:?} should be far less than slow node's 500ms (hedging should avoid it)",
total_latency
"Total latency {total_latency:?} should be far less than slow node's 500ms (hedging should avoid it)"
);
// Verify we got a response from a fast replica (not the slow one)
@ -194,8 +184,7 @@ async fn p5_2_a1_chaos_slow_node_avoided_via_hedging() {
let doc_id = hits[0]["id"].as_str().unwrap();
assert!(
doc_id == "fast-1" || doc_id == "fast-2",
"Response should come from a fast replica, got {}",
doc_id
"Response should come from a fast replica, got {doc_id}"
);
// Hedge count should be 1
@ -274,9 +263,9 @@ async fn p5_2_a2_hedging_p95_close_to_healthy_baseline() {
let hedged_p95 = percentile(&degraded_with_hedge_latencies, 95);
let no_hedge_p95 = percentile(&degraded_no_hedge_latencies, 95);
println!("Healthy p95: {:?}", healthy_p95);
println!("Hedged p95: {:?}", hedged_p95);
println!("No-hedge p95: {:?}", no_hedge_p95);
println!("Healthy p95: {healthy_p95:?}");
println!("Hedged p95: {hedged_p95:?}");
println!("No-hedge p95: {no_hedge_p95:?}");
// With hedging, p95 should be close to healthy baseline
// Without hedging, p95 would be degraded by the slow node
@ -285,25 +274,20 @@ async fn p5_2_a2_hedging_p95_close_to_healthy_baseline() {
// but hedging should definitely be better than no hedging
assert!(
hedged_p95 < no_hedge_p95,
"Hedged p95 {:?} should be better than no-hedge p95 {:?}",
hedged_p95,
no_hedge_p95
"Hedged p95 {hedged_p95:?} should be better than no-hedge p95 {no_hedge_p95:?}"
);
// Hedged p95 should not be dramatically worse than healthy baseline
// (allowing 5× for test overhead with 15ms hedge trigger)
assert!(
hedged_p95 < healthy_p95 * 5,
"Hedged p95 {:?} should be within 5× of healthy p95 {:?}",
hedged_p95,
healthy_p95
"Hedged p95 {hedged_p95:?} should be within 5× of healthy p95 {healthy_p95:?}"
);
// Without hedging, p95 would be severely degraded (close to 500ms)
assert!(
no_hedge_p95 > Duration::from_millis(200),
"No-hedge p95 {:?} should be severely degraded by slow node",
no_hedge_p95
"No-hedge p95 {no_hedge_p95:?} should be severely degraded by slow node"
);
}
@ -456,8 +440,7 @@ async fn run_searches_with_latency(
}
println!(
"Total hedges issued: {} out of {} queries",
total_hedges, count
"Total hedges issued: {total_hedges} out of {count} queries"
);
latencies

View file

@ -79,18 +79,15 @@ async fn p5_3_a1_degraded_node_receives_less_traffic() {
let _expected = 200 / 3;
assert!(
(20..=90).contains(&count0),
"node-0 baseline count {} out of expected range 20-90",
count0
"node-0 baseline count {count0} out of expected range 20-90"
);
assert!(
(20..=90).contains(&count1),
"node-1 baseline count {} out of expected range 20-90",
count1
"node-1 baseline count {count1} out of expected range 20-90"
);
assert!(
(20..=90).contains(&count2),
"node-2 baseline count {} out of expected range 20-90",
count2
"node-2 baseline count {count2} out of expected range 20-90"
);
// Induce degradation on node-1: 200ms latency
@ -113,20 +110,17 @@ async fn p5_3_a1_degraded_node_receives_less_traffic() {
// Expect node-1 to get <15% of traffic
assert!(
degraded_count1 < 30,
"degraded node-1 still receiving too much traffic: {}",
degraded_count1
"degraded node-1 still receiving too much traffic: {degraded_count1}"
);
// Healthy nodes should receive more traffic
assert!(
degraded_count0 > 50,
"healthy node-0 not receiving enough traffic: {}",
degraded_count0
"healthy node-0 not receiving enough traffic: {degraded_count0}"
);
assert!(
degraded_count2 > 50,
"healthy node-2 not receiving enough traffic: {}",
degraded_count2
"healthy node-2 not receiving enough traffic: {degraded_count2}"
);
}
@ -161,8 +155,7 @@ async fn p5_3_a2_degraded_node_recovers() {
let degraded_count1 = *degraded_dist.get("node-1").unwrap_or(&0);
assert!(
degraded_count1 < 20,
"node-1 should be degraded, got {} selections",
degraded_count1
"node-1 should be degraded, got {degraded_count1} selections"
);
// Clear latency: record good responses for node-1
@ -186,24 +179,15 @@ async fn p5_3_a2_degraded_node_recovers() {
assert!(
(recovered_count1 as isize - expected as isize).abs() <= tolerance as isize,
"node-1 recovered count {} not close to expected {} (tolerance {})",
recovered_count1,
expected,
tolerance
"node-1 recovered count {recovered_count1} not close to expected {expected} (tolerance {tolerance})"
);
assert!(
(recovered_count0 as isize - expected as isize).abs() <= tolerance as isize,
"node-0 count {} not close to expected {} (tolerance {})",
recovered_count0,
expected,
tolerance
"node-0 count {recovered_count0} not close to expected {expected} (tolerance {tolerance})"
);
assert!(
(recovered_count2 as isize - expected as isize).abs() <= tolerance as isize,
"node-2 count {} not close to expected {} (tolerance {})",
recovered_count2,
expected,
tolerance
"node-2 count {recovered_count2} not close to expected {expected} (tolerance {tolerance})"
);
}
@ -246,8 +230,7 @@ async fn p5_3_a3_exploration_samples_degraded_node() {
let count2 = *dist.get("node-2").unwrap_or(&0);
println!(
"Distribution: node-0={}, node-1={}, node-2={}",
count0, count1, count2
"Distribution: node-0={count0}, node-1={count1}, node-2={count2}"
);
// Node-2 is severely degraded but should still get some traffic via exploration
@ -257,16 +240,14 @@ async fn p5_3_a3_exploration_samples_degraded_node() {
// Allow range 5-30 for statistical variance (3 sigma)
assert!(
(5..=30).contains(&count2),
"exploration not working: degraded node-2 got {} selections, expected ~17 (range 5-30)",
count2
"exploration not working: degraded node-2 got {count2} selections, expected ~17 (range 5-30)"
);
// Healthy nodes should split the remaining ~95%
let healthy_total = count0 + count1;
assert!(
healthy_total >= 900,
"healthy nodes didn't get enough traffic: {}",
healthy_total
"healthy nodes didn't get enough traffic: {healthy_total}"
);
// Each healthy node should get roughly half of remaining
@ -274,15 +255,11 @@ async fn p5_3_a3_exploration_samples_degraded_node() {
let tolerance = 100;
assert!(
(count0 as isize - expected_healthy).abs() <= tolerance,
"node-0 count {} not close to expected {}",
count0,
expected_healthy
"node-0 count {count0} not close to expected {expected_healthy}"
);
assert!(
(count1 as isize - expected_healthy).abs() <= tolerance,
"node-1 count {} not close to expected {}",
count1,
expected_healthy
"node-1 count {count1} not close to expected {expected_healthy}"
);
}
@ -308,10 +285,10 @@ async fn p5_3_a4_round_robin_fallback() {
let fourth = selector.select(&candidates, 0).await;
// Should cycle through candidates in order
assert_eq!(first, candidates.get(0).cloned());
assert_eq!(first, candidates.first().cloned());
assert_eq!(second, candidates.get(1).cloned());
assert_eq!(third, candidates.get(2).cloned());
assert_eq!(fourth, candidates.get(0).cloned()); // Wrap around
assert_eq!(fourth, candidates.first().cloned()); // Wrap around
}
// ─────────────────────────────────────────────────────────────
@ -376,9 +353,7 @@ async fn in_flight_count_affects_score() {
assert!(
score0 > score1,
"node-0 with in-flight requests should have higher score: {} > {}",
score0,
score1
"node-0 with in-flight requests should have higher score: {score0} > {score1}"
);
}
@ -412,9 +387,7 @@ async fn error_rate_affects_score() {
assert!(
score0 > score1,
"node-0 with errors should have higher score: {} > {}",
score0,
score1
"node-0 with errors should have higher score: {score0} > {score1}"
);
// Verify error_rate is set
@ -479,10 +452,7 @@ async fn test_random_strategy() {
let diff = (*count as isize - expected as isize).abs();
assert!(
diff <= 100, // Allow 10% variance
"{}: got {} selections, expected ~{}",
node,
count,
expected
"{node}: got {count} selections, expected ~{expected}"
);
}
}

View file

@ -246,7 +246,7 @@ async fn p13_4_a10_result_parity_narrowed_vs_full_fanout() {
for pk in test_pks {
let shard = shard_for_key(pk, 64);
let plan = planner
.plan("products", &Some(format!("product_id = \"{}\"", pk)), 64)
.plan("products", &Some(format!("product_id = \"{pk}\"")), 64)
.await;
assert!(plan.narrowed, "Plan should be narrowed for each PK");

View file

@ -132,7 +132,7 @@ async fn flip_alias_history_retention() {
// Perform 12 flips with history_retention=10
for i in 1..=12 {
let new_target = format!("products_v{}", i);
let new_target = format!("products_v{i}");
store.flip_alias("products", &new_target, 10).unwrap();
}
@ -146,7 +146,7 @@ async fn flip_alias_history_retention() {
// Verify history contains the most recent 10 targets
// After 12 flips from v0, we should have v2..v11 (10 entries)
// v1 was evicted
let expected: Vec<String> = (2..=11).map(|i| format!("products_v{}", i)).collect();
let expected: Vec<String> = (2..=11).map(|i| format!("products_v{i}")).collect();
let actual: Vec<String> = alias.history.iter().map(|h| h.uid.clone()).collect();
assert_eq!(actual, expected);
}
@ -371,9 +371,9 @@ async fn list_aliases() {
// Create multiple aliases
for i in 1..=3 {
let new_alias = NewAlias {
name: format!("alias{}", i),
name: format!("alias{i}"),
kind: "single".to_string(),
current_uid: Some(format!("target_v{}", i)),
current_uid: Some(format!("target_v{i}")),
target_uids: None,
version: 1,
created_at: 1000 + (i as i64),
@ -463,9 +463,9 @@ async fn registry_sync_from_store() {
// Create aliases directly in the store
for i in 1..=3 {
let new_alias = NewAlias {
name: format!("sync{}", i),
name: format!("sync{i}"),
kind: "single".to_string(),
current_uid: Some(format!("target_v{}", i)),
current_uid: Some(format!("target_v{i}")),
target_uids: None,
version: 1,
created_at: 1000 + (i as i64),

View file

@ -13,8 +13,8 @@ fn make_test_topology() -> Topology {
let mut topo = Topology::new(64, 2, 2);
for i in 0u32..3 {
let mut node = Node::new(
NodeId::new(format!("node-{}", i)),
format!("http://node-{}:7700", i),
NodeId::new(format!("node-{i}")),
format!("http://node-{i}:7700"),
i % 2,
);
node.status = NodeStatus::Active;

View file

@ -41,7 +41,7 @@ fn test_document_distribution_uniformity() {
// Simulate 1000 documents and track which shard each goes to
let mut shard_counts: std::collections::HashMap<u32, usize> = std::collections::HashMap::new();
for i in 0..1000 {
let key = format!("doc:{}", i);
let key = format!("doc:{i}");
let shard_id = shard_for_key(&key, shard_count);
*shard_counts.entry(shard_id).or_insert(0) += 1;
}
@ -53,11 +53,10 @@ fn test_document_distribution_uniformity() {
let max_docs_per_node = 1000 * 26 / 64; // ~406 docs
// Check that no shard has unreasonable count
for (_shard, count) in &shard_counts {
for count in shard_counts.values() {
assert!(
*count >= 5 && *count <= 30,
"Shard has unusual count: {}",
count
"Shard has unusual count: {count}"
);
}
}
@ -270,11 +269,10 @@ fn test_shard_distribution_rf1() {
assert_eq!(node_shard_counts.len(), 3, "All 3 nodes should have shards");
// With 64 shards and 3 nodes, each should have ~21 shards (17-26 range per plan §8)
for (_node, count) in &node_shard_counts {
for count in node_shard_counts.values() {
assert!(
(17..=26).contains(count),
"Node has {} shards, expected 17-26",
count
"Node has {count} shards, expected 17-26"
);
}
}

View file

@ -103,8 +103,8 @@ fn test_1000_docs_indexed_retrievable_by_id() {
// Verify the original fields are present
assert_eq!(stored_doc["id"], doc_id);
assert_eq!(stored_doc["title"], format!("Document {}", i));
assert_eq!(stored_doc["content"], format!("Content for document {}", i));
assert_eq!(stored_doc["title"], format!("Document {i}"));
assert_eq!(stored_doc["content"], format!("Content for document {i}"));
// Verify _miroir_shard was injected
assert!(
@ -150,26 +150,19 @@ fn test_docs_distribute_uniformly_across_nodes() {
for (node, count) in &node_shard_counts {
assert!(
(*count as f64) >= (shard_count as f64 * 0.15),
"node {} has {} shards, expected at least 15% of {}",
node,
count,
shard_count
"node {node} has {count} shards, expected at least 15% of {shard_count}"
);
assert!(
(*count as f64) <= (shard_count as f64 * 0.50),
"node {} has {} shards, expected at most 50% of {}",
node,
count,
shard_count
"node {node} has {count} shards, expected at most 50% of {shard_count}"
);
}
// Verify the exact 17-26 range from plan §8
for (_node, count) in &node_shard_counts {
for count in node_shard_counts.values() {
assert!(
(17..=26).contains(count),
"node has {} shards, expected 17-26",
count
"node has {count} shards, expected 17-26"
);
}
}
@ -375,16 +368,16 @@ async fn test_delete_by_ids_array_produces_independent_per_shard_calls() {
// Find IDs that route to different shards
let s1 = 0u32;
let mut s2 = 1u32;
while shard_for_key(&format!("doc-{}", s1), shard_count)
== shard_for_key(&format!("doc-{}", s2), shard_count)
while shard_for_key(&format!("doc-{s1}"), shard_count)
== shard_for_key(&format!("doc-{s2}"), shard_count)
{
s2 += 1;
}
(
format!("doc-{}", s1),
format!("doc-{}", s2),
shard_for_key(&format!("doc-{}", s1), shard_count),
shard_for_key(&format!("doc-{}", s2), shard_count),
format!("doc-{s1}"),
format!("doc-{s2}"),
shard_for_key(&format!("doc-{s1}"), shard_count),
shard_for_key(&format!("doc-{s2}"), shard_count),
)
} else {
(doc_a.to_string(), doc_b.to_string(), shard_a, shard_b)
@ -424,7 +417,7 @@ async fn test_delete_by_ids_array_produces_independent_per_shard_calls() {
// Each shard should have exactly one ID
for (shard_id, id_list) in &shard_id_map {
assert_eq!(id_list.len(), 1, "shard {} should have 1 ID", shard_id);
assert_eq!(id_list.len(), 1, "shard {shard_id} should have 1 ID");
}
}

View file

@ -230,8 +230,8 @@ async fn test_paging_no_dupes_or_gaps() {
for i in 0..3 {
// Only 3 nodes to ensure simple routing
topo.add_node(Node::new(
NodeId::new(format!("node-{}", i)),
format!("http://node-{}:7700", i),
NodeId::new(format!("node-{i}")),
format!("http://node-{i}:7700"),
0,
));
}
@ -254,7 +254,7 @@ async fn test_paging_no_dupes_or_gaps() {
}
client.responses.insert(
NodeId::new(format!("node-{}", i)),
NodeId::new(format!("node-{i}")),
json!({
"hits": hits,
"estimatedTotalHits": 17,
@ -295,7 +295,7 @@ async fn test_paging_no_dupes_or_gaps() {
.await
.unwrap();
assert_eq!(result.hits.len(), 10, "Page {} should have 10 hits", page);
assert_eq!(result.hits.len(), 10, "Page {page} should have 10 hits");
for hit in &result.hits {
let id = hit.get("id").unwrap().as_str().unwrap().to_string();
all_ids.push(id);
@ -313,8 +313,8 @@ async fn test_paging_no_dupes_or_gaps() {
// Verify all docs from doc-000 to doc-049 are present
for i in 0..50 {
let expected = format!("doc-{:03}", i);
assert!(all_ids.contains(&expected), "Missing document {}", expected);
let expected = format!("doc-{i:03}");
assert!(all_ids.contains(&expected), "Missing document {expected}");
}
}
@ -664,9 +664,7 @@ async fn test_search_read_path_integration() {
.unwrap();
assert!(
score_i >= score_j,
"Hits should be sorted by score descending: {} >= {}",
score_i,
score_j
"Hits should be sorted by score descending: {score_i} >= {score_j}"
);
}
}

View file

@ -23,23 +23,19 @@ fn test_all_miroir_error_codes_have_correct_shape() {
// Verify all required fields exist
assert!(
json_val.get("message").is_some(),
"message field missing for {:?}",
code
"message field missing for {code:?}"
);
assert!(
json_val.get("code").is_some(),
"code field missing for {:?}",
code
"code field missing for {code:?}"
);
assert!(
json_val.get("type").is_some(),
"type field missing for {:?}",
code
"type field missing for {code:?}"
);
assert!(
json_val.get("link").is_some(),
"link field missing for {:?}",
code
"link field missing for {code:?}"
);
// Verify field types
@ -60,9 +56,7 @@ fn test_error_code_strings_have_miroir_prefix() {
let code_str = code.as_str();
assert!(
code_str.starts_with("miroir_"),
"Error code {:?} ({}) does not start with 'miroir_'",
code,
code_str
"Error code {code:?} ({code_str}) does not start with 'miroir_'"
);
}
}
@ -167,7 +161,7 @@ fn test_error_json_matches_meilisearch_shape() {
/// Test 6: Error with custom metadata preserves shape.
#[test]
fn test_error_with_custom_metadata_preserves_shape() {
let mut err = MeilisearchError::new(
let err = MeilisearchError::new(
MiroirCode::ReservedField,
"document contains reserved field `_miroir_shard`",
);
@ -217,7 +211,7 @@ fn test_reserved_field_error_includes_field_name() {
let field_name = "_miroir_internal";
let err = MeilisearchError::new(
MiroirCode::ReservedField,
&format!("document contains reserved field `{}`", field_name),
format!("document contains reserved field `{field_name}`"),
);
let json_val = serde_json::to_value(&err).expect("failed to serialize");
@ -263,15 +257,11 @@ fn test_error_link_format_is_consistent() {
let link = code.doc_link();
assert!(
link.starts_with("https://github.com/jedarden/miroir/blob/main/docs/errors.md#"),
"Error code {:?} has unexpected link format: {}",
code,
link
"Error code {code:?} has unexpected link format: {link}"
);
assert!(
link.ends_with(code.as_str()),
"Error code {:?} link doesn't end with code: {}",
code,
link
"Error code {code:?} link doesn't end with code: {link}"
);
}
}

View file

@ -397,7 +397,7 @@ fn test_task_count_survives_restart() {
{
let store = open_store(path).unwrap();
for i in 0..10 {
let task = new_test_task(&format!("mtask-count-{}", i));
let task = new_test_task(&format!("mtask-count-{i}"));
store.insert_task(&task).unwrap();
}
}

View file

@ -82,7 +82,7 @@ proptest! {
let mut inserted_tasks = Vec::new();
for i in 0..count {
let miroir_id = format!("task-{}", i);
let miroir_id = format!("task-{i}");
let mut task = new_test_task(miroir_id.clone());
task.created_at = 1714500000000 + (i as i64 * 1000);

View file

@ -13,8 +13,8 @@ use tokio::sync::RwLock;
use miroir_core::{
config::UnavailableShardPolicy,
migration::{MigrationConfig, MigrationCoordinator, NodeId as MigrationNodeId, ShardId},
rebalancer::{HttpMigrationExecutor, MigrationExecutor, Rebalancer, RebalancerConfig},
migration::MigrationConfig,
rebalancer::{MigrationExecutor, Rebalancer, RebalancerConfig},
router::assign_shard_in_group,
scatter::execute_scatter,
scatter::{MockNodeClient, SearchRequest},
@ -65,7 +65,7 @@ impl DrainTestExecutor {
});
stored
.entry((node.to_string(), shard_id))
.or_insert_with(Vec::new)
.or_default()
.push(doc);
}
}
@ -119,7 +119,7 @@ impl MigrationExecutor for DrainTestExecutor {
let mut stored = self.stored_docs.lock().unwrap();
let docs = stored
.entry((target_node.to_string(), shard_id as u32))
.or_insert_with(Vec::new);
.or_default();
// Deduplicate by document ID
if let Some(doc_id) = doc.get("id").and_then(|v| v.as_str()) {
@ -172,7 +172,7 @@ async fn p43_drain_node_searches_still_succeed_zero_degraded() {
let rf = 2;
// Create 3-node topology with RF=2
let mut topo = create_test_topology(shards, 3, rf);
let topo = create_test_topology(shards, 3, rf);
let executor = Arc::new(DrainTestExecutor::default());
@ -202,7 +202,7 @@ async fn p43_drain_node_searches_still_succeed_zero_degraded() {
anti_entropy_enabled: false,
};
let mut rebalancer = Rebalancer::new(config, topo_arc.clone(), migration_config)
let rebalancer = Rebalancer::new(config, topo_arc.clone(), migration_config)
.with_migration_executor(executor.clone());
// Start drain operation
@ -211,7 +211,7 @@ async fn p43_drain_node_searches_still_succeed_zero_degraded() {
};
let result = rebalancer.drain_node(request).await;
assert!(result.is_ok(), "Drain should succeed: {:?}", result);
assert!(result.is_ok(), "Drain should succeed: {result:?}");
// Wait for drain to complete
let mut attempts = 0;
@ -290,7 +290,7 @@ async fn p43_verify_drain_returns_zero_for_all_shards() {
let docs_per_shard = 50;
let rf = 2;
let mut topo = create_test_topology(shards, 3, rf);
let topo = create_test_topology(shards, 3, rf);
let executor = Arc::new(DrainTestExecutor::default());
// Populate node-1 with documents for shards it's actually assigned to hold
@ -309,7 +309,7 @@ async fn p43_verify_drain_returns_zero_for_all_shards() {
let config = RebalancerConfig::default();
let migration_config = MigrationConfig::default();
let mut rebalancer = Rebalancer::new(config, topo_arc.clone(), migration_config)
let rebalancer = Rebalancer::new(config, topo_arc.clone(), migration_config)
.with_migration_executor(executor.clone());
let request = miroir_core::rebalancer::DrainNodeRequest {
@ -348,8 +348,7 @@ async fn p43_verify_drain_returns_zero_for_all_shards() {
let count = executor.get_stored_doc_count("node-1", shard_id);
assert_eq!(
count, 0,
"Shard {} should have 0 documents after drain, got {}",
shard_id, count
"Shard {shard_id} should have 0 documents after drain, got {count}"
);
}
}
@ -369,9 +368,7 @@ async fn p43_verify_drain_returns_zero_for_all_shards() {
// We verify at least some documents were migrated (not exact count)
assert!(
total_docs > 0,
"Shard {} should have at least some docs on remaining nodes, got {}",
shard_id,
total_docs
"Shard {shard_id} should have at least some docs on remaining nodes, got {total_docs}"
);
}
}
@ -386,7 +383,7 @@ async fn p43_remove_without_drain_returns_conflict() {
let shards = 64;
let rf = 2;
let mut topo = create_test_topology(shards, 3, rf);
let topo = create_test_topology(shards, 3, rf);
// Try to remove node-1 without draining first
let topo_arc = Arc::new(RwLock::new(topo.clone()));
@ -405,11 +402,10 @@ async fn p43_remove_without_drain_returns_conflict() {
// Should fail with 409 Conflict
assert!(result.is_err(), "Remove without drain should fail");
let err = result.unwrap_err();
let err_msg = format!("{}", err);
let err_msg = format!("{err}");
assert!(
err_msg.contains("not in draining state") || err_msg.contains("drain"),
"Error should mention draining: {}",
err
"Error should mention draining: {err}"
);
}
@ -430,7 +426,7 @@ async fn p43_force_drain_rf1_surfaces_warning() {
let config = RebalancerConfig::default();
let migration_config = MigrationConfig::default();
let mut rebalancer = Rebalancer::new(config, topo_arc.clone(), migration_config);
let rebalancer = Rebalancer::new(config, topo_arc.clone(), migration_config);
// Try force drain
let request = miroir_core::rebalancer::DrainNodeRequest {
@ -503,7 +499,7 @@ async fn p43_cannot_drain_last_node_in_group() {
let config = RebalancerConfig::default();
let migration_config = MigrationConfig::default();
let mut rebalancer = Rebalancer::new(config, topo_arc.clone(), migration_config);
let rebalancer = Rebalancer::new(config, topo_arc.clone(), migration_config);
let request = miroir_core::rebalancer::DrainNodeRequest {
node_id: "node-0".to_string(),

View file

@ -22,7 +22,7 @@ proptest! {
rf in 1usize..4,
) {
let nodes: Vec<NodeId> = (0..node_count)
.map(|i| NodeId::new(format!("node-{}", i)))
.map(|i| NodeId::new(format!("node-{i}")))
.collect();
let rf = rf.min(node_count);
@ -43,7 +43,7 @@ proptest! {
rf in 1usize..4,
) {
let nodes: Vec<NodeId> = (0..node_count)
.map(|i| NodeId::new(format!("node-{}", i)))
.map(|i| NodeId::new(format!("node-{i}")))
.collect();
let rf = rf.min(node_count);
@ -93,7 +93,7 @@ proptest! {
rf in 1usize..3,
) {
let nodes_old: Vec<NodeId> = (0..node_count)
.map(|i| NodeId::new(format!("node-{}", i)))
.map(|i| NodeId::new(format!("node-{i}")))
.collect();
let mut nodes_new = nodes_old.clone();
@ -146,7 +146,7 @@ proptest! {
rf in 1usize..3,
) {
let nodes_all: Vec<NodeId> = (0..node_count)
.map(|i| NodeId::new(format!("node-{}", i)))
.map(|i| NodeId::new(format!("node-{i}")))
.collect();
let nodes_removed: Vec<NodeId> = nodes_all[..node_count - 1].to_vec();
@ -199,7 +199,7 @@ proptest! {
rf in 1usize..3,
) {
let nodes: Vec<NodeId> = (0..node_count)
.map(|i| NodeId::new(format!("node-{}", i)))
.map(|i| NodeId::new(format!("node-{i}")))
.collect();
let rf = rf.min(node_count);
@ -241,7 +241,7 @@ proptest! {
rf in 1usize..5,
) {
let nodes: Vec<NodeId> = (0..node_count)
.map(|i| NodeId::new(format!("node-{}", i)))
.map(|i| NodeId::new(format!("node-{i}")))
.collect();
let rf = rf.min(node_count);
@ -260,7 +260,7 @@ proptest! {
rf in 1usize..5,
) {
let nodes: Vec<NodeId> = (0..node_count)
.map(|i| NodeId::new(format!("node-{}", i)))
.map(|i| NodeId::new(format!("node-{i}")))
.collect();
let rf = rf.min(node_count);
@ -286,7 +286,7 @@ proptest! {
rf in 1usize..5,
) {
let nodes: Vec<NodeId> = (0..node_count)
.map(|i| NodeId::new(format!("node-{}", i)))
.map(|i| NodeId::new(format!("node-{i}")))
.collect();
let rf = rf.min(node_count);
@ -334,8 +334,7 @@ mod regression_tests {
let actual = shard_for_key(key, shard_count);
assert_eq!(
actual, expected,
"shard_for_key({:?}, {})",
key, shard_count
"shard_for_key({key:?}, {shard_count})"
);
}
}

View file

@ -34,9 +34,9 @@ pub enum CredentialError {
impl std::fmt::Display for CredentialError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CredentialError::NotFound(msg) => write!(f, "Credential not found: {}", msg),
CredentialError::IoError(e) => write!(f, "IO error: {}", e),
CredentialError::ParseError(msg) => write!(f, "Parse error: {}", msg),
CredentialError::NotFound(msg) => write!(f, "Credential not found: {msg}"),
CredentialError::IoError(e) => write!(f, "IO error: {e}"),
CredentialError::ParseError(msg) => write!(f, "Parse error: {msg}"),
}
}
}
@ -94,7 +94,7 @@ fn load_from_credentials_file() -> Result<Option<String>, CredentialError> {
let contents = fs::read_to_string(path).map_err(CredentialError::IoError)?;
let creds: CredentialsFile = toml::from_str(&contents)
.map_err(|e| CredentialError::ParseError(format!("Invalid TOML: {}", e)))?;
.map_err(|e| CredentialError::ParseError(format!("Invalid TOML: {e}")))?;
if let Some(profile) = creds.default {
if let Some(key) = profile.admin_api_key {

View file

@ -292,7 +292,7 @@ mod tests {
fn too_short_cookie_fails() {
let key = test_key();
// Only 10 bytes — shorter than nonce + tag
let short = URL_SAFE_NO_PAD.encode(&[0u8; 10]);
let short = URL_SAFE_NO_PAD.encode([0u8; 10]);
assert_eq!(
unseal_session(&short, &key).unwrap_err(),
SealError::MalformedCookie

View file

@ -10,7 +10,7 @@
//! the rotation overlap window. Validation accepts either secret.
use axum::{
extract::{FromRef, Request, State},
extract::{Request, State},
http::{HeaderMap, Method},
middleware::Next,
response::{IntoResponse, Response},
@ -99,14 +99,14 @@ pub fn jwt_encode(header: &JwtHeader, claims: &JwtClaims, secret: &[u8]) -> Resu
let header_b64 = URL_SAFE_NO_PAD.encode(header_json.as_bytes());
let payload_b64 = URL_SAFE_NO_PAD.encode(claims_json.as_bytes());
let signing_input = format!("{}.{}", header_b64, payload_b64);
let signing_input = format!("{header_b64}.{payload_b64}");
let mut mac = HmacSha256::new_from_slice(secret).map_err(|e| format!("HMAC init: {}", e))?;
let mut mac = HmacSha256::new_from_slice(secret).map_err(|e| format!("HMAC init: {e}"))?;
mac.update(signing_input.as_bytes());
let sig = mac.finalize().into_bytes();
let sig_b64 = URL_SAFE_NO_PAD.encode(sig);
Ok(format!("{}.{}.{}", header_b64, payload_b64, sig_b64))
Ok(format!("{header_b64}.{payload_b64}.{sig_b64}"))
}
/// Decode and verify a JWT with the given secret. Returns (header, claims).
@ -329,7 +329,7 @@ impl std::error::Error for JwtValidationError {}
pub fn generate_csrf_token() -> String {
let mut bytes = [0u8; 32];
rand::rngs::OsRng.fill_bytes(&mut bytes);
URL_SAFE_NO_PAD.encode(&bytes)
URL_SAFE_NO_PAD.encode(bytes)
}
/// Extract the CSRF token from the `X-CSRF-Token` header.
@ -420,7 +420,7 @@ pub fn validate_origin(
if allowed == "same-origin" {
if let Some(host) = headers.get("host").and_then(|h| h.to_str().ok()) {
// Construct origin from scheme (https) and host
let same_origin = format!("https://{}", host);
let same_origin = format!("https://{host}");
if provided_origin == same_origin || provided_origin == host {
return OriginVerdict::Allowed;
}
@ -1072,7 +1072,7 @@ fn epoch_seconds() -> u64 {
#[cfg(test)]
mod tests {
use super::*;
use axum::http::StatusCode;
fn test_key() -> SealKey {
SealKey::from_bytes([42u8; 32])
@ -1893,10 +1893,7 @@ mod tests {
let ratio = all_wrong_duration.as_secs_f64() / one_wrong_duration.as_secs_f64();
assert!(
ratio > 0.5 && ratio < 2.0,
"Timing ratio {} suggests non-constant-time comparison: all_wrong={:?}, one_wrong={:?}",
ratio,
all_wrong_duration,
one_wrong_duration,
"Timing ratio {ratio} suggests non-constant-time comparison: all_wrong={all_wrong_duration:?}, one_wrong={one_wrong_duration:?}",
);
}
@ -1989,9 +1986,7 @@ mod tests {
for (method, path) in cases {
assert!(
is_dispatch_exempt(&method, path),
"Expected ({}, {}) to be dispatch-exempt",
method,
path,
"Expected ({method}, {path}) to be dispatch-exempt",
);
}
}

View file

@ -91,7 +91,7 @@ impl NodeClient for HttpClient {
if let Some(global_idf) = &request.global_idf {
body["_miroir_global_idf"] = serde_json::to_value(global_idf).map_err(|e| {
NodeError::NetworkError(format!("Failed to serialize global_idf: {}", e))
NodeError::NetworkError(format!("Failed to serialize global_idf: {e}"))
})?;
}
@ -109,14 +109,14 @@ impl NodeClient for HttpClient {
error = %e,
"node call failed"
);
NodeError::NetworkError(format!("Request failed: {}", e))
NodeError::NetworkError(format!("Request failed: {e}"))
})?;
let status = response.status();
let body_text = response
.text()
.await
.map_err(|e| NodeError::NetworkError(format!("Failed to read response: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("Failed to read response: {e}")))?;
let duration_ms = start.elapsed().as_millis() as u64;
@ -140,7 +140,7 @@ impl NodeClient for HttpClient {
);
serde_json::from_str(&body_text)
.map_err(|e| NodeError::NetworkError(format!("Failed to parse JSON response: {}", e)))
.map_err(|e| NodeError::NetworkError(format!("Failed to parse JSON response: {e}")))
}
async fn write_documents(
@ -179,13 +179,13 @@ impl NodeClient for HttpClient {
let response = req_builder
.send()
.await
.map_err(|e| NodeError::NetworkError(format!("Request failed: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("Request failed: {e}")))?;
let status = response.status();
let body_text = response
.text()
.await
.map_err(|e| NodeError::NetworkError(format!("Failed to read response: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("Failed to read response: {e}")))?;
if !status.is_success() {
// Try to parse as Meilisearch error
@ -215,7 +215,7 @@ impl NodeClient for HttpClient {
// Parse successful response
let json: Value = serde_json::from_str(&body_text).map_err(|e| {
NodeError::NetworkError(format!("Failed to parse JSON response: {}", e))
NodeError::NetworkError(format!("Failed to parse JSON response: {e}"))
})?;
let duration_ms = start.elapsed().as_millis() as u64;
@ -263,13 +263,13 @@ impl NodeClient for HttpClient {
.json(&request.ids)
.send()
.await
.map_err(|e| NodeError::NetworkError(format!("Request failed: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("Request failed: {e}")))?;
let status = response.status();
let body_text = response
.text()
.await
.map_err(|e| NodeError::NetworkError(format!("Failed to read response: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("Failed to read response: {e}")))?;
let duration_ms = start.elapsed().as_millis() as u64;
tracing::debug!(
@ -310,7 +310,7 @@ impl NodeClient for HttpClient {
// Parse successful response
let json: Value = serde_json::from_str(&body_text).map_err(|e| {
NodeError::NetworkError(format!("Failed to parse JSON response: {}", e))
NodeError::NetworkError(format!("Failed to parse JSON response: {e}"))
})?;
Ok(DeleteResponse {
@ -351,13 +351,13 @@ impl NodeClient for HttpClient {
.json(&request.filter)
.send()
.await
.map_err(|e| NodeError::NetworkError(format!("Request failed: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("Request failed: {e}")))?;
let status = response.status();
let body_text = response
.text()
.await
.map_err(|e| NodeError::NetworkError(format!("Failed to read response: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("Failed to read response: {e}")))?;
let duration_ms = start.elapsed().as_millis() as u64;
tracing::debug!(
@ -398,7 +398,7 @@ impl NodeClient for HttpClient {
// Parse successful response
let json: Value = serde_json::from_str(&body_text).map_err(|e| {
NodeError::NetworkError(format!("Failed to parse JSON response: {}", e))
NodeError::NetworkError(format!("Failed to parse JSON response: {e}"))
})?;
Ok(DeleteResponse {
@ -437,7 +437,7 @@ impl NodeClient for HttpClient {
.header("Authorization", format!("Bearer {}", self.master_key))
.send()
.await
.map_err(|e| NodeError::NetworkError(format!("Stats request failed: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("Stats request failed: {e}")))?;
if !stats_resp.status().is_success() {
// Index not found or node unreachable — return empty stats
@ -451,7 +451,7 @@ impl NodeClient for HttpClient {
let stats_body: Value = stats_resp
.json()
.await
.map_err(|e| NodeError::NetworkError(format!("Failed to parse stats: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("Failed to parse stats: {e}")))?;
let total_docs = stats_body
.get("numberOfDocuments")
@ -471,11 +471,11 @@ impl NodeClient for HttpClient {
.json(&search_body)
.send()
.await
.map_err(|e| NodeError::NetworkError(format!("DF search failed: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("DF search failed: {e}")))?;
if search_resp.status().is_success() {
let body: Value = search_resp.json().await.map_err(|e| {
NodeError::NetworkError(format!("Failed to parse DF response: {}", e))
NodeError::NetworkError(format!("Failed to parse DF response: {e}"))
})?;
let df = body
.get("estimatedTotalHits")
@ -522,16 +522,16 @@ impl NodeClient for HttpClient {
async move {
let response = client
.get(&url)
.header("Authorization", format!("Bearer {}", master_key))
.header("Authorization", format!("Bearer {master_key}"))
.send()
.await
.map_err(|e| NodeError::NetworkError(format!("Request failed: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("Request failed: {e}")))?;
let status = response.status();
let body_text = response
.text()
.await
.map_err(|e| NodeError::NetworkError(format!("Failed to read response: {}", e)))?;
.map_err(|e| NodeError::NetworkError(format!("Failed to read response: {e}")))?;
if !status.is_success() {
return Err(NodeError::HttpError {
@ -542,7 +542,7 @@ impl NodeClient for HttpClient {
// Parse successful response
let json: Value = serde_json::from_str(&body_text).map_err(|e| {
NodeError::NetworkError(format!("Failed to parse JSON response: {}", e))
NodeError::NetworkError(format!("Failed to parse JSON response: {e}"))
})?;
Ok(TaskStatusResponse {
@ -576,7 +576,7 @@ impl miroir_core::group_sync_worker::SyncNodeClient for HttpClient {
) -> std::result::Result<serde_json::Value, String> {
let url = self.documents_url(address, &request.index_uid);
let filter_json = serde_json::to_string(&request.filter)
.map_err(|e| format!("Failed to serialize filter: {}", e))?;
.map_err(|e| format!("Failed to serialize filter: {e}"))?;
let response = self
.client
@ -589,19 +589,19 @@ impl miroir_core::group_sync_worker::SyncNodeClient for HttpClient {
])
.send()
.await
.map_err(|e| format!("Request failed: {}", e))?;
.map_err(|e| format!("Request failed: {e}"))?;
let status = response.status();
let body_text = response
.text()
.await
.map_err(|e| format!("Failed to read response: {}", e))?;
.map_err(|e| format!("Failed to read response: {e}"))?;
if !status.is_success() {
return Err(format!("HTTP {}: {}", status.as_u16(), body_text));
}
serde_json::from_str(&body_text).map_err(|e| format!("Failed to parse JSON: {}", e))
serde_json::from_str(&body_text).map_err(|e| format!("Failed to parse JSON: {e}"))
}
async fn write_documents(
@ -620,13 +620,13 @@ impl miroir_core::group_sync_worker::SyncNodeClient for HttpClient {
.json(&documents)
.send()
.await
.map_err(|e| format!("Request failed: {}", e))?;
.map_err(|e| format!("Request failed: {e}"))?;
let status = response.status();
let body_text = response
.text()
.await
.map_err(|e| format!("Failed to read response: {}", e))?;
.map_err(|e| format!("Failed to read response: {e}"))?;
if !status.is_success() {
return Err(format!("HTTP {}: {}", status.as_u16(), body_text));

View file

@ -6,14 +6,12 @@ use async_trait::async_trait;
use axum::http::request::Parts;
use axum::{
extract::{FromRequestParts, Request, State},
http::{HeaderMap, HeaderValue, StatusCode},
http::{HeaderMap, HeaderValue},
middleware::Next,
response::{IntoResponse, Response},
routing::get,
Extension, Router,
response::Response,
routing::get, Router,
};
use hex;
use miroir_core::config::MiroirConfig;
use prometheus::{
Counter, CounterVec, Encoder, Gauge, GaugeVec, Histogram, HistogramOpts, HistogramVec, Opts,
@ -31,6 +29,12 @@ use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct RequestId(pub String);
impl Default for RequestId {
fn default() -> Self {
Self::new()
}
}
impl RequestId {
/// Create a new RequestId from a UUIDv7.
///
@ -67,13 +71,9 @@ impl RequestId {
/// Extracted from the `X-Miroir-Session` header and stored in request extensions.
/// Handlers can access it via `Request.extensions().get::<SessionId>()`.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[derive(Default)]
pub struct SessionId(pub String);
impl Default for SessionId {
fn default() -> Self {
Self(String::new())
}
}
impl SessionId {
/// Get the inner session ID string.
@ -142,7 +142,7 @@ pub async fn request_id_middleware(mut req: Request, next: Next) -> Response {
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.and_then(|s| RequestId::parse(s.to_string()))
.unwrap_or_else(RequestId::new);
.unwrap_or_default();
// Store in request extensions for handler access
req.extensions_mut().insert(request_id.clone());
@ -1532,9 +1532,9 @@ impl Metrics {
let metric_families = self.registry.gather();
let mut buffer = Vec::new();
encoder.encode(&metric_families, &mut buffer)?;
Ok(String::from_utf8(buffer).map_err(|e| {
prometheus::Error::Msg(format!("failed to convert metrics to UTF-8: {}", e))
})?)
String::from_utf8(buffer).map_err(|e| {
prometheus::Error::Msg(format!("failed to convert metrics to UTF-8: {e}"))
})
}
pub fn admin_session_key_generated(&self) -> Gauge {
@ -1562,7 +1562,7 @@ pub fn generate_request_id() -> String {
let hash = hasher.finish();
// Encode as hex (16 chars = 64 bits)
format!("{:016x}", hash)
format!("{hash:016x}")
}
/// Extension trait to add request ID extraction utilities.
@ -1695,7 +1695,7 @@ pub async fn telemetry_middleware(
// Base fields: timestamp (from tracing-subscriber), level, message, duration_ms
// Additional fields (index, node_count, estimated_hits, degraded)
// are added by request handlers via the tracing span.
let message = format!("{} {}", method, status);
let message = format!("{method} {status}");
if status.is_server_error() {
tracing::error!(
target: "miroir.request",
@ -1753,7 +1753,7 @@ async fn metrics_handler(State(metrics): State<Metrics>) -> Response {
Ok(metrics) => metrics,
Err(e) => {
tracing::error!(error = %e, "failed to encode metrics");
format!("# ERROR: failed to encode metrics: {}\n", e)
format!("# ERROR: failed to encode metrics: {e}\n")
}
};
@ -2516,7 +2516,7 @@ mod tests {
"miroir_rebalance_duration_seconds",
];
for name in &expected_metrics {
assert!(output.contains(name), "missing metric: {}", name);
assert!(output.contains(name), "missing metric: {name}");
}
// With defaults (all §13 enabled), advanced metrics should be present
@ -2574,7 +2574,7 @@ mod tests {
"miroir_search_ui_p95_ms",
];
for name in &advanced_metrics {
assert!(output.contains(name), "missing advanced metric: {}", name);
assert!(output.contains(name), "missing advanced metric: {name}");
}
}
@ -2625,8 +2625,7 @@ mod tests {
for name in &advanced_names {
assert!(
!encoded.contains(name),
"advanced metric should not appear when disabled: {}",
name
"advanced metric should not appear when disabled: {name}"
);
}
}
@ -2881,7 +2880,7 @@ mod tests {
fn test_json_log_format_is_valid() {
// Verify that tracing-subscriber's JSON layer produces valid JSON
// This test ensures the log format matches plan §10 requirements
use tracing_subscriber::fmt::format::FmtSpan;
// Build a JSON subscriber like the one in main.rs
let subscriber = tracing_subscriber::fmt()

View file

@ -6,7 +6,7 @@ use super::{admin_endpoints, aliases, canary, cdc, dumps, explain, session};
use crate::admin_ui;
use axum::{
extract::FromRef,
routing::{delete, get, patch, post, put},
routing::{delete, get, post, put},
Router,
};

View file

@ -8,21 +8,20 @@ use axum::{
};
use miroir_core::{
config::MiroirConfig,
group_addition::{GroupAdditionCoordinator, GroupAdditionId},
group_addition::GroupAdditionCoordinator,
group_sync_worker::GroupSyncWorker,
leader_election::{LeaderElection, LeaderElectionMetricsCallback},
migration::{MigrationConfig, MigrationCoordinator},
mode_a_coordinator::ModeACoordinator,
mode_c_worker::{ModeCWorker, ModeCWorkerConfig},
peer_discovery::PeerDiscovery,
rebalancer::{MigrationExecutor, Rebalancer, RebalancerConfig, RebalancerMetrics},
rebalancer::{Rebalancer, RebalancerConfig, RebalancerMetrics},
rebalancer_worker::{
RebalancerMetricsCallback, RebalancerWorker, RebalancerWorkerConfig, TopologyChangeEvent,
},
replica_selection::{ReplicaSelector, SelectionObserver},
reshard::ReshardingRegistry,
router,
scatter::{DeleteByFilterRequest, FetchDocumentsRequest, WriteRequest},
task_registry::TaskRegistryImpl,
task_store::{NewAdminSession, RedisTaskStore, TaskStore},
topology::{Node, NodeId, Topology},
@ -114,9 +113,9 @@ impl VersionState {
{
let cache = self.version_cache.read().await;
let last_update = self.last_cache_update.read().await;
if let (Some(ref cached), Some(last)) = (cache.as_ref(), last_update.as_ref()) {
if let (Some(cached), Some(last)) = (cache.as_ref(), last_update.as_ref()) {
if last.elapsed().as_secs() < self.cache_ttl_secs {
return Ok((**cached).clone());
return Ok((*cached).clone());
}
}
}
@ -967,7 +966,7 @@ impl AppState {
// For each replica group, check if we have enough healthy nodes
for group in topo.groups() {
let healthy = group.healthy_nodes(&node_map);
let required = (topo.rf() + 1) / 2; // Simple majority for quorum
let required = topo.rf().div_ceil(2); // Simple majority for quorum
if healthy.len() < required {
return false;
}
@ -1227,8 +1226,7 @@ pub fn parse_rate_limit(s: &str) -> Result<(u64, u64), String> {
let parts: Vec<&str> = s.split('/').collect();
if parts.len() != 2 {
return Err(format!(
"invalid rate limit format: '{}', expected 'N/UNIT'",
s
"invalid rate limit format: '{s}', expected 'N/UNIT'"
));
}
let limit: u64 = parts[0]
@ -1241,8 +1239,7 @@ pub fn parse_rate_limit(s: &str) -> Result<(u64, u64), String> {
"day" | "d" => 86400,
unit => {
return Err(format!(
"invalid time unit: '{}', expected second/minute/hour/day",
unit
"invalid time unit: '{unit}', expected second/minute/hour/day"
))
}
};
@ -1253,7 +1250,7 @@ pub fn parse_rate_limit(s: &str) -> Result<(u64, u64), String> {
fn generate_session_id() -> String {
let mut bytes = [0u8; 24];
rand::rngs::OsRng.fill_bytes(&mut bytes);
hex::encode(&bytes)
hex::encode(bytes)
}
/// POST /_miroir/admin/login — admin login with rate limiting and exponential backoff.
@ -1330,8 +1327,7 @@ where
Json(AdminLoginResponse {
success: false,
message: Some(format!(
"Too many failed login attempts. Try again in {} seconds.",
ws
"Too many failed login attempts. Try again in {ws} seconds."
)),
csrf_token: None,
}),
@ -1381,8 +1377,7 @@ where
success: false,
message: if let Some(ws) = wait_seconds {
Some(format!(
"Too many failed login attempts. Try again in {} seconds.",
ws
"Too many failed login attempts. Try again in {ws} seconds."
))
} else {
Some("Too many login attempts. Please try again later.".into())
@ -1572,8 +1567,7 @@ where
[(
"Set-Cookie",
format!(
"{}=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0",
COOKIE_NAME
"{COOKIE_NAME}=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0"
),
)],
Json(AdminLogoutResponse {
@ -1591,8 +1585,7 @@ where
[(
"Set-Cookie",
format!(
"{}=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0",
COOKIE_NAME
"{COOKIE_NAME}=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0"
),
)],
Json(AdminLogoutResponse {
@ -1641,8 +1634,7 @@ where
[(
"Set-Cookie",
format!(
"{}=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0",
COOKIE_NAME
"{COOKIE_NAME}=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0"
),
)],
Json(AdminLogoutResponse {
@ -1722,7 +1714,7 @@ where
if topo.node(&node_id).is_some() {
return Err((
StatusCode::BAD_REQUEST,
format!("Node {} already exists", id),
format!("Node {id} already exists"),
));
}
// Check if replica group exists
@ -1730,7 +1722,7 @@ where
if replica_group >= group_count {
return Err((
StatusCode::BAD_REQUEST,
format!("Replica group {} does not exist", replica_group),
format!("Replica group {replica_group} does not exist"),
));
}
let node = Node::new(node_id, address, replica_group);
@ -1749,7 +1741,7 @@ where
error!(error = %e, node_id = %id, "failed to send NodeAdded event to rebalancer worker");
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to queue rebalancing: {}", e),
format!("Failed to queue rebalancing: {e}"),
));
}
}
@ -1803,7 +1795,7 @@ where
let topo = app_state.topology.read().await;
let node = topo
.node(&node_id_obj)
.ok_or_else(|| (StatusCode::NOT_FOUND, format!("Node {} not found", node_id)))?;
.ok_or_else(|| (StatusCode::NOT_FOUND, format!("Node {node_id} not found")))?;
// Check if this is the last node in the group
let group = topo
@ -1830,10 +1822,8 @@ where
return Err((
StatusCode::BAD_REQUEST,
format!(
"Node {} is not in draining state (current: {:?}), use force=true to bypass",
node_id, node_status
)
.into(),
"Node {node_id} is not in draining state (current: {node_status:?}), use force=true to bypass"
),
));
}
@ -1890,7 +1880,7 @@ where
let node_id_obj = NodeId::new(node_id.clone());
let node = topo
.node(&node_id_obj)
.ok_or_else(|| (StatusCode::NOT_FOUND, format!("Node {} not found", node_id)))?;
.ok_or_else(|| (StatusCode::NOT_FOUND, format!("Node {node_id} not found")))?;
// Check if this is the last node in the group
let group = topo
@ -1932,7 +1922,7 @@ where
error!(error = %e, node_id = %node_id, "failed to send NodeDraining event to rebalancer worker");
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to queue drain: {}", e),
format!("Failed to queue drain: {e}"),
));
}
@ -1981,7 +1971,7 @@ where
let node_id_obj = NodeId::new(node_id.clone());
let node = topo
.node(&node_id_obj)
.ok_or_else(|| (StatusCode::NOT_FOUND, format!("Node {} not found", node_id)))?;
.ok_or_else(|| (StatusCode::NOT_FOUND, format!("Node {node_id} not found")))?;
let replica_group = node.replica_group;
@ -2004,7 +1994,7 @@ where
error!(error = %e, node_id = %node_id, "failed to send NodeFailed event to rebalancer worker");
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to queue node failure: {}", e),
format!("Failed to queue node failure: {e}"),
));
}
@ -2043,7 +2033,7 @@ where
let node_id_obj = NodeId::new(node_id.clone());
let node = topo
.node(&node_id_obj)
.ok_or_else(|| (StatusCode::NOT_FOUND, format!("Node {} not found", node_id)))?;
.ok_or_else(|| (StatusCode::NOT_FOUND, format!("Node {node_id} not found")))?;
let replica_group = node.replica_group;
@ -2066,7 +2056,7 @@ where
error!(error = %e, node_id = %node_id, "failed to send NodeRecovered event to rebalancer worker");
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to queue node recovery: {}", e),
format!("Failed to queue node recovery: {e}"),
));
}
@ -2181,7 +2171,7 @@ where
);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to trigger rebalance: {}", e),
format!("Failed to trigger rebalance: {e}"),
));
}
@ -2259,7 +2249,7 @@ where
}
for (&shard_id, shard_state) in job.shards.iter() {
let pct_complete = if job.shards.len() > 0 {
let pct_complete = if !job.shards.is_empty() {
let completed = job
.shards
.values()
@ -2434,7 +2424,7 @@ where
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to start group addition: {}", e),
format!("Failed to start group addition: {e}"),
)
})?;
@ -2442,7 +2432,7 @@ where
coord.begin_sync(addition_id).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to start sync: {}", e),
format!("Failed to start sync: {e}"),
)
})?;
@ -2488,7 +2478,7 @@ where
.ok_or_else(|| {
(
StatusCode::NOT_FOUND,
format!("No active addition for group {}", group_id),
format!("No active addition for group {group_id}"),
)
})?;
@ -2521,7 +2511,7 @@ where
"complete": complete,
"failed": failed,
},
"started_at": addition.started_at.map(|t| format!("{:?}", t)),
"started_at": addition.started_at.map(|t| format!("{t:?}")),
})))
}
@ -2566,8 +2556,7 @@ where
(
StatusCode::PRECONDITION_FAILED,
format!(
"Group {} is not ready for activation (sync not complete)",
group_id
"Group {group_id} is not ready for activation (sync not complete)"
),
)
})?;
@ -2588,13 +2577,13 @@ where
let source_group = topo.group(source_group_id).ok_or_else(|| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Source group {} not found", source_group_id),
format!("Source group {source_group_id} not found"),
)
})?;
let new_group = topo.group(group_id).ok_or_else(|| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("New group {} not found", group_id),
format!("New group {group_id} not found"),
)
})?;
@ -2606,13 +2595,13 @@ where
if source_nodes.is_empty() {
return Err((
StatusCode::PRECONDITION_FAILED,
format!("No healthy nodes in source group {}", source_group_id),
format!("No healthy nodes in source group {source_group_id}"),
));
}
if new_nodes.is_empty() {
return Err((
StatusCode::PRECONDITION_FAILED,
format!("No healthy nodes in new group {}", group_id),
format!("No healthy nodes in new group {group_id}"),
));
}
@ -2631,7 +2620,7 @@ where
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to create HTTP client: {}", e),
format!("Failed to create HTTP client: {e}"),
)
})?;
@ -2660,7 +2649,7 @@ where
tracing::error!(error = %e, "Failed to fetch stats from source node");
(
StatusCode::SERVICE_UNAVAILABLE,
format!("Failed to fetch stats from source node: {}", e),
format!("Failed to fetch stats from source node: {e}"),
)
})?
.json()
@ -2669,7 +2658,7 @@ where
tracing::error!(error = %e, "Failed to parse stats from source node");
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to parse stats from source node: {}", e),
format!("Failed to parse stats from source node: {e}"),
)
})?;
@ -2686,7 +2675,7 @@ where
tracing::error!(error = %e, "Failed to fetch stats from new group node");
(
StatusCode::SERVICE_UNAVAILABLE,
format!("Failed to fetch stats from new group node: {}", e),
format!("Failed to fetch stats from new group node: {e}"),
)
})?
.json()
@ -2695,7 +2684,7 @@ where
tracing::error!(error = %e, "Failed to parse stats from new group node");
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to parse stats from new group node: {}", e),
format!("Failed to parse stats from new group node: {e}"),
)
})?;
@ -2711,11 +2700,7 @@ where
// Calculate variance percentage (allowing for writes during sync)
let variance = if source_count > 0 {
let diff = if source_count > new_count {
source_count - new_count
} else {
new_count - source_count
};
let diff = source_count.abs_diff(new_count);
(diff as f64 / source_count as f64) * 100.0
} else {
0.0
@ -2727,8 +2712,7 @@ where
return Err((
StatusCode::PRECONDITION_FAILED,
format!(
"Verification failed: new group has {} docs, source has {} docs (variance: {:.3}%) - must be within {:.1}%",
new_count, source_count, variance, MAX_VARIANCE_PERCENT
"Verification failed: new group has {new_count} docs, source has {source_count} docs (variance: {variance:.3}%) - must be within {MAX_VARIANCE_PERCENT:.1}%"
),
));
}
@ -2748,7 +2732,7 @@ where
coord.mark_group_active(addition_id).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to activate group: {}", e),
format!("Failed to activate group: {e}"),
)
})?;
}
@ -2926,10 +2910,7 @@ where
let mut filtered = diffs;
if let Some(target) = &params.target {
filtered = filtered
.into_iter()
.filter(|d| &d.target == target)
.collect();
filtered.retain(|d| &d.target == target);
}
Ok(Json(serde_json::json!({
@ -3049,16 +3030,15 @@ where
for key in obj.keys() {
let requires_restart = RESTART_REQUIRED_FIELDS
.iter()
.any(|field| key.starts_with(&format!("{}.", field)) || key == *field);
.any(|field| key.starts_with(&format!("{field}.")) || key == *field);
if requires_restart {
return Err((
StatusCode::BAD_REQUEST,
format!(
"Cannot modify '{}' at runtime. \
"Cannot modify '{key}' at runtime. \
This setting requires a pod restart to take effect. \
Please update the configuration file and restart the pod.",
key
Please update the configuration file and restart the pod."
),
));
}
@ -3070,7 +3050,7 @@ where
let merged_json = serde_json::to_value(&config).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to serialize current config: {}", e),
format!("Failed to serialize current config: {e}"),
)
})?;
@ -3078,7 +3058,7 @@ where
let updated_config: MiroirConfig = serde_json::from_value(merged_json).map_err(|e| {
(
StatusCode::BAD_REQUEST,
format!("Invalid configuration: {}", e),
format!("Invalid configuration: {e}"),
)
})?;
@ -3086,7 +3066,7 @@ where
if let Err(e) = updated_config.validate() {
return Err((
StatusCode::BAD_REQUEST,
format!("Configuration validation failed: {}", e),
format!("Configuration validation failed: {e}"),
));
}
@ -3396,7 +3376,7 @@ where
});
Ok(Json(ReshardResponse {
operation_id: format!("reshard-{}-{}", index_uid, now),
operation_id: format!("reshard-{index_uid}-{now}"),
index_uid,
old_shards,
new_shards: req.new_shards,

View file

@ -7,7 +7,7 @@ use axum::{
Json,
};
use miroir_core::{
alias::{Alias, AliasKind},
alias::AliasKind,
config::MiroirConfig,
task_store::TaskStore,
};
@ -152,8 +152,7 @@ where
Json(ErrorResponse {
code: "alias_exists_ilm_managed".to_string(),
message: format!(
"alias '{}' exists and is managed by ILM policy; use ILM API to modify",
name
"alias '{name}' exists and is managed by ILM policy; use ILM API to modify"
),
}),
));
@ -179,7 +178,7 @@ where
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
code: "alias_creation_failed".to_string(),
message: format!("failed to create alias: {}", e),
message: format!("failed to create alias: {e}"),
}),
)
})?;
@ -229,7 +228,7 @@ where
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
code: "alias_lookup_failed".to_string(),
message: format!("failed to lookup alias: {}", e),
message: format!("failed to lookup alias: {e}"),
}),
)
})?;
@ -259,7 +258,7 @@ where
StatusCode::NOT_FOUND,
Json(ErrorResponse {
code: "alias_not_found".to_string(),
message: format!("alias '{}' not found", name),
message: format!("alias '{name}' not found"),
}),
)),
}
@ -307,7 +306,7 @@ where
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
code: "alias_lookup_failed".to_string(),
message: format!("failed to lookup alias: {}", e),
message: format!("failed to lookup alias: {e}"),
}),
)
})?;
@ -317,7 +316,7 @@ where
StatusCode::NOT_FOUND,
Json(ErrorResponse {
code: "alias_not_found".to_string(),
message: format!("alias '{}' not found", name),
message: format!("alias '{name}' not found"),
}),
)
})?;
@ -351,7 +350,7 @@ where
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
code: "alias_flip_failed".to_string(),
message: format!("failed to flip alias: {}", e),
message: format!("failed to flip alias: {e}"),
}),
)
})?;
@ -367,7 +366,7 @@ where
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
code: "alias_lookup_failed".to_string(),
message: format!("failed to lookup updated alias: {}", e),
message: format!("failed to lookup updated alias: {e}"),
}),
)
})?
@ -438,7 +437,7 @@ where
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
code: "alias_deletion_failed".to_string(),
message: format!("failed to delete alias: {}", e),
message: format!("failed to delete alias: {e}"),
}),
)
})?;
@ -450,7 +449,7 @@ where
StatusCode::NOT_FOUND,
Json(ErrorResponse {
code: "alias_not_found".to_string(),
message: format!("alias '{}' not found", name),
message: format!("alias '{name}' not found"),
}),
))
}
@ -489,7 +488,7 @@ where
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
code: "alias_list_failed".to_string(),
message: format!("failed to list aliases: {}", e),
message: format!("failed to list aliases: {e}"),
}),
)
})?;
@ -513,9 +512,9 @@ where
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::Request;
use tower::ServiceExt;
#[test]
fn test_create_alias_request_single() {

View file

@ -99,7 +99,7 @@ where
let assertions: Vec<CanaryAssertion> = req
.assertions
.into_iter()
.map(|v| serde_json::from_value(v))
.map(serde_json::from_value)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| {
tracing::error!(error = %e, "Invalid canary assertion");
@ -197,7 +197,7 @@ where
tracing::error!(error = %e, "Failed to get canary");
StatusCode::INTERNAL_SERVER_ERROR
})?
.ok_or_else(|| StatusCode::NOT_FOUND)?;
.ok_or(StatusCode::NOT_FOUND)?;
let runs = state.store.get_canary_runs(&id, 100).unwrap_or_default();
@ -232,7 +232,7 @@ where
tracing::error!(error = %e, "Failed to get canary");
StatusCode::INTERNAL_SERVER_ERROR
})?
.ok_or_else(|| StatusCode::NOT_FOUND)?;
.ok_or(StatusCode::NOT_FOUND)?;
// Parse query
let query: SearchQuery = serde_json::from_value(serde_json::json!(req.query)).map_err(|e| {
@ -244,7 +244,7 @@ where
let assertions: Vec<CanaryAssertion> = req
.assertions
.into_iter()
.map(|v| serde_json::from_value(v))
.map(serde_json::from_value)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| {
tracing::error!(error = %e, "Invalid canary assertion");
@ -351,7 +351,7 @@ where
let captured = queries
.iter()
.find(|q| q.index_uid == index_uid)
.ok_or_else(|| StatusCode::NOT_FOUND)?;
.ok_or(StatusCode::NOT_FOUND)?;
let id = Uuid::new_v4().to_string();

View file

@ -8,9 +8,7 @@ use axum::{
http::StatusCode,
Json,
};
use miroir_core::cdc::CdcManager;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
/// Query parameters for GET /_miroir/changes.
#[derive(Debug, Deserialize)]
@ -103,7 +101,7 @@ where
#[cfg(test)]
mod tests {
use super::*;
use miroir_core::cdc::{CdcConfig, CdcEvent, CdcOperation};
#[test]
fn test_changes_query_params_default_limit() {

View file

@ -30,10 +30,9 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use tracing::{debug, info, instrument};
use tracing::instrument;
use crate::client::HttpClient;
use crate::middleware::SessionId;
use crate::routes::admin_endpoints::AppState;
/// Document write parameters from query string.
@ -386,8 +385,7 @@ async fn write_documents_impl(
return Err(MeilisearchError::new(
MiroirCode::MultiAliasNotWritable,
format!(
"alias '{}' is a multi-target alias and is read-only (managed by ILM); writes must go to the concrete index or the write alias",
index
"alias '{index}' is a multi-target alias and is read-only (managed by ILM); writes must go to the concrete index or the write alias"
),
));
}
@ -404,12 +402,12 @@ async fn write_documents_impl(
// 1. Extract primary key from first document if not provided
let primary_key =
primary_key.or_else(|| documents.first().and_then(|doc| extract_primary_key(doc)));
primary_key.or_else(|| documents.first().and_then(extract_primary_key));
let primary_key = primary_key.ok_or_else(|| {
MeilisearchError::new(
MiroirCode::PrimaryKeyRequired,
format!("primary key required for index `{}`", index),
format!("primary key required for index `{index}`"),
)
})?;
@ -433,7 +431,7 @@ async fn write_documents_impl(
if anti_entropy_enabled && doc.get(updated_at_field).is_some() {
return Err(MeilisearchError::new(
MiroirCode::ReservedField,
format!("document contains reserved field `{}` (reserved when anti_entropy.enabled: true)", updated_at_field),
format!("document contains reserved field `{updated_at_field}` (reserved when anti_entropy.enabled: true)"),
));
}
@ -444,8 +442,7 @@ async fn write_documents_impl(
return Err(MeilisearchError::new(
MiroirCode::ReservedField,
format!(
"document contains reserved field `{}` (reserved when ttl.enabled: true)",
expires_at_field
"document contains reserved field `{expires_at_field}` (reserved when ttl.enabled: true)"
),
));
}
@ -454,8 +451,7 @@ async fn write_documents_impl(
return Err(MeilisearchError::new(
MiroirCode::PrimaryKeyRequired,
format!(
"document at index {} missing primary key field `{}`",
i, primary_key
"document at index {i} missing primary key field `{primary_key}`"
),
));
}
@ -571,7 +567,7 @@ async fn write_documents_impl(
if targets.is_empty() {
return Err(MeilisearchError::new(
MiroirCode::ShardUnavailable,
format!("no available nodes for shard {}", shard_id),
format!("no available nodes for shard {shard_id}"),
));
}
@ -710,7 +706,7 @@ async fn write_documents_impl(
.map_err(|e| {
MeilisearchError::new(
MiroirCode::ShardUnavailable,
format!("failed to register task: {}", e),
format!("failed to register task: {e}"),
)
})?;
@ -831,8 +827,7 @@ async fn delete_by_ids_impl(
return Err(MeilisearchError::new(
MiroirCode::MultiAliasNotWritable,
format!(
"alias '{}' is a multi-target alias and is read-only (managed by ILM); deletes must go to the concrete index or the write alias",
index
"alias '{index}' is a multi-target alias and is read-only (managed by ILM); deletes must go to the concrete index or the write alias"
),
));
}
@ -872,7 +867,7 @@ async fn delete_by_ids_impl(
if targets.is_empty() {
return Err(MeilisearchError::new(
MiroirCode::ShardUnavailable,
format!("no available nodes for shard {}", shard_id),
format!("no available nodes for shard {shard_id}"),
));
}
@ -942,7 +937,7 @@ async fn delete_by_ids_impl(
.map_err(|e| {
MeilisearchError::new(
MiroirCode::ShardUnavailable,
format!("failed to register task: {}", e),
format!("failed to register task: {e}"),
)
})?;
@ -1112,7 +1107,7 @@ async fn delete_by_filter_impl(
.map_err(|e| {
MeilisearchError::new(
MiroirCode::ShardUnavailable,
format!("failed to register task: {}", e),
format!("failed to register task: {e}"),
)
})?;
@ -1210,7 +1205,7 @@ fn build_response_with_degraded_header(
let body = serde_json::to_string(&response).map_err(|e| {
MeilisearchError::new(
MiroirCode::ShardUnavailable,
format!("failed to serialize response: {}", e),
format!("failed to serialize response: {e}"),
)
})?;
@ -1222,16 +1217,16 @@ fn build_response_with_degraded_header(
if degraded_groups > 0 {
builder = builder.header(
HEADER_MIROIR_DEGRADED,
format!("groups={}", degraded_groups),
format!("groups={degraded_groups}"),
);
}
Ok(builder.body(axum::body::Body::from(body)).map_err(|e| {
builder.body(axum::body::Body::from(body)).map_err(|e| {
MeilisearchError::new(
MiroirCode::ShardUnavailable,
format!("failed to build response: {}", e),
format!("failed to build response: {e}"),
)
})?)
})
}
/// Build an error response as JSON (for forwarded node errors).
@ -1289,7 +1284,7 @@ mod tests {
fn reserved_field_error(field: &str) -> MeilisearchError {
MeilisearchError::new(
MiroirCode::ReservedField,
format!("document contains reserved field `{}`", field),
format!("document contains reserved field `{field}`"),
)
}
@ -1312,8 +1307,7 @@ mod tests {
let err = MeilisearchError::new(
MiroirCode::ReservedField,
format!(
"document contains reserved field `{}` (reserved when anti_entropy.enabled: true)",
field
"document contains reserved field `{field}` (reserved when anti_entropy.enabled: true)"
),
);
assert_eq!(err.code, "miroir_reserved_field");
@ -1327,8 +1321,7 @@ mod tests {
let err = MeilisearchError::new(
MiroirCode::ReservedField,
format!(
"document contains reserved field `{}` (reserved when ttl.enabled: true)",
field
"document contains reserved field `{field}` (reserved when ttl.enabled: true)"
),
);
assert_eq!(err.code, "miroir_reserved_field");

View file

@ -5,18 +5,12 @@
//! - `GET /_miroir/dumps/import/{id}/status` — get import status
use axum::extract::{Extension, FromRef, Path};
use axum::http::StatusCode;
use axum::routing::{get, post};
use axum::{Json, Router};
use miroir_core::api_error::{MeilisearchError, MiroirCode};
use miroir_core::config::Config;
use miroir_core::dump_import::{DumpImportManager, DumpImportPhase, DumpImportStatus};
use miroir_core::topology::Topology;
use serde_json::Value;
use std::sync::Arc;
use crate::client::HttpClient;
use crate::middleware::Metrics;
/// Request body for starting a dump import.
#[derive(serde::Deserialize)]
@ -91,7 +85,7 @@ where
Err(e) => {
return Err(MeilisearchError::new(
MiroirCode::InvalidRequest,
format!("invalid base64 dump_data: {}", e),
format!("invalid base64 dump_data: {e}"),
))
}
}
@ -124,7 +118,7 @@ where
.map_err(|e| {
MeilisearchError::new(
MiroirCode::InternalError,
format!("failed to start import: {}", e),
format!("failed to start import: {e}"),
)
})?;
@ -143,7 +137,7 @@ where
bytes_read
);
let status_url = format!("/_miroir/dumps/import/{}/status", import_id);
let status_url = format!("/_miroir/dumps/import/{import_id}/status");
Ok(Json(DumpImportResponse {
miroir_task_id: import_id,
@ -175,7 +169,7 @@ where
let status = manager.get_status(&id).await.ok_or_else(|| {
MeilisearchError::new(
MiroirCode::NotFound,
format!("import task not found: {}", id),
format!("import task not found: {id}"),
)
})?;
@ -222,7 +216,7 @@ fn base64_decode(s: &str) -> Result<Vec<u8>, String> {
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(s)
.map_err(|e| format!("base64 decode failed: {}", e))
.map_err(|e| format!("base64 decode failed: {e}"))
}
/// Get current UNIX timestamp in milliseconds.

View file

@ -1,12 +1,11 @@
//! Query explain API endpoint (plan §13.20).
use axum::{
extract::{Extension, FromRef, Path, Query},
extract::{Extension, Path, Query},
http::{HeaderMap, StatusCode},
Json,
};
use miroir_core::{
api_error::{MeilisearchError, MiroirCode},
config::MiroirConfig,
explainer::{BroadcastPending, Explainer, SearchQueryExplanation, Warning},
query_planner::QueryPlanner,
@ -15,7 +14,6 @@ use miroir_core::{
topology::Topology,
};
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
@ -55,7 +53,7 @@ pub async fn explain_search<S>(
Query(params): Query<ExplainParams>,
Extension(state): Extension<Arc<AppState>>,
headers: HeaderMap,
Json(mut query): Json<SearchQuery>,
Json(query): Json<SearchQuery>,
) -> Result<Json<serde_json::Value>, StatusCode>
where
S: Clone + Send + Sync + 'static,
@ -309,8 +307,7 @@ async fn check_unfilterable_attributes(
warnings.push(Warning::UnfilterableAttribute {
attribute: attr.clone(),
suggestion: format!(
"add '{}' to filterableAttributes or remove from filter",
attr
"add '{attr}' to filterableAttributes or remove from filter"
),
});
}
@ -336,8 +333,8 @@ fn extract_attributes_from_filter(filter: &str) -> Vec<String> {
let known_attrs = vec!["id", "sku", "category", "price", "status", "tenant"];
for attr in known_attrs {
if filter_lower.contains(&format!(r#"{}"#, attr))
|| filter_lower.contains(&format!(r#"{}"#, attr))
if filter_lower.contains(&attr.to_string())
|| filter_lower.contains(&attr.to_string())
{
attrs.push(attr.to_string());
}

View file

@ -11,7 +11,7 @@
//! - `GET /stats` — global stats across all indexes
use axum::extract::{Extension, Path};
use axum::http::{HeaderMap, StatusCode};
use axum::http::StatusCode;
use axum::routing::{get, post};
use axum::{Json, Router};
use futures_util::future::join_all;
@ -19,7 +19,6 @@ use miroir_core::api_error::{MeilisearchError, MiroirCode};
use miroir_core::config::Config;
use miroir_core::error::MiroirError;
use miroir_core::scatter::{PreflightRequest, PreflightResponse, TermStats};
use miroir_core::settings::{BroadcastPhase, SettingsBroadcast};
use miroir_core::topology::Topology;
use reqwest::Client;
use serde_json::Value;
@ -38,14 +37,14 @@ fn convert_miroir_error(e: MiroirError) -> MeilisearchError {
"settings divergence detected across nodes",
),
MiroirError::NotFound(msg) => {
MeilisearchError::new(MiroirCode::NoQuorum, format!("not found: {}", msg))
MeilisearchError::new(MiroirCode::NoQuorum, format!("not found: {msg}"))
}
MiroirError::InvalidState(msg) => {
MeilisearchError::new(MiroirCode::NoQuorum, format!("invalid state: {}", msg))
MeilisearchError::new(MiroirCode::NoQuorum, format!("invalid state: {msg}"))
}
_ => MeilisearchError::new(
MiroirCode::NoQuorum,
format!("settings broadcast error: {}", e),
format!("settings broadcast error: {e}"),
),
}
}
@ -86,9 +85,9 @@ impl MeilisearchClient {
.json(body)
.send()
.await
.map_err(|e| format!("request failed: {}", e))?;
.map_err(|e| format!("request failed: {e}"))?;
let status = resp.status().as_u16();
let text = resp.text().await.map_err(|e| format!("read body: {}", e))?;
let text = resp.text().await.map_err(|e| format!("read body: {e}"))?;
Ok((status, text))
}
@ -107,9 +106,9 @@ impl MeilisearchClient {
.json(body)
.send()
.await
.map_err(|e| format!("request failed: {}", e))?;
.map_err(|e| format!("request failed: {e}"))?;
let status = resp.status().as_u16();
let text = resp.text().await.map_err(|e| format!("read body: {}", e))?;
let text = resp.text().await.map_err(|e| format!("read body: {e}"))?;
Ok((status, text))
}
@ -122,9 +121,9 @@ impl MeilisearchClient {
.header(self.auth_header().0, &self.auth_header().1)
.send()
.await
.map_err(|e| format!("request failed: {}", e))?;
.map_err(|e| format!("request failed: {e}"))?;
let status = resp.status().as_u16();
let text = resp.text().await.map_err(|e| format!("read body: {}", e))?;
let text = resp.text().await.map_err(|e| format!("read body: {e}"))?;
Ok((status, text))
}
@ -137,9 +136,9 @@ impl MeilisearchClient {
.header(self.auth_header().0, &self.auth_header().1)
.send()
.await
.map_err(|e| format!("request failed: {}", e))?;
.map_err(|e| format!("request failed: {e}"))?;
let status = resp.status().as_u16();
let text = resp.text().await.map_err(|e| format!("read body: {}", e))?;
let text = resp.text().await.map_err(|e| format!("read body: {e}"))?;
Ok((status, text))
}
@ -351,7 +350,7 @@ async fn create_index_handler(
// Phase 1: Create index on every node sequentially
for address in &nodes {
match client.post_raw(address, "/indexes", &body).await {
Ok((status, text)) if status >= 200 && status < 300 => {
Ok((status, text)) if (200..300).contains(&status) => {
if first_response.is_none() {
first_response = serde_json::from_str(&text).ok();
}
@ -361,8 +360,7 @@ async fn create_index_handler(
// Rollback: delete index on all previously created nodes
rollback_delete_index(&client, uid, &created_on).await;
let msg = format!(
"index creation failed on node {}: HTTP {} — {}",
address, status, text
"index creation failed on node {address}: HTTP {status} — {text}"
);
return Err(forward_or_miroir(status, &text, &msg));
}
@ -370,7 +368,7 @@ async fn create_index_handler(
rollback_delete_index(&client, uid, &created_on).await;
return Err(MeilisearchError::new(
MiroirCode::NoQuorum,
format!("index creation failed on node {}: {}", address, e),
format!("index creation failed on node {address}: {e}"),
));
}
}
@ -383,10 +381,10 @@ async fn create_index_handler(
if let Some(first_addr) = nodes.first() {
match client
.get_raw(first_addr, &format!("/indexes/{}/settings", uid))
.get_raw(first_addr, &format!("/indexes/{uid}/settings"))
.await
{
Ok((status, text)) if status >= 200 && status < 300 => {
Ok((status, text)) if (200..300).contains(&status) => {
if let Ok(settings) = serde_json::from_str::<Value>(&text) {
if let Some(existing) = settings
.get("filterableAttributes")
@ -411,9 +409,9 @@ async fn create_index_handler(
let mut patch_ok: Vec<String> = Vec::new();
for address in &nodes {
let path = format!("/indexes/{}/settings", uid);
let path = format!("/indexes/{uid}/settings");
match client.patch_raw(address, &path, &filterable_patch).await {
Ok((_status, _text)) if _status >= 200 && _status < 300 => {
Ok((_status, _text)) if (200..300).contains(&_status) => {
patch_ok.push(address.clone());
}
Ok((status, _text)) => {
@ -454,7 +452,7 @@ async fn create_index_handler(
async fn rollback_delete_index(client: &MeilisearchClient, uid: &str, nodes: &[String]) {
for address in nodes {
let path = format!("/indexes/{}", uid);
let path = format!("/indexes/{uid}");
match client.delete_raw(address, &path).await {
Ok(_) => tracing::info!(node = %address, "rollback: deleted index"),
Err(e) => {
@ -483,7 +481,7 @@ async fn list_indexes_handler(
tracing::error!(error = %e, "list indexes failed");
StatusCode::INTERNAL_SERVER_ERROR
})?;
if status >= 200 && status < 300 {
if (200..300).contains(&status) {
let json: Value = serde_json::from_str(&text).unwrap_or(serde_json::json!({"results": []}));
Ok(Json(json))
} else {
@ -504,12 +502,12 @@ async fn get_index_handler(
.nodes
.first()
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
let path = format!("/indexes/{}", index);
let path = format!("/indexes/{index}");
let (status, text) = client.get_raw(&address.address, &path).await.map_err(|e| {
tracing::error!(error = %e, "get index failed");
StatusCode::INTERNAL_SERVER_ERROR
})?;
if status >= 200 && status < 300 {
if (200..300).contains(&status) {
Ok(Json(serde_json::from_str(&text).unwrap_or(Value::Null)))
} else {
Err(StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR))
@ -528,13 +526,13 @@ async fn update_index_handler(
) -> Result<Json<Value>, MeilisearchError> {
let client = MeilisearchClient::new(config.node_master_key.clone());
let nodes = all_node_addresses(&config);
let path = format!("/indexes/{}", index);
let path = format!("/indexes/{index}");
// Snapshot current index state from all nodes before applying changes
let mut snapshots: Vec<(String, Value)> = Vec::new();
for address in &nodes {
match client.get_raw(address, &path).await {
Ok((status, text)) if status >= 200 && status < 300 => {
Ok((status, text)) if (200..300).contains(&status) => {
let snapshot: Value = serde_json::from_str(&text).unwrap_or(Value::Null);
snapshots.push((address.clone(), snapshot));
}
@ -542,13 +540,13 @@ async fn update_index_handler(
return Err(forward_or_miroir(
status,
&text,
&format!("failed to snapshot index on {}: HTTP {}", address, status),
&format!("failed to snapshot index on {address}: HTTP {status}"),
));
}
Err(e) => {
return Err(MeilisearchError::new(
MiroirCode::NoQuorum,
format!("failed to snapshot index on {}: {}", address, e),
format!("failed to snapshot index on {address}: {e}"),
));
}
}
@ -560,7 +558,7 @@ async fn update_index_handler(
for (address, _) in &snapshots {
match client.patch_raw(address, &path, &body).await {
Ok((status, text)) if status >= 200 && status < 300 => {
Ok((status, text)) if (200..300).contains(&status) => {
if first_response.is_none() {
first_response = serde_json::from_str(&text).ok();
}
@ -569,8 +567,7 @@ async fn update_index_handler(
Ok((status, text)) => {
rollback_index_update(&client, &path, &snapshots, &applied).await;
let msg = format!(
"index update failed on {}: HTTP {} — {}",
address, status, text
"index update failed on {address}: HTTP {status} — {text}"
);
return Err(forward_or_miroir(status, &text, &msg));
}
@ -578,7 +575,7 @@ async fn update_index_handler(
rollback_index_update(&client, &path, &snapshots, &applied).await;
return Err(MeilisearchError::new(
MiroirCode::NoQuorum,
format!("index update failed on {}: {}", address, e),
format!("index update failed on {address}: {e}"),
));
}
}
@ -599,7 +596,7 @@ async fn rollback_index_update(
for address in applied {
if let Some((_, snapshot)) = snapshots.iter().find(|(a, _)| a == address) {
match client.patch_raw(address, path, snapshot).await {
Ok((_status, _text)) if _status >= 200 && _status < 300 => {
Ok((_status, _text)) if (200..300).contains(&_status) => {
tracing::info!(node = %address, "index update rollback succeeded");
}
Ok((status, _text)) => {
@ -632,18 +629,18 @@ async fn delete_index_handler(
let mut errors: Vec<String> = Vec::new();
for address in &nodes {
let path = format!("/indexes/{}", index);
let path = format!("/indexes/{index}");
match client.delete_raw(address, &path).await {
Ok((status, text)) if status >= 200 && status < 300 => {
Ok((status, text)) if (200..300).contains(&status) => {
if first_response.is_none() {
first_response = serde_json::from_str(&text).ok();
}
}
Ok((status, text)) => {
errors.push(format!("{}: HTTP {}{}", address, status, text));
errors.push(format!("{address}: HTTP {status}{text}"));
}
Err(e) => {
errors.push(format!("{}: {}", address, e));
errors.push(format!("{address}: {e}"));
}
}
}
@ -708,7 +705,7 @@ async fn get_index_stats_handler(
if success_count == 0 {
return Err(MeilisearchError::new(
MiroirCode::NoQuorum,
format!("stats unavailable for index `{}`: all nodes failed", index),
format!("stats unavailable for index `{index}`: all nodes failed"),
));
}
@ -751,11 +748,11 @@ pub async fn global_stats_handler(
.map_err(|e| {
MeilisearchError::new(
MiroirCode::NoQuorum,
format!("failed to list indexes: {}", e),
format!("failed to list indexes: {e}"),
)
})?;
if status < 200 || status >= 300 {
if !(200..300).contains(&status) {
return Err(MeilisearchError::new(
MiroirCode::NoQuorum,
"failed to list indexes",
@ -775,22 +772,19 @@ pub async fn global_stats_handler(
for idx in &index_list {
if let Some(uid) = idx.get("uid").and_then(|v| v.as_str()) {
for address in &nodes {
match client.get_index_stats(address, uid).await {
Ok(stats) => {
if let Some(n) = stats.get("numberOfDocuments").and_then(|v| v.as_u64()) {
total_docs += n;
}
if let Some(fd) = stats.get("fieldDistribution").and_then(|v| v.as_object())
{
for (field, count) in fd {
if let Some(c) = count.as_u64() {
*total_field_distribution.entry(field.clone()).or_insert(0) +=
c;
}
if let Ok(stats) = client.get_index_stats(address, uid).await {
if let Some(n) = stats.get("numberOfDocuments").and_then(|v| v.as_u64()) {
total_docs += n;
}
if let Some(fd) = stats.get("fieldDistribution").and_then(|v| v.as_object())
{
for (field, count) in fd {
if let Some(c) = count.as_u64() {
*total_field_distribution.entry(field.clone()).or_insert(0) +=
c;
}
}
}
Err(_) => {}
}
}
}
@ -833,7 +827,7 @@ async fn update_settings_subpath_handler(
Extension(config): Extension<Arc<Config>>,
Json(body): Json<Value>,
) -> Result<Json<Value>, MeilisearchError> {
let path = format!("/settings/{}", subpath);
let path = format!("/settings/{subpath}");
two_phase_settings_broadcast(&state, &config, &index, &path, &body).await
}
@ -853,18 +847,18 @@ async fn two_phase_settings_broadcast(
) -> Result<Json<Value>, MeilisearchError> {
// Use sequential strategy for rollback compatibility
if config.settings_broadcast.strategy == "sequential" {
return update_settings_broadcast_legacy(&config, index, settings_path, body).await;
return update_settings_broadcast_legacy(config, index, settings_path, body).await;
}
let client = MeilisearchClient::new(config.node_master_key.clone());
let nodes = all_node_addresses(config);
let full_path = format!("/indexes/{}{}", index, settings_path);
let full_path = format!("/indexes/{index}{settings_path}");
// Check if a broadcast is already in flight
if state.settings_broadcast.is_in_flight(index).await {
return Err(MeilisearchError::new(
MiroirCode::IndexAlreadyExists,
format!("settings broadcast already in flight for index '{}'", index),
format!("settings broadcast already in flight for index '{index}'"),
));
}
@ -882,7 +876,7 @@ async fn two_phase_settings_broadcast(
for address in &nodes {
match client.patch_raw(address, &full_path, body).await {
Ok((status, text)) if status >= 200 && status < 300 => {
Ok((status, text)) if (200..300).contains(&status) => {
if first_response.is_none() {
first_response = serde_json::from_str(&text).ok();
}
@ -894,10 +888,10 @@ async fn two_phase_settings_broadcast(
}
}
Ok((status, text)) => {
errors.push(format!("{}: HTTP {}{}", address, status, text));
errors.push(format!("{address}: HTTP {status}{text}"));
}
Err(e) => {
errors.push(format!("{}: {}", address, e));
errors.push(format!("{address}: {e}"));
}
}
}
@ -942,7 +936,7 @@ async fn two_phase_settings_broadcast(
.map(|address| {
let client = client.clone();
let address = address.clone();
let path = format!("/indexes/{}{}", index, settings_path);
let path = format!("/indexes/{index}{settings_path}");
async move { (address.clone(), client.get_raw(&address, &path).await) }
})
.collect();
@ -955,17 +949,17 @@ async fn two_phase_settings_broadcast(
for (address, result) in results {
match result {
Ok((status, text)) if status >= 200 && status < 300 => {
Ok((status, text)) if (200..300).contains(&status) => {
if let Ok(settings) = serde_json::from_str::<Value>(&text) {
let hash = fingerprint_settings(&settings);
node_hashes.insert(address, hash);
}
}
Ok((status, text)) => {
verify_errors.push(format!("{}: HTTP {}{}", address, status, text));
verify_errors.push(format!("{address}: HTTP {status}{text}"));
}
Err(e) => {
verify_errors.push(format!("{}: {}", address, e));
verify_errors.push(format!("{address}: {e}"));
}
}
}
@ -1019,7 +1013,7 @@ async fn two_phase_settings_broadcast(
.settings_broadcast
.abort(
index,
format!("max repair retries ({}) exceeded", max_retries),
format!("max repair retries ({max_retries}) exceeded"),
)
.await
.ok();
@ -1039,7 +1033,7 @@ async fn two_phase_settings_broadcast(
return Err(MeilisearchError::new(
MiroirCode::NoQuorum,
format!("settings divergence detected after {} retries - writes frozen on index", max_retries),
format!("settings divergence detected after {max_retries} retries - writes frozen on index"),
));
}
@ -1065,7 +1059,7 @@ async fn two_phase_settings_broadcast(
let mut repair_errors: Vec<String> = Vec::new();
for address in &mismatched_nodes {
match client.patch_raw(address, &full_path, body).await {
Ok((status, _text)) if status >= 200 && status < 300 => {
Ok((status, _text)) if (200..300).contains(&status) => {
tracing::info!(
node = %address,
index = %index,
@ -1074,10 +1068,10 @@ async fn two_phase_settings_broadcast(
);
}
Ok((status, text)) => {
repair_errors.push(format!("{}: HTTP {}{}", address, status, text));
repair_errors.push(format!("{address}: HTTP {status}{text}"));
}
Err(e) => {
repair_errors.push(format!("{}: {}", address, e));
repair_errors.push(format!("{address}: {e}"));
}
}
}
@ -1165,13 +1159,13 @@ async fn update_settings_broadcast_legacy(
) -> Result<Json<Value>, MeilisearchError> {
let client = MeilisearchClient::new(config.node_master_key.clone());
let nodes = all_node_addresses(config);
let full_path = format!("/indexes/{}{}", index, settings_path);
let full_path = format!("/indexes/{index}{settings_path}");
// Snapshot current settings from all nodes before applying changes
let mut snapshots: Vec<(String, Value)> = Vec::new();
for address in &nodes {
match client.get_raw(address, &full_path).await {
Ok((status, text)) if status >= 200 && status < 300 => {
Ok((status, text)) if (200..300).contains(&status) => {
let snapshot: Value = serde_json::from_str(&text).unwrap_or(Value::Null);
snapshots.push((address.clone(), snapshot));
}
@ -1180,15 +1174,14 @@ async fn update_settings_broadcast_legacy(
status,
&text,
&format!(
"failed to snapshot settings on {}: HTTP {}",
address, status
"failed to snapshot settings on {address}: HTTP {status}"
),
));
}
Err(e) => {
return Err(MeilisearchError::new(
MiroirCode::NoQuorum,
format!("failed to snapshot settings on {}: {}", address, e),
format!("failed to snapshot settings on {address}: {e}"),
));
}
}
@ -1200,7 +1193,7 @@ async fn update_settings_broadcast_legacy(
for (address, _snapshot) in &snapshots {
match client.patch_raw(address, &full_path, body).await {
Ok((status, text)) if status >= 200 && status < 300 => {
Ok((status, text)) if (200..300).contains(&status) => {
if first_response.is_none() {
first_response = serde_json::from_str(&text).ok();
}
@ -1210,8 +1203,7 @@ async fn update_settings_broadcast_legacy(
// Rollback all previously applied nodes
rollback_settings(&client, &full_path, &snapshots, &applied).await;
let msg = format!(
"settings update failed on {}: HTTP {} — {}",
address, status, text
"settings update failed on {address}: HTTP {status} — {text}"
);
return Err(forward_or_miroir(status, &text, &msg));
}
@ -1219,7 +1211,7 @@ async fn update_settings_broadcast_legacy(
rollback_settings(&client, &full_path, &snapshots, &applied).await;
return Err(MeilisearchError::new(
MiroirCode::NoQuorum,
format!("settings update failed on {}: {}", address, e),
format!("settings update failed on {address}: {e}"),
));
}
}
@ -1241,7 +1233,7 @@ async fn rollback_settings(
// Find the snapshot for this address
if let Some((_, snapshot)) = snapshots.iter().find(|(a, _)| a == address) {
match client.patch_raw(address, full_path, snapshot).await {
Ok((_status, _text)) if _status >= 200 && _status < 300 => {
Ok((_status, _text)) if (200..300).contains(&_status) => {
tracing::info!(node = %address, "settings rollback succeeded");
}
Ok((status, _text)) => {
@ -1272,12 +1264,12 @@ async fn get_settings_handler(
.nodes
.first()
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
let path = format!("/indexes/{}/settings", index);
let path = format!("/indexes/{index}/settings");
let (status, text) = client.get_raw(&address.address, &path).await.map_err(|e| {
tracing::error!(error = %e, "get settings failed");
StatusCode::INTERNAL_SERVER_ERROR
})?;
if status >= 200 && status < 300 {
if (200..300).contains(&status) {
Ok(Json(serde_json::from_str(&text).unwrap_or(Value::Null)))
} else {
Err(StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR))
@ -1307,7 +1299,7 @@ async fn preview_settings_handler(
.nodes
.first()
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
let current_settings_path = format!("/indexes/{}/settings", index);
let current_settings_path = format!("/indexes/{index}/settings");
let (status, text) = client
.get_raw(&first_address.address, &current_settings_path)
.await
@ -1316,7 +1308,7 @@ async fn preview_settings_handler(
StatusCode::INTERNAL_SERVER_ERROR
})?;
let current_settings: Value = if status >= 200 && status < 300 {
let current_settings: Value = if (200..300).contains(&status) {
serde_json::from_str(&text).unwrap_or(Value::Null)
} else {
// Index doesn't exist yet - current settings are null
@ -1450,12 +1442,12 @@ async fn get_settings_subpath_handler(
.nodes
.first()
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
let path = format!("/indexes/{}/settings/{}", index, subpath);
let path = format!("/indexes/{index}/settings/{subpath}");
let (status, text) = client.get_raw(&address.address, &path).await.map_err(|e| {
tracing::error!(error = %e, "get settings subpath failed");
StatusCode::INTERNAL_SERVER_ERROR
})?;
if status >= 200 && status < 300 {
if (200..300).contains(&status) {
Ok(Json(serde_json::from_str(&text).unwrap_or(Value::Null)))
} else {
Err(StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR))

View file

@ -61,7 +61,7 @@ async fn create_key_handler(
for address in &nodes {
match client.post_raw(address, "/keys", &body).await {
Ok((status, text)) if status >= 200 && status < 300 => {
Ok((status, text)) if (200..300).contains(&status) => {
if first_response.is_none() {
first_response = serde_json::from_str(&text).ok();
}
@ -71,8 +71,7 @@ async fn create_key_handler(
// Rollback: delete key on all previously created nodes
rollback_delete_key(&client, &body, &created_on).await;
let msg = format!(
"key creation failed on {}: HTTP {} — {}",
address, status, text
"key creation failed on {address}: HTTP {status} — {text}"
);
return Err(forward_or_miroir(status, &text, &msg));
}
@ -80,7 +79,7 @@ async fn create_key_handler(
rollback_delete_key(&client, &body, &created_on).await;
return Err(MeilisearchError::new(
MiroirCode::NoQuorum,
format!("key creation failed on {}: {}", address, e),
format!("key creation failed on {address}: {e}"),
));
}
}
@ -107,7 +106,7 @@ async fn rollback_delete_key(client: &MeilisearchClient, body: &Value, nodes: &[
}
for address in nodes {
let path = format!("/keys/{}", key_or_name);
let path = format!("/keys/{key_or_name}");
match client.delete_raw(address, &path).await {
Ok(_) => tracing::info!(node = %address, "key rollback: deleted key"),
Err(e) => {
@ -128,13 +127,13 @@ async fn update_key_handler(
) -> Result<Json<Value>, MeilisearchError> {
let client = MeilisearchClient::new(config.node_master_key.clone());
let nodes = all_node_addresses(&config);
let path = format!("/keys/{}", key);
let path = format!("/keys/{key}");
// Snapshot current key state from all nodes
let mut snapshots: Vec<(String, Value)> = Vec::new();
for address in &nodes {
match client.get_raw(address, &path).await {
Ok((status, text)) if status >= 200 && status < 300 => {
Ok((status, text)) if (200..300).contains(&status) => {
let snapshot: Value = serde_json::from_str(&text).unwrap_or(Value::Null);
snapshots.push((address.clone(), snapshot));
}
@ -142,13 +141,13 @@ async fn update_key_handler(
return Err(forward_or_miroir(
status,
&text,
&format!("failed to snapshot key on {}: HTTP {}", address, status),
&format!("failed to snapshot key on {address}: HTTP {status}"),
));
}
Err(e) => {
return Err(MeilisearchError::new(
MiroirCode::NoQuorum,
format!("failed to snapshot key on {}: {}", address, e),
format!("failed to snapshot key on {address}: {e}"),
));
}
}
@ -160,7 +159,7 @@ async fn update_key_handler(
for (address, _snapshot) in &snapshots {
match client.patch_raw(address, &path, &body).await {
Ok((status, text)) if status >= 200 && status < 300 => {
Ok((status, text)) if (200..300).contains(&status) => {
if first_response.is_none() {
first_response = serde_json::from_str(&text).ok();
}
@ -169,8 +168,7 @@ async fn update_key_handler(
Ok((status, text)) => {
rollback_key_update(&client, &path, &snapshots, &applied).await;
let msg = format!(
"key update failed on {}: HTTP {} — {}",
address, status, text
"key update failed on {address}: HTTP {status} — {text}"
);
return Err(forward_or_miroir(status, &text, &msg));
}
@ -178,7 +176,7 @@ async fn update_key_handler(
rollback_key_update(&client, &path, &snapshots, &applied).await;
return Err(MeilisearchError::new(
MiroirCode::NoQuorum,
format!("key update failed on {}: {}", address, e),
format!("key update failed on {address}: {e}"),
));
}
}
@ -199,7 +197,7 @@ async fn rollback_key_update(
for address in applied {
if let Some((_, snapshot)) = snapshots.iter().find(|(a, _)| a == address) {
match client.patch_raw(address, path, snapshot).await {
Ok((_status, _text)) if _status >= 200 && _status < 300 => {
Ok((_status, _text)) if (200..300).contains(&_status) => {
tracing::info!(node = %address, "key rollback succeeded");
}
Ok((status, _text)) => {
@ -223,22 +221,22 @@ async fn delete_key_handler(
) -> Result<Json<Value>, MeilisearchError> {
let client = MeilisearchClient::new(config.node_master_key.clone());
let nodes = all_node_addresses(&config);
let path = format!("/keys/{}", key);
let path = format!("/keys/{key}");
let mut first_response: Option<Value> = None;
let mut errors: Vec<String> = Vec::new();
for address in &nodes {
match client.delete_raw(address, &path).await {
Ok((status, text)) if status >= 200 && status < 300 => {
Ok((status, text)) if (200..300).contains(&status) => {
if first_response.is_none() {
first_response = serde_json::from_str(&text).ok();
}
}
Ok((status, text)) => {
errors.push(format!("{}: HTTP {}{}", address, status, text));
errors.push(format!("{address}: HTTP {status}{text}"));
}
Err(e) => {
errors.push(format!("{}: {}", address, e));
errors.push(format!("{address}: {e}"));
}
}
}
@ -285,7 +283,7 @@ async fn list_keys_handler(
tracing::error!(error = %e, "list keys failed");
StatusCode::INTERNAL_SERVER_ERROR
})?;
if status >= 200 && status < 300 {
if (200..300).contains(&status) {
Ok(Json(serde_json::from_str(&text).unwrap_or(Value::Null)))
} else {
Err(StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR))
@ -305,12 +303,12 @@ async fn get_key_handler(
.nodes
.first()
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
let path = format!("/keys/{}", key);
let path = format!("/keys/{key}");
let (status, text) = client.get_raw(&address.address, &path).await.map_err(|e| {
tracing::error!(error = %e, "get key failed");
StatusCode::INTERNAL_SERVER_ERROR
})?;
if status >= 200 && status < 300 {
if (200..300).contains(&status) {
Ok(Json(serde_json::from_str(&text).unwrap_or(Value::Null)))
} else {
Err(StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR))

View file

@ -9,10 +9,8 @@ use miroir_core::{
config::UnavailableShardPolicy,
merger::{MergeStrategy, ScoreMergeStrategy},
multi_search::{MultiSearchExecutor, MultiSearchResponse, SearchResultData},
query_planner::QueryPlanner,
scatter::{
dfs_query_then_fetch_search, plan_search_scatter_with_narrowing, NodeClient, SearchRequest,
VectorMode,
},
shadow::ShadowOperation,
topology::Topology,
@ -22,9 +20,8 @@ use serde_json::Value;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::RwLock;
use tracing::{debug, info, instrument};
use tracing::{debug, instrument};
use crate::routes::admin_endpoints::AppState;
/// Multi-search state.
#[derive(Clone)]
@ -299,7 +296,7 @@ where
let topology = topology.clone();
let node_client = node_client.clone();
let config = config_for_executor.clone();
let strategy = strategy.clone();
let strategy = strategy;
let policy = policy;
let replica_selector = replica_selector_for_executor.clone();
let query_planner = query_planner_for_executor.clone();

View file

@ -624,8 +624,7 @@ async fn search_handler(
let _err = MeilisearchError::new(
MiroirCode::SettingsVersionStale,
format!(
"no covering set available for settings version floor {} on index '{}'",
floor, index
"no covering set available for settings version floor {floor} on index '{index}'"
),
);
return Response::builder()
@ -848,7 +847,7 @@ async fn search_handler(
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(",");
response = response.header("X-Miroir-Degraded", format!("shards={}", shard_ids));
response = response.header("X-Miroir-Degraded", format!("shards={shard_ids}"));
} else if result.degraded {
response = response.header("X-Miroir-Degraded", "partial");
}
@ -1185,8 +1184,7 @@ async fn search_multi_targets(
let _err = MeilisearchError::new(
MiroirCode::SettingsVersionStale,
format!(
"no covering set available for settings version floor {} on index '{}'",
floor, primary_target
"no covering set available for settings version floor {floor} on index '{primary_target}'"
),
);
return Response::builder()
@ -1317,7 +1315,7 @@ async fn search_multi_targets(
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(",");
response = response.header("X-Miroir-Degraded", format!("shards={}", shard_ids));
response = response.header("X-Miroir-Degraded", format!("shards={shard_ids}"));
} else if result.degraded {
response = response.header("X-Miroir-Degraded", "partial");
}
@ -1369,7 +1367,7 @@ mod tests {
ranking_score: Some(false),
rest: serde_json::json!({}),
};
let debug_output = format!("{:?}", body);
let debug_output = format!("{body:?}");
assert!(
!debug_output.contains("sensitive"),

View file

@ -14,7 +14,7 @@ use axum::{
};
use miroir_core::{
cdc::AnalyticsEvent,
config::advanced::{SearchUiAuthConfig, SearchUiConfig},
config::advanced::SearchUiConfig,
task_store::{SearchUiScopedKey, TaskStore},
};
use sha2::{Digest, Sha256};
@ -25,8 +25,7 @@ use crate::auth::{
use crate::error_response::ErrorResponse;
use rust_embed::RustEmbed as Embed;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tracing::{debug, info, warn};
use tracing::{debug, info};
use crate::routes::indexes::MeilisearchClient;
use crate::scoped_key_rotation::mint_scoped_key;
@ -224,7 +223,7 @@ pub async fn mint_session(
let now = chrono::Utc::now().timestamp() as u64;
let exp = now + auth_config.session_ttl_s;
let mut scope = vec![
let scope = vec![
"search".to_string(),
"multi_search".to_string(),
"beacon".to_string(),
@ -286,7 +285,7 @@ pub async fn mint_session(
};
let token = jwt_encode(&header, &claims, secret.as_bytes())
.map_err(|e| ErrorResponse::internal_error(format!("JWT encoding failed: {}", e)))?;
.map_err(|e| ErrorResponse::internal_error(format!("JWT encoding failed: {e}")))?;
info!(
index = %index_uid,
@ -343,7 +342,7 @@ async fn get_or_create_scoped_key(
let client = MeilisearchClient::new(state.config.node_master_key.clone());
let (new_key, new_uid) = mint_scoped_key(&client, &state.config, index_uid)
.await
.map_err(|e| ErrorResponse::internal_error(format!("failed to mint scoped key: {}", e)))?;
.map_err(|e| ErrorResponse::internal_error(format!("failed to mint scoped key: {e}")))?;
let now_ms = chrono::Utc::now().timestamp_millis();
let scoped_key = SearchUiScopedKey {
@ -359,7 +358,7 @@ async fn get_or_create_scoped_key(
// Store in Redis
redis_store
.set_search_ui_scoped_key(&scoped_key)
.map_err(|e| ErrorResponse::internal_error(format!("failed to store scoped key: {}", e)))?;
.map_err(|e| ErrorResponse::internal_error(format!("failed to store scoped key: {e}")))?;
info!(
index = %index_uid,
@ -385,7 +384,7 @@ pub async fn get_config(
if let Some(row) = task_store.get_search_ui_config(&index_uid)? {
// Parse the config JSON
let config: SearchUiIndexConfig = serde_json::from_str(&row.config_json)
.map_err(|e| ErrorResponse::internal_error(format!("failed to parse config: {}", e)))?;
.map_err(|e| ErrorResponse::internal_error(format!("failed to parse config: {e}")))?;
return Ok(Json(config));
}
@ -416,7 +415,7 @@ pub async fn update_config(
// Serialize config to JSON for storage
let config_json = serde_json::to_string(&config)
.map_err(|e| ErrorResponse::internal_error(format!("failed to serialize config: {}", e)))?;
.map_err(|e| ErrorResponse::internal_error(format!("failed to serialize config: {e}")))?;
// Validate custom template if present (plan §13.21)
if let Some(template) = &config.result_template {
@ -490,7 +489,7 @@ pub async fn beacon(
// Fallback: generate a session_id from the token itself
let hash = Sha256::digest(token.as_bytes());
let hash_hex = hex::encode(&hash[..16]);
format!("anon:{}", hash_hex)
format!("anon:{hash_hex}")
}
}
} else {
@ -701,13 +700,12 @@ fn validate_template(template: &str) -> Result<(), ErrorResponse> {
if_stack.push("if");
}
// Check for {{/if}} closing
else if tag.starts_with("/if") {
if if_stack.pop() != Some("if") {
else if tag.starts_with("/if")
&& if_stack.pop() != Some("if") {
return Err(ErrorResponse::invalid_request(
"unmatched {{/if}} tag in template".to_string(),
));
}
}
pos += end + 2;
} else {
@ -718,9 +716,7 @@ fn validate_template(template: &str) -> Result<(), ErrorResponse> {
}
if !if_stack.is_empty() {
return Err(ErrorResponse::invalid_request(format!(
"unclosed {{#if}} tag in template"
)));
return Err(ErrorResponse::invalid_request("unclosed {#if} tag in template".to_string()));
}
Ok(())
@ -780,7 +776,7 @@ pub fn render_custom_template(template: &str, data: &serde_json::Value) -> Strin
// Process simple {{field}} tags
let mut rendered = result;
for (key, value) in obj {
let tag = format!("{{{{{}}}}}", key);
let tag = format!("{{{{{key}}}}}");
let value_str = match value {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Number(n) => n.to_string(),

View file

@ -156,8 +156,7 @@ where
return Err((
StatusCode::TOO_MANY_REQUESTS,
format!(
"Too many failed login attempts. Try again in {} seconds.",
ws
"Too many failed login attempts. Try again in {ws} seconds."
),
));
} else {
@ -414,8 +413,7 @@ where
[(
"Set-Cookie",
format!(
"{}=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0",
COOKIE_NAME
"{COOKIE_NAME}=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0"
),
)],
Json(serde_json::json!({

View file

@ -402,8 +402,8 @@ fn task_to_response(task: MiroirTask) -> TaskResponse {
};
let enqueued_at = format_millis_timestamp(task.created_at);
let started_at = task.started_at.map(|t| format_millis_timestamp(t));
let finished_at = task.finished_at.map(|t| format_millis_timestamp(t));
let started_at = task.started_at.map(format_millis_timestamp);
let finished_at = task.finished_at.map(format_millis_timestamp);
let error = if task.status == TaskStatus::Failed {
Some(TaskError {

View file

@ -66,7 +66,7 @@ pub async fn run_scoped_key_rotator(state: ScopedKeyRotationState) {
// Check each index for rotation need with a per-index leader lease
let indexes = discover_scoped_indexes(&state).await;
for index_uid in &indexes {
let lease_scope = format!("search_ui_key_rotation:{}", index_uid);
let lease_scope = format!("search_ui_key_rotation:{index_uid}");
let lease_now = now_ms();
let lease_ttl_ms = (state.config.leader_election.lease_ttl_s as i64) * 1000;
let expires_at = lease_now + lease_ttl_ms;
@ -111,8 +111,8 @@ pub async fn check_and_rotate(
.map_err(|e| format!("redis read failed: {e}"))?;
// Timing gate check (skip if force)
if !force {
if !should_rotate(&current, &state.config) {
if !force
&& !should_rotate(&current, &state.config) {
return Ok(RotateScopedKeyResponse {
status: "skipped".into(),
index_uid: index_uid.into(),
@ -121,7 +121,6 @@ pub async fn check_and_rotate(
error: None,
});
}
}
// Step 1: Mint new scoped key via Meilisearch POST /keys
let client = MeilisearchClient::new(state.config.node_master_key.clone());
@ -258,7 +257,7 @@ pub async fn mint_scoped_key(
config: &MiroirConfig,
index_uid: &str,
) -> Result<(String, String), String> {
let description = format!("miroir search-ui scoped key for index {}", index_uid);
let description = format!("miroir search-ui scoped key for index {index_uid}");
let body = serde_json::json!({
"description": description,
"actions": ["search"],
@ -272,7 +271,7 @@ pub async fn mint_scoped_key(
for node in &config.nodes {
match client.post_raw(&node.address, "/keys", &body).await {
Ok((status, text)) if status >= 200 && status < 300 => {
Ok((status, text)) if (200..300).contains(&status) => {
if created_key.is_none() {
let resp: serde_json::Value = serde_json::from_str(&text)
.map_err(|e| format!("parse key response: {e}"))?;
@ -302,12 +301,12 @@ pub async fn revoke_previous_key(
config: &MiroirConfig,
previous_uid: &str,
) -> Result<(), String> {
let path = format!("/keys/{}", previous_uid);
let path = format!("/keys/{previous_uid}");
let mut errors = Vec::new();
for node in &config.nodes {
match client.delete_raw(&node.address, &path).await {
Ok((_status, _text)) if _status >= 200 && _status < 300 => {}
Ok((_status, _text)) if (200..300).contains(&_status) => {}
Ok((status, _text)) => {
// 404 is fine — key was already revoked or never existed
if status != 404 {