#[cfg(feature = "tokio")]
use crate::cluster::RedisClusterHandle;
#[cfg(feature = "tokio")]
use crate::error::Result;
#[cfg(feature = "tokio")]
use crate::server::RedisServerHandle;
use std::process::Command;
#[cfg(feature = "tokio")]
use std::time::Duration;
#[cfg(feature = "tokio")]
pub fn kill_node(handle: &RedisServerHandle) {
let pid = handle.pid().to_string();
let _ = Command::new("kill").args(["-9", &pid]).output();
}
#[cfg(feature = "tokio")]
pub fn freeze_node(handle: &RedisServerHandle) {
let pid = handle.pid().to_string();
let _ = Command::new("kill").args(["-STOP", &pid]).output();
}
#[cfg(feature = "tokio")]
pub fn resume_node(handle: &RedisServerHandle) {
let pid = handle.pid().to_string();
let _ = Command::new("kill").args(["-CONT", &pid]).output();
}
#[cfg(feature = "tokio")]
pub fn pause_node(handle: &RedisServerHandle, duration: Duration) {
let pid = handle.pid().to_string();
let _ = Command::new("kill").args(["-STOP", &pid]).output();
tokio::spawn(async move {
tokio::time::sleep(duration).await;
let _ = Command::new("kill").args(["-CONT", &pid]).output();
});
}
#[cfg(feature = "tokio")]
pub async fn slow_down(handle: &RedisServerHandle, millis: u64) -> Result<String> {
handle.run(&["CLIENT", "PAUSE", &millis.to_string()]).await
}
#[cfg(feature = "tokio")]
pub async fn trigger_save(handle: &RedisServerHandle) -> Result<String> {
handle.run(&["BGSAVE"]).await
}
#[cfg(feature = "tokio")]
pub async fn flushall(handle: &RedisServerHandle) -> Result<String> {
handle.run(&["FLUSHALL"]).await
}
#[cfg(feature = "tokio")]
pub async fn fill_memory(handle: &RedisServerHandle, prefix: &str, count: usize) -> Result<()> {
let value = "x".repeat(1024);
for i in 0..count {
handle
.run(&["SET", &format!("{prefix}{i}"), &value])
.await?;
}
Ok(())
}
#[cfg(feature = "tokio")]
pub async fn kill_master_by_slot(cluster: &RedisClusterHandle, slot: u16) -> Result<u16> {
let owner = find_slot_owner(cluster, slot).await?;
let pid = owner.pid().to_string();
let _ = Command::new("kill").args(["-9", &pid]).output();
Ok(owner.port())
}
#[cfg(feature = "tokio")]
pub async fn kill_master_by_key(cluster: &RedisClusterHandle, key: &str) -> Result<u16> {
let slot = keyslot(cluster, key).await?;
kill_master_by_slot(cluster, slot).await
}
#[cfg(feature = "tokio")]
pub async fn freeze_master_by_slot(cluster: &RedisClusterHandle, slot: u16) -> Result<u16> {
let owner = find_slot_owner(cluster, slot).await?;
let pid = owner.pid().to_string();
let _ = Command::new("kill").args(["-STOP", &pid]).output();
Ok(owner.port())
}
#[cfg(feature = "tokio")]
pub async fn trigger_failover(replica: &RedisServerHandle) -> Result<String> {
let result = replica.run(&["CLUSTER", "FAILOVER"]).await?;
if result.contains("ERR") {
return replica.run(&["CLUSTER", "FAILOVER", "FORCE"]).await;
}
Ok(result)
}
#[cfg(feature = "tokio")]
pub fn partition(cluster: &RedisClusterHandle, reachable: &[usize]) -> Vec<u16> {
let mut frozen = Vec::new();
for (i, node) in cluster.nodes().iter().enumerate() {
if !reachable.contains(&i) {
let pid = node.pid().to_string();
let _ = Command::new("kill").args(["-STOP", &pid]).output();
frozen.push(node.port());
}
}
frozen
}
#[cfg(feature = "tokio")]
pub fn recover(cluster: &RedisClusterHandle) {
for node in cluster.nodes() {
let pid = node.pid().to_string();
let _ = Command::new("kill").args(["-CONT", &pid]).output();
}
}
#[cfg(feature = "tokio")]
pub async fn migrate_slot(
cluster: &RedisClusterHandle,
slot: u16,
from: &RedisServerHandle,
to: &RedisServerHandle,
) -> Result<usize> {
let guard = ReshardGuard::start(cluster, slot, from, to).await?;
let moved = guard.migrate_keys().await?;
guard.complete().await?;
Ok(moved)
}
#[cfg(feature = "tokio")]
pub async fn migrate_slots(
cluster: &RedisClusterHandle,
slots: std::ops::RangeInclusive<u16>,
from: &RedisServerHandle,
to: &RedisServerHandle,
) -> Result<usize> {
let mut moved = 0;
for slot in slots {
moved += migrate_slot(cluster, slot, from, to).await?;
}
Ok(moved)
}
#[cfg(feature = "tokio")]
pub struct ReshardGuard<'a> {
cluster: &'a RedisClusterHandle,
slot: u16,
from: &'a RedisServerHandle,
to: &'a RedisServerHandle,
to_id: String,
resolved: bool,
}
#[cfg(feature = "tokio")]
impl<'a> ReshardGuard<'a> {
pub async fn start(
cluster: &'a RedisClusterHandle,
slot: u16,
from: &'a RedisServerHandle,
to: &'a RedisServerHandle,
) -> Result<ReshardGuard<'a>> {
let from_id = node_id(from).await?;
let to_id = node_id(to).await?;
let slot_str = slot.to_string();
to.run(&["CLUSTER", "SETSLOT", &slot_str, "IMPORTING", &from_id])
.await?;
from.run(&["CLUSTER", "SETSLOT", &slot_str, "MIGRATING", &to_id])
.await?;
Ok(ReshardGuard {
cluster,
slot,
from,
to,
to_id,
resolved: false,
})
}
pub async fn migrate_keys(&self) -> Result<usize> {
let password = self.cluster.password();
let to_host = self.to.host().to_string();
let to_port = self.to.port().to_string();
let mut moved = 0;
loop {
let keys = get_keys_in_slot(self.from, self.slot, 100).await?;
if keys.is_empty() {
break;
}
for key in &keys {
let mut args = vec![
to_host.as_str(),
to_port.as_str(),
key.as_str(),
"0",
"5000",
];
if let Some(password) = password {
args.push("AUTH");
args.push(password);
}
let mut cmd = vec!["MIGRATE"];
cmd.extend(args);
self.from.run(&cmd).await?;
moved += 1;
}
}
Ok(moved)
}
pub async fn complete(mut self) -> Result<usize> {
let moved = self.migrate_keys().await?;
let slot_str = self.slot.to_string();
for node in self.cluster.master_nodes() {
node.run(&["CLUSTER", "SETSLOT", &slot_str, "NODE", &self.to_id])
.await?;
}
self.resolved = true;
Ok(moved)
}
pub async fn abort(mut self) -> Result<()> {
let slot_str = self.slot.to_string();
self.from
.run(&["CLUSTER", "SETSLOT", &slot_str, "STABLE"])
.await?;
self.to
.run(&["CLUSTER", "SETSLOT", &slot_str, "STABLE"])
.await?;
self.resolved = true;
Ok(())
}
}
#[cfg(feature = "tokio")]
impl Drop for ReshardGuard<'_> {
fn drop(&mut self) {
if self.resolved {
return;
}
let slot_str = self.slot.to_string();
self.from
.cli()
.fire_and_forget(&["CLUSTER", "SETSLOT", &slot_str, "STABLE"]);
self.to
.cli()
.fire_and_forget(&["CLUSTER", "SETSLOT", &slot_str, "STABLE"]);
}
}
#[cfg(feature = "tokio")]
async fn node_id(handle: &RedisServerHandle) -> Result<String> {
let id = handle.run(&["CLUSTER", "MYID"]).await?;
Ok(id.trim().to_string())
}
#[cfg(feature = "tokio")]
async fn get_keys_in_slot(
handle: &RedisServerHandle,
slot: u16,
count: u32,
) -> Result<Vec<String>> {
let output = handle
.run(&[
"CLUSTER",
"GETKEYSINSLOT",
&slot.to_string(),
&count.to_string(),
])
.await?;
Ok(output
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(String::from)
.collect())
}
#[cfg(feature = "tokio")]
async fn keyslot(cluster: &RedisClusterHandle, key: &str) -> Result<u16> {
let output = cluster.cli().run(&["CLUSTER", "KEYSLOT", key]).await?;
let slot: u16 = output
.trim()
.parse()
.map_err(|_| crate::error::Error::Timeout {
message: format!("could not parse CLUSTER KEYSLOT response: {output}"),
})?;
Ok(slot)
}
#[cfg(feature = "tokio")]
async fn find_slot_owner(cluster: &RedisClusterHandle, slot: u16) -> Result<&RedisServerHandle> {
for node in cluster.nodes() {
if let Ok(info) = node.run(&["CLUSTER", "NODES"]).await {
for line in info.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 9 {
continue;
}
let flags = parts[2];
if !flags.contains("master") {
continue;
}
for range_str in &parts[8..] {
if slot_in_range(range_str, slot) {
if let Some(port) = parse_cluster_node_port(parts[1]) {
for n in cluster.nodes() {
if n.port() == port {
return Ok(n);
}
}
}
}
}
}
break;
}
}
Err(crate::error::Error::Timeout {
message: format!("could not find owner of slot {slot}"),
})
}
fn slot_in_range(range_str: &str, slot: u16) -> bool {
if range_str.starts_with('[') {
return false;
}
if let Some((start, end)) = range_str.split_once('-') {
let Ok(start) = start.parse::<u16>() else {
return false;
};
let Ok(end) = end.parse::<u16>() else {
return false;
};
slot >= start && slot <= end
} else {
range_str.parse::<u16>().ok() == Some(slot)
}
}
fn parse_cluster_node_port(addr: &str) -> Option<u16> {
let host_port = addr.split('@').next()?;
let port_str = host_port.rsplit(':').next()?;
port_str.parse().ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slot_in_range_single() {
assert!(slot_in_range("5461", 5461));
assert!(!slot_in_range("5461", 5462));
}
#[test]
fn slot_in_range_range() {
assert!(slot_in_range("0-5460", 0));
assert!(slot_in_range("0-5460", 5460));
assert!(slot_in_range("0-5460", 1000));
assert!(!slot_in_range("0-5460", 5461));
}
#[test]
fn slot_in_range_import_marker() {
assert!(!slot_in_range("[123->-abc]", 123));
assert!(!slot_in_range("[123-<-abc]", 123));
}
#[test]
fn parse_port_from_cluster_nodes() {
assert_eq!(parse_cluster_node_port("127.0.0.1:7000@17000"), Some(7000));
assert_eq!(parse_cluster_node_port("127.0.0.1:7001@17001"), Some(7001));
assert_eq!(parse_cluster_node_port("garbage"), None);
}
}