use crate::{
ClientError, ErrorKind, Result, RetryReason, TimeoutKind,
client::{BatchPreparedCommand, 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),
TimeoutKind::Command,
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),
TimeoutKind::Command,
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_is_routed_by_two_successive_redirection_reasons() -> 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?;
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),
TimeoutKind::Command,
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),
TimeoutKind::Command,
client.client_reply(ClientReplyMode::On).into_future(),
)
.await??;
let a: String = timeout(
Duration::from_secs(5),
TimeoutKind::Command,
client.get("reply_a").into_future(),
)
.await??;
let b: String = timeout(
Duration::from_secs(5),
TimeoutKind::Command,
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),
TimeoutKind::Command,
client.get("skip_a").into_future(),
)
.await??;
let b: String = timeout(
Duration::from_secs(5),
TimeoutKind::Command,
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),
TimeoutKind::Command,
client.send(cmd("PING"), None),
)
.await??;
assert_eq!(
"PONG", pong,
"RESET turns replies back on, and the client must expect them again"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn a_reply_nobody_awaits_is_logged_at_debug_with_its_command() -> Result<()> {
use crate::{commands::DebugCommands, tests::LogCapture};
let mut config = get_default_config()?;
config.command_timeout = Duration::from_millis(50);
let client = Client::connect(config).await?;
let capture = LogCapture::start();
let result = client.debug_sleep(Duration::from_millis(300)).await;
assert!(
result.unwrap_err().is_timeout(),
"the command must fail on its deadline"
);
sleep(Duration::from_millis(500)).await;
let events = capture.events();
drop(capture);
let abandoned: Vec<_> = events
.iter()
.filter(|(_, message)| message.contains("receiver"))
.collect();
assert!(
!abandoned.is_empty(),
"the abandoned reply must be logged: {events:?}"
);
for (level, message) in &abandoned {
assert_eq!(
log::Level::Debug,
*level,
"an abandoned reply is routine, not a warning: {message}"
);
assert!(
message.contains("DEBUG"),
"the event must name the command: {message}"
);
}
Ok(())
}
#[tokio::test]
#[serial]
async fn a_default_command_is_failed_by_a_lost_connection_rather_than_replayed() -> Result<()> {
log_try_init();
let mut config = get_default_config()?;
config.reconnection = ReconnectionConfig::new_constant(0, 10);
assert!(
!config.retry_on_error,
"this test is about the stock default; update it deliberately if it changes"
);
config.max_command_attempts = 1;
let client = get_test_client_with_config(config).await?;
let result: Result<String> = timeout(
Duration::from_secs(5),
TimeoutKind::Command,
client.send(cmd("PING").kill_connection_on_read(1), None),
)
.await?;
let error = result.unwrap_err();
assert!(
matches!(error.kind(), ErrorKind::DisconnectedByPeer),
"expected DisconnectedByPeer, got {error:?}"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn neither_side_of_the_loop_drains_without_bound() -> Result<()> {
log_try_init();
let hook = QueueMetricsTestHook::new();
let mut config = get_default_config()?;
config.queue_metrics_test_hook = Some(hook.clone());
let cap = config.max_messages_per_wave;
let client = get_test_client_with_config(config).await?;
const BURST: usize = 2_000;
assert!(
BURST > cap * 4,
"the burst must be large enough to be cut several times"
);
for i in 0..BURST {
client.send_and_forget(cmd("PING").arg(i.to_string()), None)?;
}
let _: String = timeout(
Duration::from_secs(30),
TimeoutKind::Command,
client.send(cmd("PING"), None),
)
.await??;
assert!(
hook.write_wave_high_water() <= cap,
"a send wave took {} messages, above the {cap} cap",
hook.write_wave_high_water()
);
assert!(
hook.read_wave_high_water() <= cap,
"a read wave took {} replies, above the {cap} cap",
hook.read_wave_high_water()
);
Ok(())
}
#[tokio::test]
#[serial]
async fn the_send_queue_counts_commands_not_messages() -> Result<()> {
log_try_init();
let hook = QueueMetricsTestHook::new();
let mut config = get_default_config()?;
config.queue_metrics_test_hook = Some(hook.clone());
let client = get_test_client_with_config(config).await?;
const COMMANDS: usize = 10;
let mut pipeline = client.create_pipeline();
for i in 0..COMMANDS {
pipeline.set(format!("key{i}"), i).queue();
}
let replies: Vec<String> = pipeline.execute().await?;
assert_eq!(COMMANDS, replies.len());
assert!(
hook.queued_commands_high_water() >= COMMANDS,
"a {COMMANDS}-command pipeline peaked at {} queued commands; the total \
is counting messages, not commands",
hook.queued_commands_high_water()
);
let _: String = client.send(cmd("PING"), None).await?;
assert_eq!(
0,
hook.queued_commands(),
"the send queue is drained, so it holds no commands"
);
Ok(())
}