use crate::errors::NeedRetry::IdempotentOnly;
use crate::grpc_wrapper::raw_errors::RawError;
use std::fmt::{Debug, Display, Formatter};
use std::sync::Arc;
use ydb_grpc::ydb_proto::status_ids::StatusCode;
pub type YdbResult<T> = std::result::Result<T, YdbError>;
pub type YdbResultWithCustomerErr<T> = std::result::Result<T, YdbOrCustomerError>;
#[derive(Clone)]
pub enum YdbOrCustomerError {
YDB(YdbError),
Customer(Arc<Box<dyn std::error::Error + Send + Sync>>),
}
impl YdbOrCustomerError {
#[allow(dead_code)]
pub(crate) fn from_mess<T: Into<String>>(s: T) -> Self {
Self::Customer(Arc::new(Box::new(YdbError::Custom(s.into()))))
}
pub fn from_err<T: std::error::Error + 'static + Send + Sync>(err: T) -> Self {
Self::Customer(Arc::new(Box::new(err)))
}
pub fn to_ydb_error(self) -> YdbError {
match self {
Self::YDB(err) => err,
Self::Customer(err) => YdbError::custom(format!("{err}")),
}
}
}
impl Debug for YdbOrCustomerError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::YDB(err) => Debug::fmt(err, f),
Self::Customer(err) => Debug::fmt(err, f),
}
}
}
impl Display for YdbOrCustomerError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::YDB(err) => Display::fmt(err, f),
Self::Customer(err) => Display::fmt(err, f),
}
}
}
impl std::error::Error for YdbOrCustomerError {}
impl From<YdbError> for YdbOrCustomerError {
fn from(e: YdbError) -> Self {
Self::YDB(e)
}
}
pub(crate) enum NeedRetry {
True, IdempotentOnly, False, }
#[derive(Clone, Debug)]
#[cfg_attr(not(feature = "force-exhaustive-all"), non_exhaustive)]
pub enum YdbError {
Custom(String),
Convert(String),
NoRows,
InternalError(String),
TransportDial(Arc<tonic::transport::Error>),
Transport(String),
TransportGRPCStatus(Arc<tonic::Status>),
YdbStatusError(YdbStatusError),
}
impl YdbError {
pub(crate) fn custom<T: Into<String>>(message: T) -> Self {
Self::Custom(message.into())
}
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(not(feature = "force-exhaustive-all"), non_exhaustive)]
pub struct YdbStatusError {
#[allow(dead_code)]
pub message: String,
pub operation_status: i32,
pub issues: Vec<YdbIssue>,
}
impl YdbStatusError {
pub fn operation_status(&self) -> YdbResult<StatusCode> {
StatusCode::try_from(self.operation_status)
.map_err(|e| YdbError::InternalError(format!("unknown status code: {e}")))
}
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
#[cfg_attr(not(feature = "force-exhaustive-all"), non_exhaustive)]
pub enum YdbIssueSeverity {
#[default]
Fatal,
Error,
Warning,
Info,
Unknown(u32),
}
impl From<YdbIssueSeverity> for u32 {
fn from(value: YdbIssueSeverity) -> Self {
match value {
YdbIssueSeverity::Fatal => 0,
YdbIssueSeverity::Error => 1,
YdbIssueSeverity::Warning => 2,
YdbIssueSeverity::Info => 3,
YdbIssueSeverity::Unknown(code) => code,
}
}
}
impl From<u32> for YdbIssueSeverity {
fn from(value: u32) -> Self {
match value {
0 => YdbIssueSeverity::Fatal,
1 => YdbIssueSeverity::Error,
2 => YdbIssueSeverity::Warning,
3 => YdbIssueSeverity::Info,
value => YdbIssueSeverity::Unknown(value),
}
}
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(not(feature = "force-exhaustive-all"), non_exhaustive)]
pub struct YdbIssue {
pub issue_code: u32,
pub message: String,
pub issues: Vec<YdbIssue>,
pub severity: YdbIssueSeverity,
}
impl YdbError {
pub(crate) fn from_str<T: Into<String>>(s: T) -> YdbError {
YdbError::Custom(s.into())
}
pub(crate) fn invalidates_server_transaction(&self) -> bool {
let Self::YdbStatusError(status) = self else {
return false;
};
let Ok(code) = status.operation_status() else {
return false;
};
matches!(
code,
StatusCode::BadRequest
| StatusCode::Unauthorized
| StatusCode::InternalError
| StatusCode::Aborted
| StatusCode::SchemeError
| StatusCode::GenericError
| StatusCode::Timeout
| StatusCode::BadSession
| StatusCode::PreconditionFailed
| StatusCode::AlreadyExists
| StatusCode::NotFound
| StatusCode::SessionExpired
| StatusCode::Cancelled
| StatusCode::Unsupported
| StatusCode::SessionBusy
| StatusCode::ExternalError
)
}
pub(crate) fn need_retry(&self) -> NeedRetry {
match self {
Self::Convert(_) => NeedRetry::False,
Self::Custom(_) => NeedRetry::False,
Self::InternalError(_) => NeedRetry::False,
Self::NoRows => NeedRetry::False,
Self::TransportDial(_) => NeedRetry::True,
Self::Transport(_) => IdempotentOnly, Self::TransportGRPCStatus(status) => {
use tonic::Code;
match status.code() {
Code::Aborted | Code::ResourceExhausted => NeedRetry::True,
Code::Internal | Code::Cancelled | Code::Unavailable | Code::Unknown => {
NeedRetry::IdempotentOnly
}
_ => NeedRetry::False,
}
}
Self::YdbStatusError(ydb_err) => {
let Ok(status) = StatusCode::try_from(ydb_err.operation_status) else {
return NeedRetry::False;
};
match status {
StatusCode::Aborted
| StatusCode::Unavailable
| StatusCode::Overloaded
| StatusCode::BadSession
| StatusCode::SessionBusy => NeedRetry::True,
StatusCode::Undetermined => NeedRetry::IdempotentOnly,
_ => NeedRetry::False,
}
}
}
}
}
impl Display for YdbError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self::Debug::fmt(self, f)
}
}
macro_rules! to_custom_ydb_err {
($($t:ty),+) => {
$(
impl From<$t> for YdbError {
fn from(e: $t) -> Self {
return YdbError::Custom(e.to_string());
}
}
)+
};
}
impl std::error::Error for YdbError {}
#[cfg(test)]
mod invalidate_tx_tests {
use super::*;
use ydb_grpc::ydb_proto::status_ids::StatusCode;
fn ydb_status(status: StatusCode) -> YdbError {
YdbError::YdbStatusError(YdbStatusError {
message: "test".into(),
operation_status: status as i32,
issues: vec![],
})
}
#[test]
fn success_does_not_invalidate_server_transaction() {
assert!(!ydb_status(StatusCode::Success).invalidates_server_transaction());
}
#[test]
fn operational_errors_invalidate_server_transaction() {
assert!(ydb_status(StatusCode::PreconditionFailed).invalidates_server_transaction());
assert!(ydb_status(StatusCode::Aborted).invalidates_server_transaction());
assert!(ydb_status(StatusCode::BadSession).invalidates_server_transaction());
}
#[test]
fn ambiguous_and_transport_errors_do_not_invalidate() {
assert!(!ydb_status(StatusCode::Undetermined).invalidates_server_transaction());
assert!(!ydb_status(StatusCode::Unavailable).invalidates_server_transaction());
assert!(!ydb_status(StatusCode::Overloaded).invalidates_server_transaction());
assert!(!YdbError::Transport("timeout".into()).invalidates_server_transaction());
assert!(!YdbError::Custom("x".into()).invalidates_server_transaction());
}
#[test]
fn unknown_status_code_does_not_invalidate() {
let err = YdbError::YdbStatusError(YdbStatusError {
message: "unknown".into(),
operation_status: -1,
issues: vec![],
});
assert!(!err.invalidates_server_transaction());
}
}
to_custom_ydb_err!(
YdbOrCustomerError,
std::convert::Infallible,
http::Error,
http::uri::InvalidUriParts,
reqwest::Error,
serde_json::Error,
std::env::VarError,
std::io::Error,
std::num::TryFromIntError,
std::string::FromUtf8Error,
std::time::SystemTimeError,
&str,
strum::ParseError,
tonic::transport::Error,
tokio::sync::AcquireError,
tokio::sync::oneshot::error::RecvError,
tokio::sync::watch::error::RecvError,
tokio::task::JoinError,
tonic::codegen::http::uri::InvalidUri,
url::ParseError
);
impl From<Box<dyn std::any::Any + Send>> for YdbError {
fn from(e: Box<dyn std::any::Any + Send>) -> Self {
YdbError::Custom(format!("{e:?}"))
}
}
impl<T> From<std::sync::PoisonError<T>> for YdbError {
fn from(e: std::sync::PoisonError<T>) -> Self {
YdbError::Custom(e.to_string())
}
}
impl From<tonic::Status> for YdbError {
fn from(e: tonic::Status) -> Self {
YdbError::TransportGRPCStatus(Arc::new(e))
}
}
impl From<RawError> for YdbError {
fn from(e: RawError) -> Self {
match e {
RawError::Custom(message) => YdbError::Custom(format!("raw custom error: {message}")),
RawError::ProtobufDecodeError(message) => {
YdbError::Custom(format!("decode protobuf error: {message}"))
}
RawError::Transport(message) => YdbError::Transport(message),
RawError::TonicStatus(s) => YdbError::TransportGRPCStatus(Arc::new(*s)),
RawError::YdbStatus(status_error) => YdbError::YdbStatusError(status_error),
}
}
}