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:
parent
465075b5b3
commit
0b3552ee4f
73 changed files with 794 additions and 983 deletions
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}")))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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(¶ms)
|
||||
.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.
|
||||
|
|
|
|||
|
|
@ -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) = ¶ms.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(
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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())?;
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()))?;
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -121,8 +121,7 @@ impl NodeStatus {
|
|||
Ok(target)
|
||||
} else {
|
||||
Err(MiroirError::Topology(format!(
|
||||
"illegal state transition: {:?} → {:?}",
|
||||
self, target
|
||||
"illegal state transition: {self:?} → {target:?}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(°raded_with_hedge_latencies, 95);
|
||||
let no_hedge_p95 = percentile(°raded_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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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) = ¶ms.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,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, ¤t_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))
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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!({
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(¤t, &state.config) {
|
||||
if !force
|
||||
&& !should_rotate(¤t, &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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue