use std::error::Error as StdError;
use std::fmt;
use std::fmt::Display;
use std::fmt::Formatter;
use std::io;
use std::result::Result as StdResult;
#[derive(Debug)]
pub struct Error {
kind: ErrorKind,
source: Option<Box<dyn StdError + Send + Sync>>,
}
impl Error {
pub fn is_retryable(&self) -> bool {
matches!(
self.kind,
ErrorKind::Retryable { .. } | ErrorKind::ForwardToLeader { .. }
)
}
pub fn kind(&self) -> &ErrorKind {
&self.kind
}
pub(crate) fn forward_leader_id(&self) -> Option<u64> {
match self.kind {
ErrorKind::ForwardToLeader { leader_id } => leader_id,
_ => None,
}
}
pub(crate) fn retryable<E>(source: E) -> Self
where
E: StdError + Send + Sync + 'static,
{
Self {
kind: ErrorKind::Retryable {
reason: RetryReason::Transient,
},
source: Some(Box::new(source)),
}
}
pub(crate) fn retryable_with_reason(reason: RetryReason) -> Self {
Self {
kind: ErrorKind::Retryable { reason },
source: None,
}
}
pub(crate) fn config(msg: impl Into<String>) -> Self {
Self {
kind: ErrorKind::InvalidConfig(msg.into()),
source: None,
}
}
pub(crate) fn internal(msg: impl Into<String>) -> Self {
Self {
kind: ErrorKind::Internal(msg.into()),
source: None,
}
}
pub(crate) fn internal_with_source<E>(msg: impl Into<String>, source: E) -> Self
where
E: StdError + Send + Sync + 'static,
{
Self {
kind: ErrorKind::Internal(msg.into()),
source: Some(Box::new(source)),
}
}
pub(crate) fn forward_to_leader(leader_id: Option<u64>) -> Self {
Self {
kind: ErrorKind::ForwardToLeader { leader_id },
source: None,
}
}
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.kind)?;
if let Some(ref source) = self.source {
write!(f, ": {}", source)?;
}
Ok(())
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.source.as_ref().map(|s| s.as_ref() as _)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorKind {
Retryable { reason: RetryReason },
ForwardToLeader { leader_id: Option<u64> },
InvalidConfig(String),
Internal(String),
}
impl Display for ErrorKind {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
ErrorKind::Retryable { reason } => write!(f, "retryable error: {}", reason),
ErrorKind::ForwardToLeader { leader_id } => {
write!(f, "forward to leader: {:?}", leader_id)
}
ErrorKind::InvalidConfig(msg) => write!(f, "configuration error: {}", msg),
ErrorKind::Internal(msg) => write!(f, "internal error: {}", msg),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RetryReason {
NoLeader,
LeaderTransition,
Transient,
NodeStarting,
}
impl Display for RetryReason {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
RetryReason::NoLeader => write!(f, "no leader available"),
RetryReason::LeaderTransition => write!(f, "leader transition in progress"),
RetryReason::Transient => write!(f, "temporary failure"),
RetryReason::NodeStarting => write!(f, "target node is starting"),
}
}
}
pub type Result<T> = StdResult<T, Error>;
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum ApiError {
#[error("cannot forward request: {0}")]
CannotForward(String),
#[error("forward to leader: {leader_id:?}")]
ForwardToLeader { leader_id: Option<u64> },
#[error("internal error: {0}")]
Internal(String),
}
impl ApiError {
pub(crate) fn from_error(e: &Error) -> Self {
match e.kind() {
ErrorKind::ForwardToLeader { leader_id } => ApiError::ForwardToLeader {
leader_id: *leader_id,
},
ErrorKind::Retryable { .. } => ApiError::CannotForward(e.to_string()),
ErrorKind::InvalidConfig(_) | ErrorKind::Internal(_) => ApiError::Internal(e.to_string()),
}
}
}
impl From<ApiError> for Error {
fn from(e: ApiError) -> Self {
match e {
ApiError::ForwardToLeader { leader_id } => Self::forward_to_leader(leader_id),
ApiError::CannotForward(_) => Self::retryable_with_reason(RetryReason::Transient),
ApiError::Internal(msg) => Self::internal(msg),
}
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Self::retryable(e)
}
}
impl From<tonic::transport::Error> for Error {
fn from(e: tonic::transport::Error) -> Self {
Self::retryable(e)
}
}
impl From<tonic::Status> for Error {
fn from(status: tonic::Status) -> Self {
match status.code() {
tonic::Code::Unavailable => Self::retryable_with_reason(RetryReason::Transient),
tonic::Code::InvalidArgument => Self::config(status.message()),
_ => Self::internal(format!("gRPC error: {}", status)),
}
}
}
impl From<postcard::Error> for Error {
fn from(e: postcard::Error) -> Self {
Self::internal_with_source("serialization failed", e)
}
}
pub type RockRaftError = Error;
pub type RockRaftResult<T> = Result<T>;
#[cfg(test)]
mod tests {
use super::*;
use crate::raft::types::{decode, encode};
#[test]
fn test_forward_to_leader_kind() {
let err = Error::forward_to_leader(Some(3));
assert!(err.is_retryable());
assert_eq!(err.forward_leader_id(), Some(3));
let err = Error::forward_to_leader(None);
assert!(err.is_retryable());
assert_eq!(err.forward_leader_id(), None);
}
#[test]
fn test_non_redirect_errors_have_no_leader_id() {
assert_eq!(Error::internal("boom").forward_leader_id(), None);
assert_eq!(Error::config("bad").forward_leader_id(), None);
assert!(!Error::internal("boom").is_retryable());
}
#[test]
fn test_api_error_from_error_classification() {
let api = ApiError::from_error(&Error::forward_to_leader(Some(5)));
assert_eq!(api, ApiError::ForwardToLeader { leader_id: Some(5) });
assert!(Error::from(api).is_retryable());
let api = ApiError::from_error(&Error::retryable_with_reason(RetryReason::NoLeader));
assert!(matches!(api, ApiError::CannotForward(_)));
assert!(Error::from(api).is_retryable());
let api = ApiError::from_error(&Error::internal("storage corrupt"));
assert!(matches!(api, ApiError::Internal(_)));
assert!(!Error::from(api).is_retryable());
}
#[test]
fn test_api_error_wire_roundtrip_preserves_redirect() {
let api = ApiError::ForwardToLeader { leader_id: Some(7) };
let bytes = encode(&api).unwrap();
let decoded: ApiError = decode(&bytes).unwrap();
assert_eq!(decoded, api);
let err = Error::from(decoded);
assert_eq!(err.forward_leader_id(), Some(7));
assert!(err.is_retryable());
}
}