use async_trait::async_trait;
use flowsdk::mqtt_client::client::ConnectionResult;
use flowsdk::mqtt_client::opts::MqttClientOptions;
use flowsdk::mqtt_client::tokio_async_client::{
TokioAsyncClientConfig, TokioAsyncMqttClient, TokioMqttEventHandler,
};
use flowsdk::mqtt_client::MqttClientError;
use std::process::{Child, Command, Stdio};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::time::sleep;
#[derive(Clone)]
struct ReconnectTestHandler {
connected_count: Arc<Mutex<u32>>,
connection_lost_count: Arc<Mutex<u32>>,
reconnect_attempts: Arc<Mutex<Vec<u32>>>,
disconnected_count: Arc<Mutex<u32>>,
}
impl ReconnectTestHandler {
fn new() -> Self {
Self {
connected_count: Arc::new(Mutex::new(0)),
connection_lost_count: Arc::new(Mutex::new(0)),
reconnect_attempts: Arc::new(Mutex::new(Vec::new())),
disconnected_count: Arc::new(Mutex::new(0)),
}
}
fn get_connected_count(&self) -> u32 {
*self.connected_count.lock().unwrap()
}
fn get_connection_lost_count(&self) -> u32 {
*self.connection_lost_count.lock().unwrap()
}
fn get_reconnect_attempts(&self) -> Vec<u32> {
self.reconnect_attempts.lock().unwrap().clone()
}
fn get_disconnected_count(&self) -> u32 {
*self.disconnected_count.lock().unwrap()
}
}
#[async_trait]
impl TokioMqttEventHandler for ReconnectTestHandler {
async fn on_connected(&mut self, _result: &ConnectionResult) {
let mut count = self.connected_count.lock().unwrap();
*count += 1;
println!("✅ Connected (total: {})", *count);
}
async fn on_disconnected(&mut self, reason: Option<u8>) {
let mut count = self.disconnected_count.lock().unwrap();
*count += 1;
println!("❌ Disconnected (reason: {:?}, total: {})", reason, *count);
}
async fn on_connection_lost(&mut self) {
let mut count = self.connection_lost_count.lock().unwrap();
*count += 1;
println!("🔌 Connection lost (total: {})", *count);
}
async fn on_reconnect_attempt(&mut self, attempt: u32) {
let mut attempts = self.reconnect_attempts.lock().unwrap();
attempts.push(attempt);
println!("🔄 Reconnect attempt #{}", attempt);
}
async fn on_error(&mut self, error: &MqttClientError) {
println!("⚠️ Error: {:?}", error);
}
}
fn start_mosquitto(port: u16) -> Option<Child> {
println!("🚀 Starting mosquitto on port {}", port);
match Command::new("mosquitto")
.arg("-p")
.arg(port.to_string())
.arg("-v") .stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
Ok(child) => {
std::thread::sleep(Duration::from_millis(500));
println!("✅ Mosquitto started (PID: {})", child.id());
Some(child)
}
Err(e) => {
println!(
"⚠️ Failed to start mosquitto: {}. Make sure it's installed.",
e
);
None
}
}
}
fn stop_mosquitto(mut broker: Child) {
println!("🛑 Stopping mosquitto");
let _ = broker.kill();
let _ = broker.wait();
std::thread::sleep(Duration::from_millis(300));
}
#[tokio::test]
#[ignore] async fn test_auto_reconnect_on_broker_restart() {
const TEST_PORT: u16 = 11883;
let broker = start_mosquitto(TEST_PORT);
if broker.is_none() {
println!("⚠️ Skipping test: mosquitto not available");
return;
}
let mut broker = broker.unwrap();
let config = TokioAsyncClientConfig::builder()
.auto_reconnect(true)
.build();
let options = MqttClientOptions::builder()
.peer(format!("127.0.0.1:{}", TEST_PORT))
.client_id("test-auto-reconnect")
.clean_start(true)
.keep_alive(3) .reconnect_base_delay_ms(300) .reconnect_max_delay_ms(1000) .max_reconnect_attempts(0) .ping_timeout_multiplier(2) .build();
let handler = ReconnectTestHandler::new();
let handler_clone = handler.clone();
match TokioAsyncMqttClient::new(options, Box::new(handler), config).await {
Ok(client) => {
println!("\n🔵 Phase 1: Initial connection");
match client.connect_sync().await {
Ok(result) => {
println!(
"✅ Connected successfully: session_present={}",
result.session_present
);
assert_eq!(handler_clone.get_connected_count(), 1);
}
Err(e) => {
println!("❌ Initial connection failed: {:?}", e);
stop_mosquitto(broker);
return;
}
}
sleep(Duration::from_millis(500)).await;
println!("\n🔵 Phase 2: Stopping broker to simulate connection loss");
stop_mosquitto(broker);
println!("⏳ Waiting for connection loss detection (up to 8 seconds)...");
sleep(Duration::from_secs(8)).await;
let lost_count = handler_clone.get_connection_lost_count();
let disconnect_count = handler_clone.get_disconnected_count();
let attempts_after_loss = handler_clone.get_reconnect_attempts();
println!("\n📊 After connection loss:");
println!(" Connection lost events: {}", lost_count);
println!(" Disconnected events: {}", disconnect_count);
println!(" Reconnect attempts so far: {:?}", attempts_after_loss);
assert!(
lost_count > 0 || disconnect_count > 0,
"Should detect connection loss"
);
println!("\n🔵 Phase 3: Restarting broker");
broker = match start_mosquitto(TEST_PORT) {
Some(b) => b,
None => {
println!("❌ Failed to restart broker");
let _ = client.shutdown().await;
return;
}
};
println!("⏳ Waiting for automatic reconnection (checking every 2 seconds, max 15 seconds)...");
let mut reconnected = false;
let max_checks = 8;
for i in 0..max_checks {
sleep(Duration::from_secs(2)).await;
let current_connections = handler_clone.get_connected_count();
let current_attempts = handler_clone.get_reconnect_attempts();
println!(
" Check #{}: connections={}, attempts={:?}",
i + 1,
current_connections,
current_attempts
);
if current_connections >= 2 {
reconnected = true;
println!(" ✅ Reconnection detected!");
break;
}
}
let attempts = handler_clone.get_reconnect_attempts();
let final_connected_count = handler_clone.get_connected_count();
println!("\n📊 Final status:");
println!(" Total connections: {}", final_connected_count);
println!(" All reconnect attempts: {:?}", attempts);
println!(
" Connection lost events: {}",
handler_clone.get_connection_lost_count()
);
assert!(!attempts.is_empty(), "Should have reconnection attempts");
assert!(
reconnected,
"❌ FAILED: Automatic reconnection did not occur within {} seconds.\n\
Expected: Client should reconnect after broker restart\n\
Actual: {} connection(s), {} attempt(s)\n\
This indicates the reconnection mechanism is not working correctly.\n\
Backoff schedule: 300ms, 600ms, 1000ms (capped), repeating at 1000ms\n\
Total time available: {} seconds should allow 10+ retry attempts.",
max_checks * 2,
final_connected_count,
attempts.len(),
max_checks * 2
);
assert!(
final_connected_count >= 2,
"Should have reconnected (got {} connections)",
final_connected_count
);
println!("✅ Automatic reconnection successful!");
let _ = client.shutdown().await;
stop_mosquitto(broker);
}
Err(e) => {
stop_mosquitto(broker);
panic!("Failed to create client: {:?}", e);
}
}
}
#[tokio::test]
async fn test_reconnect_backoff_timing() {
let config = TokioAsyncClientConfig::builder()
.auto_reconnect(true)
.build();
let options = MqttClientOptions::builder()
.peer("127.0.0.1:19999") .client_id("test-backoff")
.reconnect_base_delay_ms(200) .reconnect_max_delay_ms(1000) .max_reconnect_attempts(3) .build();
let handler = ReconnectTestHandler::new();
let handler_clone = handler.clone();
match TokioAsyncMqttClient::new(options, Box::new(handler), config).await {
Ok(client) => {
println!("\n🔵 Testing reconnection backoff with unreachable broker");
let start = std::time::Instant::now();
let _ = client.connect_sync().await;
sleep(Duration::from_secs(3)).await;
let attempts = handler_clone.get_reconnect_attempts();
let elapsed = start.elapsed();
println!("\n📊 Backoff test results:");
println!(" Reconnect attempts: {:?}", attempts);
println!(" Time elapsed: {:?}", elapsed);
println!(" Expected: 3 attempts with backoff (200ms, 400ms, 800ms)");
assert!(attempts.len() <= 3, "Should not exceed max attempts");
if !attempts.is_empty() {
assert_eq!(attempts[0], 1, "First attempt should be #1");
}
let _ = client.shutdown().await;
}
Err(e) => {
panic!("Failed to create client: {:?}", e);
}
}
}
#[tokio::test]
async fn test_reconnect_disabled() {
let config = TokioAsyncClientConfig::builder()
.auto_reconnect(false) .build();
let options = MqttClientOptions::builder()
.peer("127.0.0.1:19999") .client_id("test-no-reconnect")
.build();
let handler = ReconnectTestHandler::new();
let handler_clone = handler.clone();
match TokioAsyncMqttClient::new(options, Box::new(handler), config).await {
Ok(client) => {
println!("\n🔵 Testing with auto-reconnect disabled");
let _ = client.connect_sync().await;
sleep(Duration::from_secs(2)).await;
let attempts = handler_clone.get_reconnect_attempts();
println!("\n📊 No-reconnect test results:");
println!(" Reconnect attempts: {:?}", attempts);
println!(" Expected: No reconnection attempts");
assert!(
attempts.is_empty(),
"Should not attempt reconnection when disabled"
);
let _ = client.shutdown().await;
}
Err(e) => {
panic!("Failed to create client: {:?}", e);
}
}
}
#[tokio::test]
async fn test_dynamic_reconnect_control() {
let config = TokioAsyncClientConfig::builder()
.auto_reconnect(true)
.build();
let options = MqttClientOptions::builder()
.peer("127.0.0.1:19999") .client_id("test-dynamic-reconnect")
.reconnect_base_delay_ms(200)
.max_reconnect_attempts(0) .build();
let handler = ReconnectTestHandler::new();
let handler_clone = handler.clone();
match TokioAsyncMqttClient::new(options, Box::new(handler), config).await {
Ok(client) => {
println!("\n🔵 Testing dynamic reconnect control");
let _ = client.connect_sync().await;
sleep(Duration::from_millis(500)).await;
let initial_attempts = handler_clone.get_reconnect_attempts().len();
println!(" Initial attempts: {}", initial_attempts);
client.set_auto_reconnect(false).await.unwrap();
println!(" ✋ Auto-reconnect disabled");
sleep(Duration::from_secs(1)).await;
let after_disable = handler_clone.get_reconnect_attempts().len();
println!(" Attempts after disable: {}", after_disable);
client.set_auto_reconnect(true).await.unwrap();
println!(" ✅ Auto-reconnect re-enabled");
sleep(Duration::from_millis(3000)).await;
let final_attempts = handler_clone.get_reconnect_attempts().len();
println!(" Final attempts: {}", final_attempts);
assert!(
initial_attempts > 0,
"Should have initial reconnection attempts"
);
assert!(
after_disable <= initial_attempts + 3,
"Reconnection attempts should mostly stop after disabling (initial: {}, after_disable: {})",
initial_attempts,
after_disable
);
if final_attempts > after_disable {
println!(" ✅ Dynamic reconnect control verified:");
println!(" - Started with {} attempts", initial_attempts);
println!(
" - After disable: {} attempts (delta: {})",
after_disable,
after_disable - initial_attempts
);
println!(
" - After re-enable: {} attempts (delta: {})",
final_attempts,
final_attempts - after_disable
);
} else {
println!(" ⚠️ Reconnection did not automatically resume after re-enabling.");
println!(" This is expected behavior - re-enabling auto-reconnect while");
println!(" disconnected doesn't trigger a new reconnect schedule.");
println!(" Reconnection will resume on the next connection loss event.");
println!(" - Initial attempts: {}", initial_attempts);
println!(" - After disable: {}", after_disable);
println!(" - After re-enable: {}", final_attempts);
}
let _ = client.shutdown().await;
}
Err(e) => {
panic!("Failed to create client: {:?}", e);
}
}
}