1#[derive(Debug, thiserror::Error)]
10pub enum Error {
11 #[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 #[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 #[error("{operation} timed out")]
36 Timeout { operation: String },
37
38 #[error("{0}")]
41 Conversion(String),
42
43 #[error("{transport} does not support {what}")]
45 Unsupported {
46 transport: crate::Transport,
47 what: &'static str,
48 },
49}
50
51impl Error {
52 pub fn code(&self) -> Option<i32> {
54 match self {
55 Self::Cluster { code, .. } => *code,
56 _ => None,
57 }
58 }
59
60 pub fn is_retryable(&self) -> bool {
67 matches!(self, Self::Transport { .. } | Self::Timeout { .. })
68 }
69
70 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 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 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
111pub type Result<T> = std::result::Result<T, Error>;
113
114pub 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 pub const TABLET_NOT_MOUNTED: i32 = 1702;
128}
129
130pub 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}