use crate::{
Result,
client::{ClusterConfig, Config},
commands::{
ClusterCommands,
ClusterSetSlotSubCommand::{self},
LegacyClusterNodeResult, LegacyClusterShardResult,
},
network::{ClusterConnection, ConnectionState, convert_from_legacy_shard_description},
tests::{
TestClient,
fake_server::{FakeServer, TcpNode},
},
};
fn legacy_node(id: &str, port: u16) -> LegacyClusterNodeResult {
LegacyClusterNodeResult {
id: id.to_owned(),
preferred_endpoint: "127.0.0.1".to_owned(),
ip: "127.0.0.1".to_owned(),
hostname: None,
port,
}
}
#[test]
fn cluster_selslot_command() {
let cmd = TestClient
.cluster_setslot(
12539,
ClusterSetSlotSubCommand::Migrating("37618c7eec0dd58e946e1ef0df02d8c5a9a14235"),
)
.command;
assert_eq!(
"CLUSTER SETSLOT 12539 MIGRATING 37618c7eec0dd58e946e1ef0df02d8c5a9a14235",
cmd.to_string()
);
}
#[test]
fn a_legacy_shard_without_any_node_is_skipped_rather_than_indexed() {
let converted = convert_from_legacy_shard_description(vec![
LegacyClusterShardResult {
slot: (0, 100),
nodes: vec![],
},
LegacyClusterShardResult {
slot: (101, 200),
nodes: vec![legacy_node("node-a", 7000)],
},
]);
assert_eq!(1, converted.len());
let shard = &converted[0];
assert_eq!(vec![(101, 200)], shard.slots);
assert_eq!("node-a", shard.nodes[0].id);
assert_eq!("master", shard.nodes[0].role);
}
#[test]
fn legacy_shards_sharing_a_master_are_merged_into_one_shard() {
let converted = convert_from_legacy_shard_description(vec![
LegacyClusterShardResult {
slot: (0, 100),
nodes: vec![legacy_node("node-a", 7000), legacy_node("node-b", 7001)],
},
LegacyClusterShardResult {
slot: (101, 200),
nodes: vec![legacy_node("node-a", 7000)],
},
LegacyClusterShardResult {
slot: (201, 300),
nodes: vec![legacy_node("node-c", 7002)],
},
]);
assert_eq!(2, converted.len());
assert_eq!(vec![(0, 100), (101, 200)], converted[0].slots);
assert_eq!("node-a", converted[0].nodes[0].id);
assert_eq!("master", converted[0].nodes[0].role);
assert_eq!("replica", converted[0].nodes[1].role);
assert_eq!(vec![(201, 300)], converted[1].slots);
assert_eq!("node-c", converted[1].nodes[0].id);
}
fn bulk(value: &str) -> String {
format!("${}\r\n{value}\r\n", value.len())
}
fn resp_map(entries: &[(&str, String)]) -> String {
let mut map = format!("%{}\r\n", entries.len());
for (key, value) in entries {
map.push_str(&bulk(key));
map.push_str(value);
}
map
}
fn one_shard_owning_every_slot(node_id: &str, port: u16) -> Vec<u8> {
let master = resp_map(&[
("id", bulk(node_id)),
("endpoint", bulk("127.0.0.1")),
("ip", bulk("127.0.0.1")),
("port", format!(":{port}\r\n")),
("hostname", bulk("")),
("role", bulk("master")),
("replication-offset", ":0\r\n".to_owned()),
("health", bulk("online")),
]);
let shard = resp_map(&[
("slots", "*2\r\n:0\r\n:16383\r\n".to_owned()),
("nodes", format!("*1\r\n{master}")),
]);
format!("*1\r\n{shard}").into_bytes()
}
#[tokio::test]
async fn a_reconnection_rediscovers_from_the_nodes_it_holds() -> Result<()> {
const MASTER_ID: &str = "0000000000000000000000000000000000000001";
let mut master = TcpNode::bind().await?;
let mut seed = TcpNode::bind().await?;
let shards = one_shard_owning_every_slot(MASTER_ID, master.addr.port());
master.serve(FakeServer::new().reply("CLUSTER", &shards));
seed.serve(FakeServer::new().reply("CLUSTER", &shards));
let cluster_config = ClusterConfig {
nodes: vec![seed.address()],
..Default::default()
};
let config = Config::default();
let mut connection_state = ConnectionState::default();
let mut cluster =
ClusterConnection::connect(&cluster_config, &config, &mut connection_state).await?;
assert_eq!(
1,
seed.accepted(),
"the seed serves the initial discovery, nothing being held yet"
);
let master_dials = master.accepted();
cluster.reconnect(&mut connection_state).await?;
assert!(
master.accepted() > master_dials,
"a rediscovery dials the node it holds"
);
assert_eq!(
1,
seed.accepted(),
"a held node answered, so no seed is needed"
);
drop(seed);
let master_dials = master.accepted();
cluster
.reconnect(&mut connection_state)
.await
.expect("a held node answers, so the reconnection has somewhere to go");
assert!(
master.accepted() > master_dials,
"the reconnection must fall back on the node it holds"
);
Ok(())
}