use std::fmt;
use crate::guid::Guid;
pub mod codes {
pub const OK: i32 = 0;
pub const GENERIC: i32 = 1;
pub const CANCELED: i32 = 2;
pub const TIMEOUT: i32 = 3;
pub const TRANSPORT_ERROR: i32 = 100;
pub const UNAVAILABLE: i32 = 105;
pub const REQUEST_QUEUE_SIZE_LIMIT_EXCEEDED: i32 = 108;
pub const RPC_AUTHENTICATION_ERROR: i32 = 109;
pub const RESOLVE_ERROR: i32 = 500;
pub const AUTHENTICATION_ERROR: i32 = 900;
pub const NO_SUCH_TRANSACTION: i32 = 11000;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct YtError {
pub code: i32,
pub message: String,
pub attributes: Vec<(String, Vec<u8>)>,
pub inner_errors: Vec<YtError>,
}
impl YtError {
pub fn from_proto(proto: &crate::proto::misc::TError) -> Self {
Self {
code: proto.code,
message: proto.message.clone().unwrap_or_default(),
attributes: proto
.attributes
.as_ref()
.map(|dictionary| {
dictionary
.attributes
.iter()
.map(|attribute| (attribute.key.clone(), attribute.value.clone()))
.collect()
})
.unwrap_or_default(),
inner_errors: proto.inner_errors.iter().map(Self::from_proto).collect(),
}
}
pub fn find(&self, code: i32) -> Option<&YtError> {
if self.code == code {
return Some(self);
}
self.inner_errors.iter().find_map(|inner| inner.find(code))
}
pub fn has_code(&self, code: i32) -> bool {
self.find(code).is_some()
}
pub fn innermost(&self) -> &YtError {
let mut current = self;
while let Some(first) = current.inner_errors.first() {
current = first;
}
current
}
pub fn attribute(&self, key: &str) -> Option<&[u8]> {
self.attributes
.iter()
.find(|(name, _)| name == key)
.map(|(_, value)| value.as_slice())
}
}
impl fmt::Display for YtError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{} (code {})", self.message, self.code)?;
for inner in &self.inner_errors {
let rendered = inner.to_string();
for line in rendered.lines() {
write!(formatter, "\n {line}")?;
}
}
Ok(())
}
}
impl std::error::Error for YtError {}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{service}.{method} failed: {error}")]
Response {
service: String,
method: String,
#[source]
error: Box<YtError>,
},
#[error("bus protocol error: {0}")]
Packet(#[from] crate::bus::packet::PacketError),
#[error("connection to {address} failed: {source}")]
Connect {
address: String,
#[source]
source: std::io::Error,
},
#[error("connection lost: {0}")]
Io(#[from] std::io::Error),
#[error("protocol violation: {0}")]
Protocol(String),
#[error("could not decode {message}: {source}")]
Decode {
message: &'static str,
#[source]
source: prost::DecodeError,
},
#[error("{service}.{method} timed out after {timeout:?}")]
Timeout {
service: String,
method: String,
timeout: std::time::Duration,
},
#[error("connection closed with request {request_id} in flight")]
ConnectionClosed { request_id: Guid },
#[error("row wire format: {0}")]
Wire(#[from] crate::wire::WireError),
}
impl Error {
pub fn yt_error(&self) -> Option<&YtError> {
match self {
Self::Response { error, .. } => Some(error),
_ => None,
}
}
pub(crate) fn response(service: &str, method: &str, error: YtError) -> Self {
Self::Response {
service: service.to_owned(),
method: method.to_owned(),
error: Box::new(error),
}
}
pub fn has_code(&self, code: i32) -> bool {
self.yt_error().is_some_and(|error| error.has_code(code))
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
use crate::proto;
fn proto_error(
code: i32,
message: &str,
inner: Vec<proto::misc::TError>,
) -> proto::misc::TError {
proto::misc::TError {
code,
message: Some(message.to_owned()),
attributes: None,
inner_errors: inner,
}
}
#[test]
fn nesting_survives_the_conversion() {
let wire = proto_error(
1,
"lookup failed",
vec![proto_error(
1,
"tablet request failed",
vec![proto_error(codes::RESOLVE_ERROR, "no such table", vec![])],
)],
);
let error = YtError::from_proto(&wire);
assert_eq!(error.message, "lookup failed");
assert_eq!(error.inner_errors.len(), 1);
assert_eq!(error.innermost().message, "no such table");
assert_eq!(error.innermost().code, codes::RESOLVE_ERROR);
}
#[test]
fn find_reaches_an_inner_code() {
let wire = proto_error(
1,
"outer",
vec![proto_error(
codes::NO_SUCH_TRANSACTION,
"no such transaction",
vec![],
)],
);
let error = YtError::from_proto(&wire);
assert!(error.has_code(codes::NO_SUCH_TRANSACTION));
assert_eq!(
error.find(codes::NO_SUCH_TRANSACTION).unwrap().message,
"no such transaction"
);
assert!(!error.has_code(codes::TIMEOUT));
assert!(error.find(codes::TIMEOUT).is_none());
}
#[test]
fn display_indents_the_tree() {
let wire = proto_error(1, "outer", vec![proto_error(500, "inner", vec![])]);
let rendered = YtError::from_proto(&wire).to_string();
assert_eq!(rendered, "outer (code 1)\n inner (code 500)");
}
#[test]
fn attributes_are_kept_as_yson_bytes() {
let wire = proto::misc::TError {
code: 500,
message: Some("no such node".to_owned()),
attributes: Some(proto::ytree::TAttributeDictionary {
attributes: vec![proto::ytree::TAttribute {
key: "path".to_owned(),
value: b"\x01\x0c//tmp/nope".to_vec(),
}],
}),
inner_errors: vec![],
};
let error = YtError::from_proto(&wire);
assert_eq!(error.attribute("path"), Some(&b"\x01\x0c//tmp/nope"[..]));
assert_eq!(error.attribute("missing"), None);
}
#[test]
fn a_missing_message_reads_as_empty() {
let wire = proto::misc::TError {
code: codes::GENERIC,
message: None,
attributes: None,
inner_errors: vec![],
};
let error = YtError::from_proto(&wire);
assert_eq!(error.code, codes::GENERIC);
assert_eq!(error.message, "");
}
#[test]
fn has_code_reaches_through_the_crate_error() {
let error = Error::response(
"ApiService",
"LookupRows",
YtError::from_proto(&proto_error(
1,
"outer",
vec![proto_error(codes::NO_SUCH_TRANSACTION, "gone", vec![])],
)),
);
assert!(error.has_code(codes::NO_SUCH_TRANSACTION));
assert!(!error.has_code(codes::TIMEOUT));
assert!(error.yt_error().is_some());
assert!(error.to_string().contains("ApiService.LookupRows failed"));
}
}