#[cfg(feature = "redis")]
use redis::{ErrorKind, RedisError, RetryMethod, ServerErrorKind};
#[cfg(feature = "redis")]
use crate::{AppErrorKind, Context, Error, field};
#[cfg(feature = "redis")]
#[cfg_attr(docsrs, doc(cfg(feature = "redis")))]
impl From<RedisError> for Error {
fn from(err: RedisError) -> Self {
let (context, retry_after) = build_context(&err);
let mut error = context.into_error(err);
if let Some(secs) = retry_after {
error = error.with_retry_after_secs(secs);
}
error
}
}
#[cfg(feature = "redis")]
fn build_context(err: &RedisError) -> (Context, Option<u64>) {
let mut context = Context::new(AppErrorKind::Cache)
.with(field::str("redis.kind", format!("{:?}", err.kind())))
.with(field::str("redis.category", err.category().to_owned()))
.with(field::bool("redis.is_timeout", err.is_timeout()))
.with(field::bool(
"redis.is_cluster_error",
err.is_cluster_error()
))
.with(field::bool(
"redis.is_connection_refused",
err.is_connection_refusal()
))
.with(field::bool(
"redis.is_connection_dropped",
err.is_connection_dropped()
));
if let Some(code) = err.code() {
context = context.with(field::str("redis.code", code.to_owned()));
}
if err.is_timeout() {
context = context.category(AppErrorKind::Timeout);
} else if err.is_connection_refusal()
|| err.is_connection_dropped()
|| err.is_cluster_error()
|| err.is_io_error()
|| is_busy_loading(err)
{
context = context.category(AppErrorKind::DependencyUnavailable);
}
if let Some((addr, slot)) = err.redirect_node() {
context = context
.with(field::str("redis.redirect_addr", addr.to_owned()))
.with(field::u64("redis.redirect_slot", u64::from(slot)));
}
let (retry_method_label, retry_after) = retry_method_details(err.retry_method());
context = context.with(field::str("redis.retry_method", retry_method_label));
if let Some(secs) = retry_after {
context = context.with(field::u64("redis.retry_after_hint_secs", secs));
}
(context, retry_after)
}
#[cfg(feature = "redis")]
fn is_busy_loading(err: &RedisError) -> bool {
err.kind() == ErrorKind::Server(ServerErrorKind::BusyLoading)
}
#[cfg(feature = "redis")]
const fn retry_method_details(method: RetryMethod) -> (&'static str, Option<u64>) {
match method {
RetryMethod::NoRetry => ("NoRetry", None),
RetryMethod::RetryImmediately => ("RetryImmediately", Some(0)),
RetryMethod::AskRedirect => ("AskRedirect", Some(0)),
RetryMethod::MovedRedirect => ("MovedRedirect", Some(0)),
RetryMethod::Reconnect => ("Reconnect", Some(1)),
RetryMethod::ReconnectFromInitialConnections => {
("ReconnectFromInitialConnections", Some(1))
}
RetryMethod::WaitAndRetry => ("WaitAndRetry", Some(2)),
_ => ("Other", None)
}
}
#[cfg(all(test, feature = "redis"))]
mod tests {
use redis::ErrorKind;
use super::*;
use crate::{AppErrorKind, FieldValue};
#[test]
fn maps_io_error_to_dependency_unavailable() {
let redis_err = RedisError::from((ErrorKind::Io, "boom"));
let app_err: Error = redis_err.into();
assert!(matches!(app_err.kind, AppErrorKind::DependencyUnavailable));
let metadata = app_err.metadata();
assert_eq!(
metadata.get("redis.kind"),
Some(&FieldValue::Str("Io".into()))
);
}
#[test]
fn maps_client_error_to_cache() {
let redis_err = RedisError::from((ErrorKind::Client, "bad config"));
let app_err: Error = redis_err.into();
assert!(matches!(app_err.kind, AppErrorKind::Cache));
}
}