use std::{collections::BTreeMap, fmt};
use crate::cache_redis::RedisCacheError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum RedisClusterRedirectKind {
Moved,
Ask,
}
impl RedisClusterRedirectKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Moved => "moved",
Self::Ask => "ask",
}
}
}
impl fmt::Display for RedisClusterRedirectKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RedisClusterRedirect {
pub kind: RedisClusterRedirectKind,
pub slot: u16,
pub endpoint: String,
}
impl RedisClusterRedirect {
pub fn parse(message: &str) -> Option<Self> {
let mut parts = message.split_whitespace();
let kind = match parts.next()? {
"MOVED" => RedisClusterRedirectKind::Moved,
"ASK" => RedisClusterRedirectKind::Ask,
_ => return None,
};
let slot = parts.next()?.parse::<u16>().ok()?;
let endpoint = parts.next()?.to_string();
if endpoint.is_empty() || parts.next().is_some() {
return None;
}
Some(Self {
kind,
slot,
endpoint,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RedisClusterRouteAction {
RefreshSlot,
AskRedirect,
}
#[derive(Debug, Clone, Default)]
pub struct RedisClusterSlotMap {
slots: BTreeMap<u16, String>,
}
impl RedisClusterSlotMap {
pub fn new() -> Self {
Self::default()
}
pub fn endpoint_for_slot(&self, slot: u16) -> Option<&str> {
self.slots.get(&slot).map(String::as_str)
}
pub fn set_slot(&mut self, slot: u16, endpoint: impl Into<String>) {
self.slots.insert(slot, endpoint.into());
}
pub fn apply_redirect(&mut self, redirect: &RedisClusterRedirect) -> RedisClusterRouteAction {
match redirect.kind {
RedisClusterRedirectKind::Moved => {
self.set_slot(redirect.slot, redirect.endpoint.clone());
RedisClusterRouteAction::RefreshSlot
}
RedisClusterRedirectKind::Ask => RedisClusterRouteAction::AskRedirect,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum RedisCommandOutcome {
Success,
Timeout,
Error,
NoScript,
Redirect(RedisClusterRedirectKind),
Degraded,
BreakerRejected,
}
impl RedisCommandOutcome {
pub fn as_str(self) -> &'static str {
match self {
Self::Success => "success",
Self::Timeout => "timeout",
Self::Error => "error",
Self::NoScript => "noscript",
Self::Redirect(kind) => kind.as_str(),
Self::Degraded => "degraded",
Self::BreakerRejected => "breaker_rejected",
}
}
pub fn is_noscript(self) -> bool {
matches!(self, Self::NoScript)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum RedisCommandEventKind {
Command,
Pool,
Redirect,
Script,
Degradation,
Breaker,
}
impl RedisCommandEventKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Command => "command",
Self::Pool => "pool",
Self::Redirect => "redirect",
Self::Script => "script",
Self::Degradation => "degradation",
Self::Breaker => "breaker",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RedisCommandEvent {
pub event: RedisCommandEventKind,
pub outcome: RedisCommandOutcome,
}
impl RedisCommandEvent {
pub fn new(event: RedisCommandEventKind, outcome: RedisCommandOutcome) -> Self {
Self { event, outcome }
}
pub fn redirect(kind: RedisClusterRedirectKind) -> Self {
Self::new(
RedisCommandEventKind::Redirect,
RedisCommandOutcome::Redirect(kind),
)
}
}
pub fn classify_redis_error(error: &RedisCacheError) -> RedisCommandOutcome {
match error {
RedisCacheError::Timeout(_) => RedisCommandOutcome::Timeout,
RedisCacheError::BreakerOpen(_) => RedisCommandOutcome::BreakerRejected,
RedisCacheError::ClusterRedirect(redirect) => RedisCommandOutcome::Redirect(redirect.kind),
RedisCacheError::Backend(message) if is_noscript_message(message) => {
RedisCommandOutcome::NoScript
}
RedisCacheError::Backend(message) => RedisClusterRedirect::parse(message)
.map(|redirect| RedisCommandOutcome::Redirect(redirect.kind))
.unwrap_or(RedisCommandOutcome::Error),
_ => RedisCommandOutcome::Error,
}
}
pub(crate) fn is_noscript_message(message: &str) -> bool {
let normalized = message.trim_start().to_ascii_uppercase();
normalized.starts_with("NOSCRIPT")
|| normalized.contains(" NOSCRIPT")
|| normalized.contains("NOSCRIPTERROR")
}