#![allow(clippy::await_holding_lock)]
use crate::sharding::coordinator::{CollectionShardConfig, ShardAssignment};
use crate::sharding::router::ShardRouter;
use crate::storage::http_client::get_http_client;
use crate::storage::StorageEngine;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[async_trait]
pub trait BatchSender: Send + Sync {
async fn send_batch(
&self,
db_name: &str,
coll_name: &str,
config: &CollectionShardConfig,
batch: Vec<(String, Value)>,
) -> Result<Vec<String>, String>;
async fn should_pause_resharding(&self) -> bool {
false
}
}
#[allow(clippy::too_many_arguments)]
async fn verify_migrated_documents(
storage: &StorageEngine,
db_name: &str,
coll_name: &str,
keys: &[String],
config: &CollectionShardConfig,
assignments: &std::collections::HashMap<u16, crate::sharding::coordinator::ShardAssignment>,
my_node_id: &str,
cluster_manager: Option<&std::sync::Arc<crate::cluster::manager::ClusterManager>>,
) -> Vec<String> {
let mut verified_keys = Vec::new();
let mut keys_by_node: std::collections::HashMap<String, Vec<(String, u16)>> =
std::collections::HashMap::new();
let mut local_keys: Vec<(String, u16)> = Vec::new();
for key in keys {
let shard_id = ShardRouter::route(key, config.num_shards);
if let Some(assignment) = assignments.get(&shard_id) {
if assignment.primary_node == my_node_id {
local_keys.push((key.clone(), shard_id));
} else {
keys_by_node
.entry(assignment.primary_node.clone())
.or_default()
.push((key.clone(), shard_id));
}
} else {
tracing::debug!("RESHARD: No assignment for key {}, trusting batch", key);
verified_keys.push(key.clone());
}
}
for (key, shard_id) in local_keys {
let physical_name = format!("{}_s{}", coll_name, shard_id);
if let Ok(db) = storage.get_database(db_name) {
if let Ok(physical_coll) = db.get_collection(&physical_name) {
if physical_coll.get(&key).is_ok() {
verified_keys.push(key);
} else {
tracing::warn!(
"RESHARD: Local verification FAILED for {} in {}",
key,
physical_name
);
}
} else {
tracing::debug!(
"RESHARD: Collection {} not found locally, trusting batch",
physical_name
);
verified_keys.push(key);
}
}
}
if let Some(mgr) = cluster_manager {
let client = get_http_client();
let secret = storage
.cluster_config()
.and_then(|c| c.keyfile.clone())
.unwrap_or_default();
for (node_id, node_keys) in keys_by_node {
if let Some(addr) = mgr.get_node_api_address(&node_id) {
let mut keys_by_shard: std::collections::HashMap<u16, Vec<String>> =
std::collections::HashMap::new();
for (key, shard_id) in node_keys {
keys_by_shard.entry(shard_id).or_default().push(key);
}
for (shard_id, shard_keys) in keys_by_shard {
let physical_name = format!("{}_s{}", coll_name, shard_id);
let url = format!(
"http://{}/_api/database/{}/document/{}/_verify",
addr, db_name, physical_name
);
match client
.post(&url)
.header("X-Cluster-Secret", &secret)
.json(&serde_json::json!({ "keys": shard_keys }))
.timeout(std::time::Duration::from_secs(30))
.send()
.await
{
Ok(response) => {
if let Ok(body) = response.json::<serde_json::Value>().await {
if let Some(found) = body.get("found").and_then(|f| f.as_array()) {
for key_val in found {
if let Some(k) = key_val.as_str() {
verified_keys.push(k.to_string());
}
}
let missing = body
.get("missing")
.and_then(|m| m.as_array())
.map(|a| a.len())
.unwrap_or(0);
if missing > 0 {
tracing::warn!("RESHARD: Remote verification found {} missing docs on node {}", missing, node_id);
}
}
}
}
Err(e) => {
tracing::error!("RESHARD: Failed to verify docs on node {}: {} - NOT marking as verified", node_id, e);
}
}
}
} else {
tracing::error!(
"RESHARD: Cannot find address for node {} - NOT marking docs as verified",
node_id
);
}
}
} else {
for key in keys {
if !verified_keys.contains(key) {
verified_keys.push(key.clone());
}
}
}
verified_keys
}
#[allow(clippy::too_many_arguments)]
pub async fn reshard_collection<S: BatchSender>(
storage: &StorageEngine,
sender: &S,
db_name: &str,
coll_name: &str,
old_shards: u16,
new_shards: u16,
my_node_id: &str,
old_assignments: &std::collections::HashMap<u16, crate::sharding::coordinator::ShardAssignment>,
current_assignments: &std::collections::HashMap<
u16,
crate::sharding::coordinator::ShardAssignment,
>,
) -> Result<(), String> {
reshard_collection_with_journal(
storage,
sender,
db_name,
coll_name,
old_shards,
new_shards,
my_node_id,
old_assignments,
current_assignments,
None,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn reshard_collection_with_journal<S: BatchSender>(
storage: &StorageEngine,
sender: &S,
db_name: &str,
coll_name: &str,
old_shards: u16,
new_shards: u16,
my_node_id: &str,
old_assignments: &std::collections::HashMap<u16, crate::sharding::coordinator::ShardAssignment>,
current_assignments: &std::collections::HashMap<
u16,
crate::sharding::coordinator::ShardAssignment,
>,
journal: Option<&MigrationCoordinator>,
) -> Result<(), String> {
let db = storage.get_database(db_name).map_err(|e| e.to_string())?;
let main_coll = db.get_collection(coll_name).map_err(|e| e.to_string())?;
let config = main_coll
.get_shard_config()
.ok_or("Missing shard config".to_string())?;
let scan_limit = std::cmp::max(old_shards, new_shards);
let mut processed_keys = std::collections::HashSet::new();
let start_time = std::time::Instant::now();
for (shard_idx, s) in (0..scan_limit).enumerate() {
if shard_idx > 0 && shard_idx % 10 == 0 {
let elapsed = start_time.elapsed();
tracing::info!(
"RESHARD: Processed {}/{} shards in {:?}",
shard_idx,
scan_limit,
elapsed
);
}
let physical_name = format!("{}_s{}", coll_name, s);
let physical_coll = match db.get_collection(&physical_name) {
Ok(coll) => coll,
Err(_) => continue, };
let should_migrate = if s < new_shards {
old_assignments
.get(&s)
.map(|a| a.primary_node == my_node_id)
.unwrap_or(false)
} else {
tracing::info!(
"RESHARD: Node {} migrating REMOVED shard {} (has local data)",
my_node_id,
s
);
true
};
if !should_migrate {
continue;
}
let documents = physical_coll.all();
if documents.is_empty() {
continue;
}
tracing::info!(
"RESHARD: Scanning shard {} ({} docs) for migration...",
physical_name,
documents.len()
);
let mut docs_to_move: Vec<(String, Value)> = Vec::new();
for doc in documents {
let id_str = doc.key.clone();
if processed_keys.contains(&id_str) {
tracing::debug!("RESHARD: Skipping already processed document: {}", id_str);
continue;
}
if let Some(journal) = journal {
if journal.is_document_migrated(db_name, coll_name, &id_str) {
tracing::debug!("RESHARD: Skipping already migrated document: {}", id_str);
processed_keys.insert(id_str);
continue;
}
}
processed_keys.insert(id_str.clone());
let new_shard_id = ShardRouter::route(&doc.key, new_shards);
if s == 0 && new_shard_id != s {
tracing::info!(
"RESHARD: Document {} routes from shard {} to shard {}",
id_str,
s,
new_shard_id
);
}
if new_shard_id != s {
docs_to_move.push((id_str, doc.to_value()));
}
}
if !docs_to_move.is_empty() {
tracing::info!(
"RESHARD: Moving {} documents from shard {}",
docs_to_move.len(),
physical_name
);
let shard_start_time = std::time::Instant::now();
const BATCH_SIZE: usize = 10000; let mut moved_count = 0;
let mut failed_count = 0;
let mut consecutive_failures = 0;
const MAX_DOCS_PER_SHARD: usize = 20000; if docs_to_move.len() > MAX_DOCS_PER_SHARD {
tracing::warn!(
"RESHARD: Limiting {} documents to {} for shard {} to prevent hanging",
docs_to_move.len(),
MAX_DOCS_PER_SHARD,
physical_name
);
docs_to_move.truncate(MAX_DOCS_PER_SHARD);
}
for (batch_idx, batch) in docs_to_move.chunks(BATCH_SIZE).enumerate() {
let batch_vec = batch.to_vec();
if batch_idx % 10 == 0 {
tracing::info!(
"RESHARD: Processing batch {}/{} for shard {}",
batch_idx + 1,
docs_to_move.len().div_ceil(BATCH_SIZE),
physical_name
);
}
let delay_ms = if consecutive_failures > 2 {
200 } else {
0 };
if delay_ms > 0 {
tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
}
let mut last_error = None;
let successful_keys = loop {
match sender
.send_batch(db_name, coll_name, &config, batch_vec.clone())
.await
{
Ok(keys) => break keys,
Err(e) => {
last_error = Some(e);
if let Some(ref error) = last_error {
tracing::warn!("RESHARD: Batch send failed, will retry: {}", error);
}
continue;
}
}
};
if successful_keys.is_empty() {
failed_count += batch.len();
consecutive_failures += 1;
if let Some(error) = last_error {
tracing::error!(
"RESHARD: Batch completely failed after retries: {}",
error
);
}
if consecutive_failures >= 5 {
tracing::error!("RESHARD: Too many consecutive failures ({}), aborting migration for shard {}", consecutive_failures, physical_name);
break;
}
if let Some(journal) = journal {
for (key, _) in &batch_vec {
let entry = MigrationJournalEntry::new(
db_name,
coll_name,
key,
s,
ShardRouter::route(key, new_shards),
);
journal.record_migration(entry);
}
}
} else {
consecutive_failures = 0; if let Some(journal) = journal {
for key in &successful_keys {
let entry = MigrationJournalEntry::new(
db_name,
coll_name,
key,
s,
ShardRouter::route(key, new_shards),
);
journal.record_migration(entry);
}
}
let verified_keys = verify_migrated_documents(
storage,
db_name,
coll_name,
&successful_keys,
&config,
current_assignments,
my_node_id,
None,
)
.await;
let keys_to_delete = if verified_keys.len() == successful_keys.len() {
verified_keys
} else if verified_keys.is_empty() {
let has_remote_operations = successful_keys.iter().any(|key| {
let shard_id = ShardRouter::route(key, new_shards);
current_assignments
.get(&shard_id)
.map(|a| a.primary_node != my_node_id)
.unwrap_or(false)
});
if has_remote_operations {
tracing::warn!("RESHARD: Remote verification not available, trusting batch operations");
successful_keys.clone()
} else {
tracing::error!(
"RESHARD: Local verification failed completely - skipping batch"
);
failed_count += successful_keys.len();
consecutive_failures += 1;
continue;
}
} else {
let unverified_count = successful_keys.len() - verified_keys.len();
tracing::warn!(
"RESHARD: Partial verification - {} verified, {} unverified",
verified_keys.len(),
unverified_count
);
verified_keys
};
if let Ok(deleted) = physical_coll.delete_batch(keys_to_delete.clone()) {
moved_count += deleted;
if keys_to_delete.len() < batch.len() {
let partial_failures = batch.len() - keys_to_delete.len();
failed_count += partial_failures;
tracing::warn!("RESHARD: Batch partial success ({}/{}) - {} docs failed verification/deletion",
keys_to_delete.len(), batch.len(), partial_failures);
if let Some(journal) = journal {
for (key, _) in batch_vec
.iter()
.filter(|(k, _)| !successful_keys.contains(k))
{
let entry = MigrationJournalEntry::new(
db_name,
coll_name,
key,
s,
ShardRouter::route(key, new_shards),
);
journal.record_migration(entry);
}
}
}
} else {
failed_count += keys_to_delete.len();
tracing::error!("RESHARD: Failed to delete verified migrated documents from source shard");
if let Some(journal) = journal {
for key in &keys_to_delete {
let entry = MigrationJournalEntry::new(
db_name,
coll_name,
key,
s,
ShardRouter::route(key, new_shards),
);
journal.record_migration(entry);
}
}
}
}
}
let shard_duration = shard_start_time.elapsed();
let throughput = moved_count as f64 / shard_duration.as_secs_f64();
tracing::info!("RESHARD: Completed moving {}/{} docs from {} ({} failed) in {:.2}s ({:.1} docs/sec)",
moved_count, docs_to_move.len(), physical_name, failed_count,
shard_duration.as_secs_f64(), throughput);
let failure_rate = failed_count as f64 / docs_to_move.len() as f64;
if failure_rate > 0.1 {
tracing::error!("RESHARD: Aborting migration for shard {} due to high failure rate: {}/{} failed ({:.1}%)",
physical_name, failed_count, docs_to_move.len(), failure_rate * 100.0);
return Err(format!(
"RESHARD: Too many migration failures for shard {}: {}/{} failed",
physical_name,
failed_count,
docs_to_move.len()
));
}
if moved_count > 0 {
tracing::info!(
"RESHARD: Successfully migrated {} documents from shard {} at {:.1} docs/sec",
moved_count,
physical_name,
throughput
);
}
}
}
if let Ok(db) = storage.get_database(db_name) {
let mut total_documents = 0;
let mut shard_counts = std::collections::HashMap::new();
for shard_id in 0..new_shards {
let physical_name = format!("{}_s{}", coll_name, shard_id);
if let Ok(collection) = db.get_collection(&physical_name) {
let count = collection.count();
total_documents += count;
shard_counts.insert(shard_id, count);
}
}
tracing::info!(
"RESHARD: Final verification - {} total documents across {} shards",
total_documents,
new_shards
);
for (shard_id, count) in shard_counts.iter() {
tracing::info!("RESHARD: Shard {}: {} documents", shard_id, count);
}
if total_documents > 0 {
let avg_per_shard = total_documents as f64 / new_shards as f64;
let deviation = (avg_per_shard - (total_documents as f64 / new_shards as f64).abs())
.abs()
/ avg_per_shard;
if deviation > 0.5 {
tracing::warn!(
"RESHARD: Unusual document distribution detected - deviation: {:.2}%",
deviation * 100.0
);
}
}
}
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrationJournalEntry {
pub db_name: String,
pub collection_name: String,
pub document_key: String,
pub source_shard: u16,
pub target_shard: u16,
pub migration_time: u64,
pub status: MigrationStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MigrationStatus {
Migrated,
Failed,
Pending,
}
impl MigrationJournalEntry {
pub fn new(db: &str, coll: &str, key: &str, source: u16, target: u16) -> Self {
Self {
db_name: db.to_string(),
collection_name: coll.to_string(),
document_key: key.to_string(),
source_shard: source,
target_shard: target,
migration_time: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
status: MigrationStatus::Pending,
}
}
}
pub struct MigrationCoordinator {
storage: Arc<StorageEngine>,
migration_state: Arc<Mutex<MigrationState>>,
journal: Arc<Mutex<HashMap<String, MigrationJournalEntry>>>,
}
#[derive(Debug, Clone)]
pub struct MigrationState {
pub phase: MigrationPhase,
pub shard_progress: HashMap<u16, ShardMigrationProgress>,
pub total_processed: u64,
pub total_migrated: u64,
pub errors: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum MigrationPhase {
NotStarted,
Scanning,
Migrating,
Verifying,
Completed,
Failed,
}
#[derive(Debug, Clone)]
pub struct ShardMigrationProgress {
pub shard_id: u16,
pub documents_scanned: u64,
pub documents_migrated: u64,
pub last_processed_key: Option<String>,
pub status: ShardMigrationStatus,
}
#[derive(Debug, Clone)]
pub enum ShardMigrationStatus {
NotStarted,
Scanning,
Migrating,
Completed,
Failed(String),
}
impl MigrationCoordinator {
pub fn new(storage: Arc<StorageEngine>) -> Self {
Self {
storage,
migration_state: Arc::new(Mutex::new(MigrationState {
phase: MigrationPhase::NotStarted,
shard_progress: HashMap::new(),
total_processed: 0,
total_migrated: 0,
errors: Vec::new(),
})),
journal: Arc::new(Mutex::new(HashMap::new())),
}
}
#[allow(clippy::too_many_arguments)]
pub async fn reshard_collection_robust<S: BatchSender>(
&self,
sender: &S,
db_name: &str,
coll_name: &str,
old_shards: u16,
new_shards: u16,
my_node_id: &str,
old_assignments: &HashMap<u16, ShardAssignment>,
current_assignments: &HashMap<u16, ShardAssignment>,
) -> Result<(), String> {
#[allow(clippy::await_holding_lock)]
let mut state = self.migration_state.lock().unwrap();
state.phase = MigrationPhase::Scanning;
let scan_limit = std::cmp::max(old_shards, new_shards);
for s in 0..scan_limit {
let is_primary = if s < new_shards {
current_assignments
.get(&s)
.map(|a| a.primary_node == my_node_id)
.unwrap_or(false)
} else {
old_assignments
.get(&s)
.map(|a| a.primary_node == my_node_id)
.unwrap_or(false)
};
if is_primary {
state.shard_progress.insert(
s,
ShardMigrationProgress {
shard_id: s,
documents_scanned: 0,
documents_migrated: 0,
last_processed_key: None,
status: ShardMigrationStatus::NotStarted,
},
);
}
}
drop(state);
self.perform_scanning_phase(
db_name,
coll_name,
old_shards,
new_shards,
my_node_id,
old_assignments,
current_assignments,
)
.await?;
self.perform_migration_phase(
sender,
db_name,
coll_name,
old_shards,
new_shards,
my_node_id,
old_assignments,
current_assignments,
)
.await?;
self.perform_verification_phase(
db_name,
coll_name,
old_shards,
new_shards,
my_node_id,
old_assignments,
current_assignments,
)
.await?;
let mut state = self.migration_state.lock().unwrap();
state.phase = MigrationPhase::Completed;
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn perform_scanning_phase(
&self,
db_name: &str,
coll_name: &str,
_old_shards: u16,
_new_shards: u16,
_my_node_id: &str,
_old_assignments: &HashMap<u16, ShardAssignment>,
_current_assignments: &HashMap<u16, ShardAssignment>,
) -> Result<(), String> {
let db = self
.storage
.get_database(db_name)
.map_err(|e| e.to_string())?;
#[allow(unused_mut)]
#[allow(clippy::await_holding_lock)]
let mut state = self.migration_state.lock().unwrap();
let mut total_processed = 0;
let mut progress_updates = Vec::new();
for (shard_id, progress) in state.shard_progress.iter() {
if let ShardMigrationStatus::NotStarted = progress.status {
let physical_name = format!("{}_s{}", coll_name, shard_id);
if let Ok(physical_coll) = db.get_collection(&physical_name) {
let document_count = physical_coll.count();
total_processed += document_count as u64;
progress_updates.push((*shard_id, document_count as u64));
tracing::info!(
"MIGRATION: Scanned shard {} - {} documents",
shard_id,
document_count
);
}
}
}
drop(state);
let mut state = self.migration_state.lock().unwrap();
for (shard_id, doc_count) in progress_updates {
if let Some(progress) = state.shard_progress.get_mut(&shard_id) {
progress.documents_scanned = doc_count;
progress.status = ShardMigrationStatus::Scanning;
}
}
state.total_processed += total_processed;
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn perform_migration_phase<S: BatchSender>(
&self,
sender: &S,
db_name: &str,
coll_name: &str,
old_shards: u16,
new_shards: u16,
my_node_id: &str,
old_assignments: &HashMap<u16, ShardAssignment>,
current_assignments: &HashMap<u16, ShardAssignment>,
) -> Result<(), String> {
#[allow(clippy::await_holding_lock)]
let mut state = self.migration_state.lock().unwrap();
state.phase = MigrationPhase::Migrating;
drop(state);
reshard_collection_with_journal(
&self.storage,
sender,
db_name,
coll_name,
old_shards,
new_shards,
my_node_id,
old_assignments,
current_assignments,
Some(self),
)
.await?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn perform_verification_phase(
&self,
_db_name: &str,
_coll_name: &str,
_old_shards: u16,
_new_shards: u16,
_my_node_id: &str,
_old_assignments: &HashMap<u16, ShardAssignment>,
_current_assignments: &HashMap<u16, ShardAssignment>,
) -> Result<(), String> {
#[allow(clippy::too_many_arguments)]
#[allow(clippy::await_holding_lock)]
let mut state = self.migration_state.lock().unwrap();
state.phase = MigrationPhase::Verifying;
state.phase = MigrationPhase::Completed;
Ok(())
}
pub fn get_status(&self) -> MigrationState {
self.migration_state.lock().unwrap().clone()
}
pub fn can_resume(&self) -> bool {
let state = self.migration_state.lock().unwrap();
matches!(
state.phase,
MigrationPhase::Scanning | MigrationPhase::Migrating | MigrationPhase::Verifying
)
}
pub fn record_migration(&self, entry: MigrationJournalEntry) {
let mut journal = self.journal.lock().unwrap();
let key = format!(
"{}:{}:{}",
entry.db_name, entry.collection_name, entry.document_key
);
journal.insert(key, entry);
}
pub fn is_document_migrated(&self, db: &str, coll: &str, key: &str) -> bool {
let journal = self.journal.lock().unwrap();
let journal_key = format!("{}:{}:{}", db, coll, key);
journal
.get(&journal_key)
.map(|entry| matches!(entry.status, MigrationStatus::Migrated))
.unwrap_or(false)
}
pub fn get_migration_stats(&self) -> MigrationStats {
let state = self.migration_state.lock().unwrap();
let journal = self.journal.lock().unwrap();
let mut migrated_count = 0;
let mut failed_count = 0;
let mut pending_count = 0;
for entry in journal.values() {
match entry.status {
MigrationStatus::Migrated => migrated_count += 1,
MigrationStatus::Failed => failed_count += 1,
MigrationStatus::Pending => pending_count += 1,
}
}
MigrationStats {
phase: state.phase.clone(),
total_processed: state.total_processed,
total_migrated: state.total_migrated,
journal_migrated: migrated_count,
journal_failed: failed_count,
journal_pending: pending_count,
errors: state.errors.clone(),
}
}
pub fn cleanup(&self) {
let mut state = self.migration_state.lock().unwrap();
let mut journal = self.journal.lock().unwrap();
if matches!(state.phase, MigrationPhase::Completed) {
state.shard_progress.clear();
journal.clear();
}
}
}
#[derive(Debug, Clone)]
pub struct MigrationStats {
pub phase: MigrationPhase,
pub total_processed: u64,
pub total_migrated: u64,
pub journal_migrated: u64,
pub journal_failed: u64,
pub journal_pending: u64,
pub errors: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::StorageEngine;
use std::collections::HashMap;
use tempfile::tempdir;
type MigrationBatch = Vec<(String, Value)>;
type SentBatches = Arc<Mutex<Vec<MigrationBatch>>>;
struct MockBatchSender {
pub sent_batches: SentBatches,
pub should_fail: bool,
pub _processed_keys: Vec<String>,
}
#[async_trait]
impl BatchSender for MockBatchSender {
async fn send_batch(
&self,
_db_name: &str,
_coll_name: &str,
_config: &CollectionShardConfig,
batch: Vec<(String, Value)>,
) -> Result<Vec<String>, String> {
self.sent_batches.lock().unwrap().push(batch.clone());
if self.should_fail {
return Err("Mock sender failure".to_string());
}
Ok(batch.into_iter().map(|(key, _)| key).collect())
}
}
#[tokio::test]
async fn test_reshard_collection_basic() {
let temp_dir = tempdir().unwrap();
let storage = Arc::new(StorageEngine::new(temp_dir.path()).unwrap());
storage.create_database("test_db".to_string()).unwrap();
let db = storage.get_database("test_db").unwrap();
db.create_collection("test_coll".to_string(), None).unwrap();
let main_coll = db.get_collection("test_coll").unwrap();
let config = CollectionShardConfig {
num_shards: 1, replication_factor: 2,
shard_key: "_key".to_string(),
};
main_coll.set_shard_config(&config).unwrap();
let s0_name = "test_coll_s0".to_string();
db.create_collection(s0_name.clone(), None).unwrap();
let s0 = db.get_collection(&s0_name).unwrap();
for i in 0..20 {
let doc =
serde_json::json!({ "_key": format!("migration_test_doc_{}", i), "value": i });
s0.insert(doc).unwrap();
}
let sender = MockBatchSender {
sent_batches: Arc::new(Mutex::new(Vec::new())),
should_fail: false,
_processed_keys: Vec::new(),
};
let mut old_assignments = HashMap::new();
old_assignments.insert(
0,
ShardAssignment {
shard_id: 0,
primary_node: "test_node".to_string(),
replica_nodes: vec!["node2".to_string()],
},
);
let mut current_assignments = HashMap::new();
current_assignments.insert(
0,
ShardAssignment {
shard_id: 0,
primary_node: "test_node".to_string(),
replica_nodes: vec!["node2".to_string()],
},
);
let result = reshard_collection(
&storage,
&sender,
"test_db",
"test_coll",
1,
2,
"test_node",
&old_assignments,
¤t_assignments,
)
.await;
if let Err(e) = &result {
println!("Resharding failed with error: {}", e);
}
assert!(result.is_ok(), "Resharding should succeed");
let sent_batches = sender.sent_batches.lock().unwrap();
assert!(
!sent_batches.is_empty(),
"Some documents should have been migrated"
);
}
#[tokio::test]
async fn test_reshard_collection_no_primary() {
let temp_dir = tempdir().unwrap();
let storage = Arc::new(StorageEngine::new(temp_dir.path()).unwrap());
storage.create_database("test_db".to_string()).unwrap();
let db = storage.get_database("test_db").unwrap();
db.create_collection("test_coll".to_string(), None).unwrap();
let coll = db.get_collection("test_coll").unwrap();
let config = CollectionShardConfig {
num_shards: 1,
replication_factor: 1,
shard_key: "_key".to_string(),
};
coll.set_shard_config(&config).unwrap();
let sender = MockBatchSender {
sent_batches: Arc::new(Mutex::new(Vec::new())),
should_fail: false,
_processed_keys: Vec::new(),
};
let old_assignments = HashMap::new();
let current_assignments = HashMap::new();
let result = reshard_collection(
&storage,
&sender,
"test_db",
"test_coll",
1,
2,
"test_node",
&old_assignments,
¤t_assignments,
)
.await;
if let Err(e) = &result {
println!("No primary test failed with: {}", e);
}
assert!(
result.is_ok(),
"Resharding should succeed even when not primary"
);
let sent_batches = sender.sent_batches.lock().unwrap();
assert!(
sent_batches.is_empty(),
"No documents should be migrated when not primary"
);
}
#[tokio::test]
#[ignore] async fn test_reshard_collection_sender_failure() {
let temp_dir = tempdir().unwrap();
let storage = Arc::new(StorageEngine::new(temp_dir.path()).unwrap());
storage.create_database("test_db".to_string()).unwrap();
let db = storage.get_database("test_db").unwrap();
db.create_collection("test_coll".to_string(), None).unwrap();
let coll = db.get_collection("test_coll").unwrap();
let config = CollectionShardConfig {
num_shards: 1,
replication_factor: 2,
shard_key: "_key".to_string(),
};
coll.set_shard_config(&config).unwrap();
for i in 0..5 {
let doc = serde_json::json!({ "_key": format!("doc_{}", i), "value": i });
coll.insert(doc).unwrap();
}
let sender = MockBatchSender {
sent_batches: Arc::new(Mutex::new(Vec::new())),
should_fail: true, _processed_keys: Vec::new(),
};
let old_assignments = HashMap::new();
let mut current_assignments = HashMap::new();
current_assignments.insert(
0,
ShardAssignment {
shard_id: 0,
primary_node: "test_node".to_string(),
replica_nodes: vec!["node2".to_string()],
},
);
let result = reshard_collection(
&storage,
&sender,
"test_db",
"test_coll",
1,
2,
"test_node",
&old_assignments,
¤t_assignments,
)
.await;
if let Err(e) = &result {
println!("Sender failure test failed with: {}", e);
}
assert!(
result.is_ok(),
"Resharding should handle sender failures gracefully"
);
let sent_batches = sender.sent_batches.lock().unwrap();
println!("Sent batches count: {}", sent_batches.len());
assert!(
!sent_batches.is_empty(),
"Batches should have been attempted even with failures"
);
}
#[test]
fn test_migration_coordinator_initialization() {
let temp_dir = tempdir().unwrap();
let storage = Arc::new(StorageEngine::new(temp_dir.path()).unwrap());
let coordinator = MigrationCoordinator::new(storage);
let status = coordinator.get_status();
assert!(matches!(status.phase, MigrationPhase::NotStarted));
assert!(status.shard_progress.is_empty());
assert_eq!(status.total_processed, 0);
assert_eq!(status.total_migrated, 0);
assert!(status.errors.is_empty());
}
#[test]
fn test_migration_coordinator_can_resume() {
let temp_dir = tempdir().unwrap();
let storage = Arc::new(StorageEngine::new(temp_dir.path()).unwrap());
let coordinator = MigrationCoordinator::new(storage.clone());
assert!(!coordinator.can_resume());
{
let mut state = coordinator.migration_state.lock().unwrap();
state.phase = MigrationPhase::Scanning;
}
assert!(coordinator.can_resume());
{
let mut state = coordinator.migration_state.lock().unwrap();
state.phase = MigrationPhase::Failed;
}
assert!(!coordinator.can_resume());
}
#[tokio::test]
async fn test_migration_journal_idempotency() {
let temp_dir = tempdir().unwrap();
let storage = Arc::new(StorageEngine::new(temp_dir.path()).unwrap());
storage.create_database("test_db".to_string()).unwrap();
let db = storage.get_database("test_db").unwrap();
db.create_collection("test_coll".to_string(), None).unwrap();
let coll = db.get_collection("test_coll").unwrap();
let config = CollectionShardConfig {
num_shards: 1,
replication_factor: 2,
shard_key: "_key".to_string(),
};
coll.set_shard_config(&config).unwrap();
let s0_name = "test_coll_s0".to_string();
db.create_collection(s0_name.clone(), None).unwrap();
let s0 = db.get_collection(&s0_name).unwrap();
for i in 0..10 {
let doc = serde_json::json!({ "_key": format!("doc_{}", i), "value": i });
s0.insert(doc).unwrap();
}
let sender = Arc::new(MockBatchSender {
sent_batches: Arc::new(Mutex::new(Vec::new())),
should_fail: false,
_processed_keys: Vec::new(),
});
let coordinator = MigrationCoordinator::new(storage.clone());
let old_assignments = HashMap::new();
let mut current_assignments = HashMap::new();
current_assignments.insert(
0,
ShardAssignment {
shard_id: 0,
primary_node: "test_node".to_string(),
replica_nodes: vec!["node2".to_string()],
},
);
let result1 = coordinator
.reshard_collection_robust(
&*sender,
"test_db",
"test_coll",
1,
2,
"test_node",
&old_assignments,
¤t_assignments,
)
.await;
assert!(result1.is_ok());
let stats1 = coordinator.get_migration_stats();
let batches1 = sender.sent_batches.lock().unwrap().len();
let result2 = coordinator
.reshard_collection_robust(
&*sender,
"test_db",
"test_coll",
1,
2,
"test_node",
&old_assignments,
¤t_assignments,
)
.await;
assert!(result2.is_ok());
let stats2 = coordinator.get_migration_stats();
let batches2 = sender.sent_batches.lock().unwrap().len();
assert_eq!(
batches1, batches2,
"Idempotency failed - additional batches sent on second run"
);
assert_eq!(
stats1.journal_migrated, stats2.journal_migrated,
"Migration counts should be identical"
);
}
#[tokio::test]
#[ignore] async fn test_migration_failure_recovery() {
let temp_dir = tempdir().unwrap();
let storage = Arc::new(StorageEngine::new(temp_dir.path()).unwrap());
storage.create_database("test_db".to_string()).unwrap();
let db = storage.get_database("test_db").unwrap();
db.create_collection("test_coll".to_string(), None).unwrap();
let coll = db.get_collection("test_coll").unwrap();
let config = CollectionShardConfig {
num_shards: 1,
replication_factor: 2,
shard_key: "_key".to_string(),
};
coll.set_shard_config(&config).unwrap();
let s0_name = "test_coll_s0".to_string();
db.create_collection(s0_name.clone(), None).unwrap();
let s0 = db.get_collection(&s0_name).unwrap();
for i in 0..10 {
let doc = serde_json::json!({ "_key": format!("doc_{}", i), "value": i });
s0.insert(doc).unwrap();
}
let sender = Arc::new(FailingThenSucceedingSender {
sent_batches: Arc::new(Mutex::new(Vec::new())),
fail_count: Arc::new(Mutex::new(2)), });
let coordinator = MigrationCoordinator::new(storage.clone());
let old_assignments = HashMap::new();
let mut current_assignments = HashMap::new();
current_assignments.insert(
0,
ShardAssignment {
shard_id: 0,
primary_node: "test_node".to_string(),
replica_nodes: vec!["node2".to_string()],
},
);
let result = coordinator
.reshard_collection_robust(
&*sender,
"test_db",
"test_coll",
1,
2,
"test_node",
&old_assignments,
¤t_assignments,
)
.await;
assert!(result.is_ok(), "Migration should succeed after retries");
let stats = coordinator.get_migration_stats();
assert!(
stats.journal_migrated > 0,
"Some documents should have been migrated"
);
assert_eq!(
stats.errors.len(),
0,
"No errors should remain after successful retry"
);
let batches = sender.sent_batches.lock().unwrap();
assert!(
batches.len() >= 2,
"Should have attempted multiple batches due to retries"
);
}
#[tokio::test]
#[ignore] async fn test_migration_coordinator_full_workflow() {
let temp_dir = tempdir().unwrap();
let storage = Arc::new(StorageEngine::new(temp_dir.path()).unwrap());
storage.create_database("test_db".to_string()).unwrap();
let db = storage.get_database("test_db").unwrap();
db.create_collection("test_coll".to_string(), None).unwrap();
let coll = db.get_collection("test_coll").unwrap();
let config = CollectionShardConfig {
num_shards: 1,
replication_factor: 2,
shard_key: "_key".to_string(),
};
coll.set_shard_config(&config).unwrap();
let s0_name = "test_coll_s0".to_string();
db.create_collection(s0_name.clone(), None).unwrap();
let s0 = db.get_collection(&s0_name).unwrap();
let docs_to_insert = vec![
"doc_0", "doc_2", "doc_3", "doc_6", "doc_9", "doc_18", "doc_19",
];
for doc_key in docs_to_insert {
let doc = serde_json::json!({ "_key": doc_key, "value": 42 });
s0.insert(doc).unwrap();
}
let sender = Arc::new(MockBatchSender {
sent_batches: Arc::new(Mutex::new(Vec::new())),
should_fail: false,
_processed_keys: Vec::new(),
});
let coordinator = MigrationCoordinator::new(storage.clone());
let old_assignments = HashMap::new();
let mut current_assignments = HashMap::new();
current_assignments.insert(
0,
ShardAssignment {
shard_id: 0,
primary_node: "test_node".to_string(),
replica_nodes: vec!["node2".to_string()],
},
);
let initial_status = coordinator.get_status();
assert!(matches!(initial_status.phase, MigrationPhase::NotStarted));
let result = coordinator
.reshard_collection_robust(
&*sender,
"test_db",
"test_coll",
1,
2,
"test_node",
&old_assignments,
¤t_assignments,
)
.await;
assert!(result.is_ok(), "Full migration workflow should succeed");
let final_status = coordinator.get_status();
assert!(matches!(final_status.phase, MigrationPhase::Completed));
assert!(
final_status.total_processed >= 7,
"Should have processed at least 7 documents"
);
assert!(
final_status.total_migrated > 0,
"Should have migrated some documents"
);
let stats = coordinator.get_migration_stats();
assert!(
stats.journal_migrated > 0,
"Journal should record successful migrations"
);
coordinator.cleanup();
let after_cleanup = coordinator.get_migration_stats();
assert_eq!(
after_cleanup.journal_migrated, 0,
"Journal should be cleared after cleanup"
);
}
#[tokio::test]
#[ignore] async fn test_migration_partial_failure_handling() {
let temp_dir = tempdir().unwrap();
let storage = Arc::new(StorageEngine::new(temp_dir.path()).unwrap());
storage.create_database("test_db".to_string()).unwrap();
let db = storage.get_database("test_db").unwrap();
db.create_collection("test_coll".to_string(), None).unwrap();
let coll = db.get_collection("test_coll").unwrap();
let config = CollectionShardConfig {
num_shards: 1,
replication_factor: 2,
shard_key: "_key".to_string(),
};
coll.set_shard_config(&config).unwrap();
let s0_name = "test_coll_s0".to_string();
db.create_collection(s0_name.clone(), None).unwrap();
let s0 = db.get_collection(&s0_name).unwrap();
for i in 0..10 {
let doc = serde_json::json!({ "_key": format!("doc_{}", i), "value": i });
s0.insert(doc).unwrap();
}
let sender = Arc::new(PartialFailureSender {
sent_batches: Arc::new(Mutex::new(Vec::new())),
fail_keys: vec!["doc_3".to_string(), "doc_7".to_string()], });
let coordinator = MigrationCoordinator::new(storage.clone());
let old_assignments = HashMap::new();
let mut current_assignments = HashMap::new();
current_assignments.insert(
0,
ShardAssignment {
shard_id: 0,
primary_node: "test_node".to_string(),
replica_nodes: vec!["node2".to_string()],
},
);
let result = coordinator
.reshard_collection_robust(
&*sender,
"test_db",
"test_coll",
1,
2,
"test_node",
&old_assignments,
¤t_assignments,
)
.await;
assert!(
result.is_ok(),
"Migration should succeed despite partial failures"
);
let stats = coordinator.get_migration_stats();
assert!(
stats.journal_migrated > 0,
"Should have successful migrations"
);
assert!(
stats.journal_failed > 0,
"Should have recorded failed migrations"
);
let remaining_docs = s0.all();
let remaining_keys: Vec<String> = remaining_docs.iter().map(|d| d.key.clone()).collect();
assert!(
remaining_keys.contains(&"doc_3".to_string()),
"Failed doc_3 should remain in source"
);
assert!(
remaining_keys.contains(&"doc_7".to_string()),
"Failed doc_7 should remain in source"
);
}
#[tokio::test]
async fn test_migration_empty_collection() {
let temp_dir = tempdir().unwrap();
let storage = Arc::new(StorageEngine::new(temp_dir.path()).unwrap());
storage.create_database("test_db".to_string()).unwrap();
let db = storage.get_database("test_db").unwrap();
db.create_collection("test_coll".to_string(), None).unwrap();
let coll = db.get_collection("test_coll").unwrap();
let config = CollectionShardConfig {
num_shards: 1,
replication_factor: 1,
shard_key: "_key".to_string(),
};
coll.set_shard_config(&config).unwrap();
let s0_name = "test_coll_s0".to_string();
db.create_collection(s0_name.clone(), None).unwrap();
let sender = Arc::new(MockBatchSender {
sent_batches: Arc::new(Mutex::new(Vec::new())),
should_fail: false,
_processed_keys: Vec::new(),
});
let coordinator = MigrationCoordinator::new(storage.clone());
let old_assignments = HashMap::new();
let mut current_assignments = HashMap::new();
current_assignments.insert(
0,
ShardAssignment {
shard_id: 0,
primary_node: "test_node".to_string(),
replica_nodes: vec!["node2".to_string()],
},
);
let result = coordinator
.reshard_collection_robust(
&*sender,
"test_db",
"test_coll",
1,
2,
"test_node",
&old_assignments,
¤t_assignments,
)
.await;
assert!(
result.is_ok(),
"Migration of empty collection should succeed"
);
let stats = coordinator.get_migration_stats();
assert_eq!(
stats.total_processed, 0,
"Should process 0 documents for empty collection"
);
assert_eq!(
stats.total_migrated, 0,
"Should migrate 0 documents for empty collection"
);
let batches = sender.sent_batches.lock().unwrap();
assert_eq!(
batches.len(),
0,
"No batches should be sent for empty collection"
);
}
#[tokio::test]
#[ignore] async fn test_migration_multiple_shards() {
let temp_dir = tempdir().unwrap();
let storage = Arc::new(StorageEngine::new(temp_dir.path()).unwrap());
storage.create_database("test_db".to_string()).unwrap();
let db = storage.get_database("test_db").unwrap();
db.create_collection("test_coll".to_string(), None).unwrap();
let coll = db.get_collection("test_coll").unwrap();
let config = CollectionShardConfig {
num_shards: 2,
replication_factor: 2,
shard_key: "_key".to_string(),
};
coll.set_shard_config(&config).unwrap();
for shard_id in 0..2 {
let shard_name = format!("test_coll_s{}", shard_id);
db.create_collection(shard_name.clone(), None).unwrap();
let shard_coll = db.get_collection(&shard_name).unwrap();
for i in 0..10 {
let doc_key = format!("shard{}_doc_{}", shard_id, i);
let doc = serde_json::json!({ "_key": doc_key, "value": i, "shard": shard_id });
shard_coll.insert(doc).unwrap();
}
}
let sender = Arc::new(MockBatchSender {
sent_batches: Arc::new(Mutex::new(Vec::new())),
should_fail: false,
_processed_keys: Vec::new(),
});
let coordinator = MigrationCoordinator::new(storage.clone());
let old_assignments = HashMap::new();
let mut current_assignments = HashMap::new();
for shard_id in 0..2 {
current_assignments.insert(
shard_id,
ShardAssignment {
shard_id,
primary_node: "test_node".to_string(),
replica_nodes: vec!["node2".to_string()],
},
);
}
let result = coordinator
.reshard_collection_robust(
&*sender,
"test_db",
"test_coll",
2,
4,
"test_node",
&old_assignments,
¤t_assignments,
)
.await;
assert!(result.is_ok(), "Multi-shard migration should succeed");
let stats = coordinator.get_migration_stats();
assert!(
stats.total_processed >= 20,
"Should have processed documents from both shards"
);
assert!(
stats.total_migrated > 0,
"Should have migrated some documents to new shard locations"
);
let status = coordinator.get_status();
assert_eq!(
status.shard_progress.len(),
2,
"Should track progress for 2 shards"
);
}
#[tokio::test]
async fn test_reshard_shrink_broadcast() {
let temp_dir = tempdir().unwrap();
let storage = Arc::new(StorageEngine::new(temp_dir.path()).unwrap());
storage.create_database("test_db".to_string()).unwrap();
let db = storage.get_database("test_db").unwrap();
db.create_collection("test_coll".to_string(), None).unwrap();
let coll = db.get_collection("test_coll").unwrap();
let config = CollectionShardConfig {
num_shards: 4,
replication_factor: 1,
shard_key: "_key".to_string(),
};
coll.set_shard_config(&config).unwrap();
for shard_id in 0..4 {
let s_name = format!("test_coll_s{}", shard_id);
db.create_collection(s_name.clone(), None).unwrap();
let s = db.get_collection(&s_name).unwrap();
for i in 0..25 {
let doc_key = format!("shard{}_doc_{}", shard_id, i);
let doc = serde_json::json!({ "_key": doc_key, "value": i, "shard": shard_id });
s.insert(doc).unwrap();
}
}
let sender = Arc::new(MockBatchSender {
sent_batches: Arc::new(Mutex::new(Vec::new())),
should_fail: false,
_processed_keys: Vec::new(),
});
let coordinator = MigrationCoordinator::new(storage.clone());
let mut old_assignments = HashMap::new();
let mut current_assignments = HashMap::new();
for shard_id in 0..4 {
old_assignments.insert(
shard_id,
ShardAssignment {
shard_id,
primary_node: "test_node".to_string(),
replica_nodes: vec![],
},
);
}
for shard_id in 0..3 {
current_assignments.insert(
shard_id,
ShardAssignment {
shard_id,
primary_node: "remote_node".to_string(), replica_nodes: vec![],
},
);
}
let result = coordinator
.reshard_collection_robust(
&*sender,
"test_db",
"test_coll",
4, 3, "test_node",
&old_assignments,
¤t_assignments,
)
.await;
assert!(result.is_ok(), "Shrink resharding should succeed");
let batches = sender.sent_batches.lock().unwrap();
assert!(!batches.is_empty(), "Should have sent migration batches");
let mut all_processed_keys = Vec::new();
for batch in batches.iter() {
for (key, _) in batch {
all_processed_keys.push(key.clone());
}
}
let shard3_keys: Vec<String> = all_processed_keys
.iter()
.filter(|k| k.starts_with("shard3_doc_"))
.cloned()
.collect();
assert!(
!shard3_keys.is_empty(),
"Should have processed shard 3 documents: {:?}",
shard3_keys
);
assert_eq!(
shard3_keys.len(),
25,
"Should process all 25 documents from shard 3"
);
}
#[tokio::test]
async fn test_regression_resharding_timeout_and_limits() {
let temp_dir = tempdir().unwrap();
let storage = Arc::new(StorageEngine::new(temp_dir.path()).unwrap());
storage.create_database("test_db".to_string()).unwrap();
let db = storage.get_database("test_db").unwrap();
db.create_collection("test_coll".to_string(), None).unwrap();
let coll = db.get_collection("test_coll").unwrap();
let config = CollectionShardConfig {
num_shards: 4,
replication_factor: 1,
shard_key: "_key".to_string(),
};
coll.set_shard_config(&config).unwrap();
for shard_id in 0..4 {
let s_name = format!("test_coll_s{}", shard_id);
db.create_collection(s_name.clone(), None).unwrap();
let s = db.get_collection(&s_name).unwrap();
for i in 0..50 {
let doc_key = format!("shard{}_doc_{}", shard_id, i);
let doc = serde_json::json!({ "_key": doc_key, "value": i, "shard": shard_id });
s.insert(doc).unwrap();
}
}
let sender = Arc::new(MockBatchSender {
sent_batches: Arc::new(Mutex::new(Vec::new())),
should_fail: false,
_processed_keys: Vec::new(),
});
let coordinator = MigrationCoordinator::new(storage.clone());
let old_assignments = HashMap::new();
let mut current_assignments = HashMap::new();
for shard_id in 0..4 {
current_assignments.insert(
shard_id,
ShardAssignment {
shard_id,
primary_node: "test_node".to_string(),
replica_nodes: vec![],
},
);
}
let start_time = std::time::Instant::now();
let result = coordinator
.reshard_collection_robust(
&*sender,
"test_db",
"test_coll",
4, 4, "test_node",
&old_assignments,
¤t_assignments,
)
.await;
let duration = start_time.elapsed();
assert!(result.is_ok(), "Resharding should succeed without hanging");
assert!(
duration.as_secs() < 10,
"Resharding should complete in under 10 seconds, took {:?}",
duration
);
let stats = coordinator.get_migration_stats();
assert_eq!(
stats.total_processed, 200,
"Should process all 200 documents"
);
}
#[tokio::test]
async fn test_regression_expansion_data_preservation() {
let temp_dir = tempdir().unwrap();
let storage = Arc::new(StorageEngine::new(temp_dir.path()).unwrap());
storage.create_database("test_db".to_string()).unwrap();
let db = storage.get_database("test_db").unwrap();
db.create_collection("test_coll".to_string(), None).unwrap();
let s_name = "test_coll_s0";
db.create_collection(s_name.to_string(), None).unwrap();
let s = db.get_collection(s_name).unwrap();
let main_coll = db.get_collection("test_coll").unwrap();
let config = CollectionShardConfig {
num_shards: 1,
replication_factor: 1,
shard_key: "_key".to_string(),
};
main_coll.set_shard_config(&config).unwrap();
s.set_shard_config(&config).unwrap();
for i in 0..1000 {
let doc_key = format!("doc_{}", i);
let doc = serde_json::json!({ "_key": doc_key, "value": i });
s.insert(doc).unwrap();
}
let sender = Arc::new(MockBatchSender {
sent_batches: Arc::new(Mutex::new(Vec::new())),
should_fail: false,
_processed_keys: Vec::new(),
});
let coordinator = MigrationCoordinator::new(storage.clone());
let mut old_assignments = HashMap::new();
old_assignments.insert(
0,
ShardAssignment {
shard_id: 0,
primary_node: "test_node".to_string(), replica_nodes: vec![],
},
);
let mut current_assignments = HashMap::new();
current_assignments.insert(
0,
ShardAssignment {
shard_id: 0,
primary_node: "test_node".to_string(), replica_nodes: vec![],
},
);
current_assignments.insert(
1,
ShardAssignment {
shard_id: 1,
primary_node: "remote_node".to_string(), replica_nodes: vec![],
},
);
let start_time = std::time::Instant::now();
let result = coordinator
.reshard_collection_robust(
&*sender,
"test_db",
"test_coll",
1, 2, "test_node",
&old_assignments,
¤t_assignments,
)
.await;
let duration = start_time.elapsed();
if let Err(e) = &result {
println!("Expansion failed with error: {}", e);
}
assert!(
duration.as_secs() < 10,
"Should complete within 10 seconds, took {:?}",
duration
);
assert!(
duration.as_secs() < 10,
"Should complete within 10 seconds, took {:?}",
duration
);
let batches = sender.sent_batches.lock().unwrap();
assert!(
!batches.is_empty(),
"Should have processed documents during expansion"
);
}
#[tokio::test]
async fn test_regression_circuit_breaker_prevents_hanging() {
let temp_dir = tempdir().unwrap();
let storage = Arc::new(StorageEngine::new(temp_dir.path()).unwrap());
storage.create_database("test_db".to_string()).unwrap();
let db = storage.get_database("test_db").unwrap();
db.create_collection("test_coll".to_string(), None).unwrap();
let coll = db.get_collection("test_coll").unwrap();
let config = CollectionShardConfig {
num_shards: 1, replication_factor: 1,
shard_key: "_key".to_string(),
};
coll.set_shard_config(&config).unwrap();
let s_name = "test_coll_s0";
db.create_collection(s_name.to_string(), None).unwrap();
let s = db.get_collection(s_name).unwrap();
for i in 0..2 {
let doc_key = format!("doc_{}", i);
let doc = serde_json::json!({ "_key": doc_key, "value": i });
s.insert(doc).unwrap();
}
let sender = Arc::new(MockBatchSender {
sent_batches: Arc::new(Mutex::new(Vec::new())),
should_fail: true, _processed_keys: Vec::new(),
});
let coordinator = MigrationCoordinator::new(storage.clone());
let old_assignments = HashMap::new();
let mut current_assignments = HashMap::new();
current_assignments.insert(
0,
ShardAssignment {
shard_id: 0,
primary_node: "test_node".to_string(),
replica_nodes: vec![],
},
);
let start_time = std::time::Instant::now();
let _result = coordinator
.reshard_collection_robust(
&*sender,
"test_db",
"test_coll",
1,
1, "test_node",
&old_assignments,
¤t_assignments,
)
.await;
let duration = start_time.elapsed();
assert!(
duration.as_millis() < 2000,
"Should complete within 2 seconds even with failures, took {:?}",
duration
);
}
struct FailingThenSucceedingSender {
sent_batches: SentBatches,
fail_count: Arc<Mutex<usize>>,
}
#[async_trait]
impl BatchSender for FailingThenSucceedingSender {
async fn send_batch(
&self,
_db_name: &str,
_coll_name: &str,
_config: &CollectionShardConfig,
batch: Vec<(String, Value)>,
) -> Result<Vec<String>, String> {
self.sent_batches.lock().unwrap().push(batch.clone());
let mut fail_count = self.fail_count.lock().unwrap();
if *fail_count > 0 {
*fail_count -= 1;
return Err("Temporary failure".to_string());
}
Ok(batch.into_iter().map(|(key, _)| key).collect())
}
}
struct PartialFailureSender {
sent_batches: SentBatches,
fail_keys: Vec<String>,
}
#[async_trait]
impl BatchSender for PartialFailureSender {
async fn send_batch(
&self,
_db_name: &str,
_coll_name: &str,
_config: &CollectionShardConfig,
batch: Vec<(String, Value)>,
) -> Result<Vec<String>, String> {
self.sent_batches.lock().unwrap().push(batch.clone());
let successful_keys: Vec<String> = batch
.into_iter()
.filter_map(|(key, _)| {
if self.fail_keys.contains(&key) {
None
} else {
Some(key)
}
})
.collect();
Ok(successful_keys)
}
}
}