use crate::{
ClientError, ErrorKind, Result, RetryReason,
client::{Client, ClientPreparedCommand, ReconnectionConfig},
commands::{
ClientReplyMode, ConnectionCommands, GenericCommands, PubSubCommands, StringCommands,
},
network::{QueueMetricsTestHook, SendBatchTestHook, sleep, timeout},
resp::cmd,
tests::{
get_default_config, get_default_port, get_test_client, get_test_client_with_config,
log_try_init,
},
};
use serial_test::serial;
use std::future::IntoFuture;
use std::{collections::HashMap, time::Duration};
#[tokio::test]
#[serial]
async fn retry_reasons_do_not_leak_across_messages_in_a_batch() -> Result<()> {
log_try_init();
let hook = SendBatchTestHook::new();
let mut config = get_default_config()?;
config.send_batch_test_hook = Some(hook.clone());
let client = Client::connect(config).await?;
hook.push_injection(Some(vec![RetryReason::Ask {
hash_slot: 0,
address: ("127.0.0.1".to_owned(), get_default_port()),
}]));
client.send_and_forget(cmd("GET").arg("net12_a"), None)?;
client.send_and_forget(cmd("STRLEN").arg("net12_b"), None)?;
let _: String = client.send(cmd("PING"), None).await?;
let fed = hook.fed_retry_reasons();
let first_idx = fed
.iter()
.position(|(name, _)| name == "GET")
.expect("GET should have been fed");
assert_eq!(
1, fed[first_idx].1,
"the first message should carry the injected reason"
);
let (second_name, second_reasons) = &fed[first_idx + 1];
assert_eq!("STRLEN", second_name, "the second message should follow");
assert_eq!(
0, *second_reasons,
"a following message must not inherit the previous message's retry reasons"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn the_queue_depth_marks_survive_the_drain() -> Result<()> {
log_try_init();
let metrics = QueueMetricsTestHook::new();
let mut config = get_default_config()?;
config.queue_metrics_test_hook = Some(metrics.clone());
let client = Client::connect(config).await?;
for i in 0..100 {
client.send_and_forget(cmd("GET").arg(format!("depth_mark_{i}")), None)?;
}
let _: String = client.send(cmd("PING"), None).await?;
let send_peak = metrics.messages_to_send_high_water();
let receive_peak = metrics.messages_to_receive_high_water();
assert!(
send_peak > 1,
"the send-queue mark should hold the peak reached before the drain, got {send_peak}"
);
assert!(
receive_peak > 1,
"the receive-queue mark should hold the peak reached before the drain, got {receive_peak}"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn non_retryable_message_behind_retryable_is_not_replayed_on_reconnect() -> Result<()> {
log_try_init();
let control = get_test_client().await?;
control.del("net01_counter").await?;
let mut config = get_default_config()?;
config.reconnection = ReconnectionConfig::new_constant(0, 100);
let client = get_test_client_with_config(config).await?;
client.send_and_forget(cmd("GET").arg("net01_dummy"), Some(true))?;
client.send_and_forget(
cmd("INCR").arg("net01_counter").kill_connection_on_read(1),
Some(false),
)?;
sleep(Duration::from_millis(500)).await;
let counter: i64 = control.get("net01_counter").await?;
assert_eq!(
1, counter,
"the non-retryable command must not be replayed on reconnect"
);
control.del("net01_counter").await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn inflight_unsubscribe_does_not_desync_responses_after_reconnect() -> Result<()> {
log_try_init();
let mut config = get_default_config()?;
config.reconnection = ReconnectionConfig::new_constant(0, 100);
config.retry_on_error = true;
let client = get_test_client_with_config(config).await?;
client.send_and_forget(
cmd("UNSUBSCRIBE")
.arg("net03_chan")
.kill_connection_on_read(1),
None,
)?;
sleep(Duration::from_millis(500)).await;
let echoed: String = timeout(
Duration::from_secs(2),
client.send(cmd("ECHO").arg("net03_marker"), None),
)
.await??;
assert_eq!(
"net03_marker", echoed,
"the follow-up response must be routed to its own caller"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn inflight_unsubscribe_is_not_turned_into_subscribe_on_reconnect() -> Result<()> {
log_try_init();
let control = get_test_client().await?;
let mut config = get_default_config()?;
config.reconnection = ReconnectionConfig::new_constant(0, 100);
let client = get_test_client_with_config(config).await?;
client.send_and_forget(
cmd("UNSUBSCRIBE")
.arg("net02_chan")
.kill_connection_on_read(1),
None,
)?;
sleep(Duration::from_millis(500)).await;
let num_sub: HashMap<String, usize> = control.pub_sub_numsub(["net02_chan"]).await?;
assert_eq!(
Some(&0usize),
num_sub.get("net02_chan"),
"an in-flight unsubscription must not be resubscribed on reconnect"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn retryable_command_fails_after_max_command_attempts() -> Result<()> {
log_try_init();
let mut config = get_default_config()?;
config.reconnection = ReconnectionConfig::new_constant(0, 10);
config.retry_on_error = true;
config.max_command_attempts = 1;
let client = get_test_client_with_config(config).await?;
let result: Result<String> = timeout(
Duration::from_secs(5),
client.send(cmd("PING").kill_connection_on_read(1), Some(true)),
)
.await?;
let error = result.unwrap_err();
assert!(
matches!(
error.kind(),
ErrorKind::Client(ClientError::MaxCommandAttemptsReached)
),
"expected MaxCommandAttemptsReached, got {error:?}"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn a_command_survives_an_ask_then_moved_on_the_default_attempt_budget() -> Result<()> {
log_try_init();
let hook = SendBatchTestHook::new();
let mut config = get_default_config()?;
config.send_batch_test_hook = Some(hook.clone());
let client = get_test_client_with_config(config.clone()).await?;
assert_eq!(
5, config.max_command_attempts,
"this test is about the stock budget; update it deliberately if it changes"
);
let address = ("127.0.0.1".to_owned(), get_default_port());
hook.push_injection(Some(vec![RetryReason::Ask {
hash_slot: 0,
address: address.clone(),
}]));
hook.push_injection(Some(vec![RetryReason::Moved {
hash_slot: 0,
address,
}]));
let result: Result<String> =
timeout(Duration::from_secs(5), client.send(cmd("PING"), Some(true))).await?;
assert_eq!(
"PONG", result?,
"a command redirected twice must still be answered"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn reply_mode_is_restored_after_reconnect() -> Result<()> {
log_try_init();
let mut config = get_default_config()?;
config.reconnection = ReconnectionConfig::new_constant(0, 100);
let client = get_test_client_with_config(config).await?;
client.del(["reply_a", "reply_b"]).await?;
let mut on_reconnect = client.on_reconnect();
client.client_reply(ClientReplyMode::Off).forget()?;
client.send_and_forget(cmd("PING").kill_connection_on_read(1), None)?;
on_reconnect
.recv()
.await
.expect("the client should have reconnected");
client.set("reply_a", "a").forget()?;
client.set("reply_b", "b").forget()?;
timeout(
Duration::from_secs(5),
client.client_reply(ClientReplyMode::On).into_future(),
)
.await??;
let a: String = timeout(Duration::from_secs(5), client.get("reply_a").into_future()).await??;
let b: String = timeout(Duration::from_secs(5), client.get("reply_b").into_future()).await??;
assert_eq!("a", a, "responses must not be shifted after a reconnection");
assert_eq!("b", b, "responses must not be shifted after a reconnection");
client.del(["reply_a", "reply_b"]).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn reply_skip_silences_only_the_next_command() -> Result<()> {
log_try_init();
let client = get_test_client().await?;
client.del(["skip_a", "skip_b"]).await?;
client.client_reply(ClientReplyMode::Skip).forget()?;
client.set("skip_a", "a").forget()?;
client.set("skip_b", "b").forget()?;
let a: String = timeout(Duration::from_secs(5), client.get("skip_a").into_future()).await??;
let b: String = timeout(Duration::from_secs(5), client.get("skip_b").into_future()).await??;
assert_eq!("a", a, "responses must not be shifted after a SKIP");
assert_eq!("b", b, "responses must not be shifted after a SKIP");
client.del(["skip_a", "skip_b"]).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn reset_restores_the_reply_mode_the_server_restored() -> Result<()> {
log_try_init();
let client = get_test_client().await?;
client.client_reply(ClientReplyMode::Off).forget()?;
client.send_and_forget(cmd("RESET"), None)?;
let pong: String = timeout(Duration::from_secs(5), client.send(cmd("PING"), None)).await??;
assert_eq!(
"PONG", pong,
"RESET turns replies back on, and the client must expect them again"
);
Ok(())
}