Skip to main content

ytsaurus_rpc/
error.rs

1//! The error model.
2//!
3//! A server-side failure arrives as `TError`
4//! (`yt_proto/yt/core/misc/proto/error.proto`): a code, a message, an
5//! attribute dictionary and — the part that matters — a list of *inner*
6//! errors. YTsaurus nests errors deeply, and the innermost one is usually the
7//! only one that says what actually went wrong: the outer layers say "lookup
8//! failed", "tablet request failed", and the innermost says "no such table".
9//! Flattening that to a string loses the diagnosis, so [`YtError`] keeps the
10//! tree and [`YtError::find`] walks it.
11
12use std::fmt;
13
14use crate::guid::Guid;
15
16/// Error codes worth naming. Values are from `yt/go/yterrors/error_code.go`,
17/// which is generated from the same C++ headers the proxy uses.
18pub mod codes {
19    pub const OK: i32 = 0;
20    pub const GENERIC: i32 = 1;
21    pub const CANCELED: i32 = 2;
22    pub const TIMEOUT: i32 = 3;
23    pub const TRANSPORT_ERROR: i32 = 100;
24    pub const UNAVAILABLE: i32 = 105;
25    pub const REQUEST_QUEUE_SIZE_LIMIT_EXCEEDED: i32 = 108;
26    pub const RPC_AUTHENTICATION_ERROR: i32 = 109;
27    pub const RESOLVE_ERROR: i32 = 500;
28    pub const AUTHENTICATION_ERROR: i32 = 900;
29    pub const NO_SUCH_TRANSACTION: i32 = 11000;
30}
31
32/// An error reported by the server, with its nesting preserved.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct YtError {
35    pub code: i32,
36    pub message: String,
37    /// Attribute values are YSON, exactly as the wire carries them; they are
38    /// kept as bytes so nothing is lost to a decoder this crate does not need.
39    pub attributes: Vec<(String, Vec<u8>)>,
40    pub inner_errors: Vec<YtError>,
41}
42
43impl YtError {
44    /// Reads the protobuf form, keeping the whole tree.
45    pub fn from_proto(proto: &crate::proto::misc::TError) -> Self {
46        Self {
47            code: proto.code,
48            message: proto.message.clone().unwrap_or_default(),
49            attributes: proto
50                .attributes
51                .as_ref()
52                .map(|dictionary| {
53                    dictionary
54                        .attributes
55                        .iter()
56                        .map(|attribute| (attribute.key.clone(), attribute.value.clone()))
57                        .collect()
58                })
59                .unwrap_or_default(),
60            inner_errors: proto.inner_errors.iter().map(Self::from_proto).collect(),
61        }
62    }
63
64    /// The first error in the tree with this code, outermost first.
65    ///
66    /// The useful question is almost never "what is the outer code" but "is
67    /// this failure a `NoSuchTransaction` anywhere inside", because retry and
68    /// reporting decisions hang on the inner code.
69    pub fn find(&self, code: i32) -> Option<&YtError> {
70        if self.code == code {
71            return Some(self);
72        }
73        self.inner_errors.iter().find_map(|inner| inner.find(code))
74    }
75
76    /// Whether this error or any error nested inside it has this code.
77    pub fn has_code(&self, code: i32) -> bool {
78        self.find(code).is_some()
79    }
80
81    /// The innermost error along the first chain of inner errors — usually the
82    /// one that names the real cause.
83    pub fn innermost(&self) -> &YtError {
84        let mut current = self;
85        while let Some(first) = current.inner_errors.first() {
86            current = first;
87        }
88        current
89    }
90
91    /// The value of an attribute, as raw YSON bytes.
92    pub fn attribute(&self, key: &str) -> Option<&[u8]> {
93        self.attributes
94            .iter()
95            .find(|(name, _)| name == key)
96            .map(|(_, value)| value.as_slice())
97    }
98}
99
100impl fmt::Display for YtError {
101    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
102        write!(formatter, "{} (code {})", self.message, self.code)?;
103        // Nesting is printed as an indented tree; a one-line rendering of a
104        // four-deep YTsaurus error is unreadable, and the depth is the
105        // diagnosis.
106        for inner in &self.inner_errors {
107            let rendered = inner.to_string();
108            for line in rendered.lines() {
109                write!(formatter, "\n    {line}")?;
110            }
111        }
112        Ok(())
113    }
114}
115
116impl std::error::Error for YtError {}
117
118/// Anything that can go wrong in this crate.
119#[derive(Debug, thiserror::Error)]
120pub enum Error {
121    /// The server answered, and the answer was a failure.
122    ///
123    /// Boxed: a `YtError` carries a whole error tree with its attributes, and
124    /// leaving it inline would make every `Result` in the crate the size of the
125    /// largest failure it can report.
126    #[error("{service}.{method} failed: {error}")]
127    Response {
128        service: String,
129        method: String,
130        #[source]
131        error: Box<YtError>,
132    },
133
134    /// The bytes on the connection were not a well-formed packet.
135    #[error("bus protocol error: {0}")]
136    Packet(#[from] crate::bus::packet::PacketError),
137
138    /// The connection failed, or was never established.
139    #[error("connection to {address} failed: {source}")]
140    Connect {
141        address: String,
142        #[source]
143        source: std::io::Error,
144    },
145
146    /// I/O on an established connection failed.
147    #[error("connection lost: {0}")]
148    Io(#[from] std::io::Error),
149
150    /// The peer sent something structurally valid but not what the protocol
151    /// allows here.
152    #[error("protocol violation: {0}")]
153    Protocol(String),
154
155    /// A protobuf message did not parse.
156    #[error("could not decode {message}: {source}")]
157    Decode {
158        message: &'static str,
159        #[source]
160        source: prost::DecodeError,
161    },
162
163    /// The request did not complete inside its deadline.
164    #[error("{service}.{method} timed out after {timeout:?}")]
165    Timeout {
166        service: String,
167        method: String,
168        timeout: std::time::Duration,
169    },
170
171    /// The connection was closed while this request was in flight.
172    #[error("connection closed with request {request_id} in flight")]
173    ConnectionClosed { request_id: Guid },
174
175    /// A rowset could not be decoded.
176    #[error("row wire format: {0}")]
177    Wire(#[from] crate::wire::WireError),
178}
179
180impl Error {
181    /// The server-reported error, if this failure came from the server.
182    pub fn yt_error(&self) -> Option<&YtError> {
183        match self {
184            Self::Response { error, .. } => Some(error),
185            _ => None,
186        }
187    }
188
189    /// Builds a response failure.
190    pub(crate) fn response(service: &str, method: &str, error: YtError) -> Self {
191        Self::Response {
192            service: service.to_owned(),
193            method: method.to_owned(),
194            error: Box::new(error),
195        }
196    }
197
198    /// Whether this failure carries the given YTsaurus error code anywhere in
199    /// its nesting.
200    pub fn has_code(&self, code: i32) -> bool {
201        self.yt_error().is_some_and(|error| error.has_code(code))
202    }
203}
204
205/// The crate's result type.
206pub type Result<T> = std::result::Result<T, Error>;
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::proto;
212
213    fn proto_error(
214        code: i32,
215        message: &str,
216        inner: Vec<proto::misc::TError>,
217    ) -> proto::misc::TError {
218        proto::misc::TError {
219            code,
220            message: Some(message.to_owned()),
221            attributes: None,
222            inner_errors: inner,
223        }
224    }
225
226    #[test]
227    fn nesting_survives_the_conversion() {
228        let wire = proto_error(
229            1,
230            "lookup failed",
231            vec![proto_error(
232                1,
233                "tablet request failed",
234                vec![proto_error(codes::RESOLVE_ERROR, "no such table", vec![])],
235            )],
236        );
237
238        let error = YtError::from_proto(&wire);
239        assert_eq!(error.message, "lookup failed");
240        assert_eq!(error.inner_errors.len(), 1);
241        assert_eq!(error.innermost().message, "no such table");
242        assert_eq!(error.innermost().code, codes::RESOLVE_ERROR);
243    }
244
245    #[test]
246    fn find_reaches_an_inner_code() {
247        let wire = proto_error(
248            1,
249            "outer",
250            vec![proto_error(
251                codes::NO_SUCH_TRANSACTION,
252                "no such transaction",
253                vec![],
254            )],
255        );
256        let error = YtError::from_proto(&wire);
257
258        assert!(error.has_code(codes::NO_SUCH_TRANSACTION));
259        assert_eq!(
260            error.find(codes::NO_SUCH_TRANSACTION).unwrap().message,
261            "no such transaction"
262        );
263        assert!(!error.has_code(codes::TIMEOUT));
264        assert!(error.find(codes::TIMEOUT).is_none());
265    }
266
267    #[test]
268    fn display_indents_the_tree() {
269        let wire = proto_error(1, "outer", vec![proto_error(500, "inner", vec![])]);
270        let rendered = YtError::from_proto(&wire).to_string();
271        assert_eq!(rendered, "outer (code 1)\n    inner (code 500)");
272    }
273
274    #[test]
275    fn attributes_are_kept_as_yson_bytes() {
276        let wire = proto::misc::TError {
277            code: 500,
278            message: Some("no such node".to_owned()),
279            attributes: Some(proto::ytree::TAttributeDictionary {
280                attributes: vec![proto::ytree::TAttribute {
281                    key: "path".to_owned(),
282                    value: b"\x01\x0c//tmp/nope".to_vec(),
283                }],
284            }),
285            inner_errors: vec![],
286        };
287        let error = YtError::from_proto(&wire);
288        assert_eq!(error.attribute("path"), Some(&b"\x01\x0c//tmp/nope"[..]));
289        assert_eq!(error.attribute("missing"), None);
290    }
291
292    #[test]
293    fn a_missing_message_reads_as_empty() {
294        let wire = proto::misc::TError {
295            code: codes::GENERIC,
296            message: None,
297            attributes: None,
298            inner_errors: vec![],
299        };
300        let error = YtError::from_proto(&wire);
301        assert_eq!(error.code, codes::GENERIC);
302        assert_eq!(error.message, "");
303    }
304
305    #[test]
306    fn has_code_reaches_through_the_crate_error() {
307        let error = Error::response(
308            "ApiService",
309            "LookupRows",
310            YtError::from_proto(&proto_error(
311                1,
312                "outer",
313                vec![proto_error(codes::NO_SUCH_TRANSACTION, "gone", vec![])],
314            )),
315        );
316        assert!(error.has_code(codes::NO_SUCH_TRANSACTION));
317        assert!(!error.has_code(codes::TIMEOUT));
318        assert!(error.yt_error().is_some());
319        assert!(error.to_string().contains("ApiService.LookupRows failed"));
320    }
321}