Skip to main content

ytsaurus_api/
error.rs

1//! One error type for both transports.
2//!
3//! The implementations have their own — `ytsaurus_client::Error` carries HTTP
4//! status codes, `ytsaurus_rpc::Error` carries a nested `TError` — and neither
5//! belongs in an interface the other implements. This keeps what a caller can
6//! actually act on and carries the original underneath, so nothing is lost.
7
8/// A failure from either transport.
9#[derive(Debug, thiserror::Error)]
10pub enum Error {
11    /// The cluster refused the request.
12    ///
13    /// `code` is the YTsaurus error code where one was reported. Both
14    /// transports produce them from the same table, so a caller can match on
15    /// them without knowing which wire it is on.
16    #[error("{operation} failed: {message}")]
17    Cluster {
18        operation: String,
19        message: String,
20        code: Option<i32>,
21        #[source]
22        source: Option<Box<dyn std::error::Error + Send + Sync>>,
23    },
24
25    /// The cluster could not be reached, or the connection failed mid-request.
26    #[error("{operation}: {message}")]
27    Transport {
28        operation: String,
29        message: String,
30        #[source]
31        source: Option<Box<dyn std::error::Error + Send + Sync>>,
32    },
33
34    /// The request did not complete inside its deadline.
35    #[error("{operation} timed out")]
36    Timeout { operation: String },
37
38    /// A value or row could not be converted between this interface's model and
39    /// the transport's.
40    #[error("{0}")]
41    Conversion(String),
42
43    /// The transport does not implement this, and says so rather than pretending.
44    #[error("{transport} does not support {what}")]
45    Unsupported {
46        transport: crate::Transport,
47        what: &'static str,
48    },
49}
50
51impl Error {
52    /// The YTsaurus error code, if the cluster reported one.
53    pub fn code(&self) -> Option<i32> {
54        match self {
55            Self::Cluster { code, .. } => *code,
56            _ => None,
57        }
58    }
59
60    /// Whether this is worth trying again.
61    ///
62    /// Deliberately coarse: a transport failure or a timeout may succeed on a
63    /// second attempt, and a refusal from the cluster generally will not. A
64    /// caller that needs the real retry rules should use the transport's own
65    /// client, which has them.
66    pub fn is_retryable(&self) -> bool {
67        matches!(self, Self::Transport { .. } | Self::Timeout { .. })
68    }
69
70    /// Builds a cluster refusal.
71    pub fn cluster(
72        operation: impl Into<String>,
73        message: impl Into<String>,
74        code: Option<i32>,
75    ) -> Self {
76        Self::Cluster {
77            operation: operation.into(),
78            message: message.into(),
79            code,
80            source: None,
81        }
82    }
83
84    /// Builds a cluster refusal that keeps the transport's own error.
85    pub fn cluster_from(
86        operation: impl Into<String>,
87        code: Option<i32>,
88        source: impl std::error::Error + Send + Sync + 'static,
89    ) -> Self {
90        Self::Cluster {
91            operation: operation.into(),
92            message: source.to_string(),
93            code,
94            source: Some(Box::new(source)),
95        }
96    }
97
98    /// Builds a transport failure that keeps the transport's own error.
99    pub fn transport_from(
100        operation: impl Into<String>,
101        source: impl std::error::Error + Send + Sync + 'static,
102    ) -> Self {
103        Self::Transport {
104            operation: operation.into(),
105            message: source.to_string(),
106            source: Some(Box::new(source)),
107        }
108    }
109}
110
111/// The interface's result type.
112pub type Result<T> = std::result::Result<T, Error>;
113
114/// Error codes both transports report, for callers that match on them.
115///
116/// The values are YTsaurus's own, from the same table the C++ and Go clients
117/// use; they do not depend on the transport.
118pub mod codes {
119    pub const OK: i32 = 0;
120    pub const GENERIC: i32 = 1;
121    pub const TIMEOUT: i32 = 3;
122    pub const RESOLVE_ERROR: i32 = 500;
123    pub const AUTHENTICATION_ERROR: i32 = 900;
124    pub const NO_SUCH_TRANSACTION: i32 = 11000;
125    /// The table is not mounted, which is the first thing to check when a
126    /// dynamic-table call fails on a cluster that was just set up.
127    pub const TABLET_NOT_MOUNTED: i32 = 1702;
128}
129
130/// Renders a chain of sources, which is where the transport's own detail lives.
131pub fn describe(error: &dyn std::error::Error) -> String {
132    let mut description = error.to_string();
133    let mut current = error.source();
134    while let Some(cause) = current {
135        description.push_str("\n  caused by: ");
136        description.push_str(&cause.to_string());
137        current = cause.source();
138    }
139    description
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[derive(Debug, thiserror::Error)]
147    #[error("the underlying thing broke")]
148    struct Underlying;
149
150    #[test]
151    fn a_cluster_refusal_keeps_its_code() {
152        let error = Error::cluster("lookup_rows", "no such table", Some(codes::RESOLVE_ERROR));
153        assert_eq!(error.code(), Some(codes::RESOLVE_ERROR));
154        assert!(!error.is_retryable(), "a refusal will refuse again");
155        assert!(error.to_string().contains("lookup_rows failed"));
156    }
157
158    #[test]
159    fn a_transport_failure_is_worth_retrying() {
160        let error = Error::transport_from("select_rows", Underlying);
161        assert!(error.is_retryable());
162        assert_eq!(error.code(), None);
163    }
164
165    #[test]
166    fn the_original_error_survives_underneath() {
167        let error = Error::cluster_from("insert_rows", Some(1), Underlying);
168        let described = describe(&error);
169        assert!(
170            described.contains("the underlying thing broke"),
171            "the transport's own error must not be thrown away: {described}"
172        );
173    }
174
175    #[test]
176    fn an_unsupported_operation_names_the_transport() {
177        let error = Error::Unsupported {
178            transport: crate::Transport::Rpc,
179            what: "operations",
180        };
181        assert_eq!(error.to_string(), "RPC does not support operations");
182    }
183}