1use std::fmt;
16
17use strum::{AsRefStr, EnumIter, EnumString, FromRepr};
18use tonic::Code;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, AsRefStr, EnumIter, FromRepr)]
22pub enum StatusCode {
23 Success = 0,
26
27 Unknown = 1000,
29 Unsupported = 1001,
31 Unexpected = 1002,
33 Internal = 1003,
35 InvalidArguments = 1004,
37 Cancelled = 1005,
39 IllegalState = 1006,
41 External = 1007,
43 DeadlineExceeded = 1008,
45 Suspended = 1009,
47 InvalidSyntax = 2000,
52 PlanQuery = 3000,
57 EngineExecuteQuery = 3001,
59 TableAlreadyExists = 4000,
64 TableNotFound = 4001,
66 TableColumnNotFound = 4002,
68 TableColumnExists = 4003,
70 DatabaseNotFound = 4004,
72 RegionNotFound = 4005,
74 RegionAlreadyExists = 4006,
76 RegionReadonly = 4007,
78 RegionNotReady = 4008,
80 RegionBusy = 4009,
82 TableUnavailable = 4010,
84 DatabaseAlreadyExists = 4011,
86 StorageUnavailable = 5000,
91 RequestOutdated = 5001,
93 RuntimeResourcesExhausted = 6000,
98
99 RateLimited = 6001,
101 UserNotFound = 7000,
106 UnsupportedPasswordType = 7001,
108 UserPasswordMismatch = 7002,
110 AuthHeaderNotFound = 7003,
112 InvalidAuthHeader = 7004,
114 AccessDenied = 7005,
116 PermissionDenied = 7006,
118 FlowAlreadyExists = 8000,
122 FlowNotFound = 8001,
123 TriggerAlreadyExists = 9000,
127 TriggerNotFound = 9001,
128 }
130
131impl StatusCode {
132 pub fn is_success(code: u32) -> bool {
134 Self::Success as u32 == code
135 }
136
137 pub fn should_log_error(&self) -> bool {
140 match self {
141 StatusCode::Unknown
142 | StatusCode::Unexpected
143 | StatusCode::Internal
144 | StatusCode::Cancelled
145 | StatusCode::DeadlineExceeded
146 | StatusCode::IllegalState
147 | StatusCode::EngineExecuteQuery
148 | StatusCode::StorageUnavailable
149 | StatusCode::RuntimeResourcesExhausted
150 | StatusCode::External => true,
151
152 StatusCode::Success
153 | StatusCode::Unsupported
154 | StatusCode::InvalidArguments
155 | StatusCode::InvalidSyntax
156 | StatusCode::TableAlreadyExists
157 | StatusCode::TableNotFound
158 | StatusCode::RegionAlreadyExists
159 | StatusCode::RegionNotFound
160 | StatusCode::PlanQuery
161 | StatusCode::FlowAlreadyExists
162 | StatusCode::FlowNotFound
163 | StatusCode::TriggerAlreadyExists
164 | StatusCode::TriggerNotFound
165 | StatusCode::RegionNotReady
166 | StatusCode::RegionBusy
167 | StatusCode::RegionReadonly
168 | StatusCode::TableColumnNotFound
169 | StatusCode::TableColumnExists
170 | StatusCode::DatabaseNotFound
171 | StatusCode::RateLimited
172 | StatusCode::UserNotFound
173 | StatusCode::TableUnavailable
174 | StatusCode::DatabaseAlreadyExists
175 | StatusCode::UnsupportedPasswordType
176 | StatusCode::UserPasswordMismatch
177 | StatusCode::AuthHeaderNotFound
178 | StatusCode::InvalidAuthHeader
179 | StatusCode::AccessDenied
180 | StatusCode::PermissionDenied
181 | StatusCode::RequestOutdated
182 | StatusCode::Suspended => false,
183 }
184 }
185
186 pub fn from_u32(value: u32) -> Option<Self> {
187 StatusCode::from_repr(value as usize)
188 }
189}
190
191impl fmt::Display for StatusCode {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 write!(f, "{self:?}")
195 }
196}
197
198#[macro_export]
199macro_rules! define_from_tonic_status {
200 ($Error: ty, $Variant: ident) => {
201 impl From<tonic::Status> for $Error {
202 fn from(e: tonic::Status) -> Self {
203 use snafu::location;
204
205 fn metadata_value(e: &tonic::Status, key: &str) -> Option<String> {
206 e.metadata()
207 .get(key)
208 .and_then(|v| String::from_utf8(v.as_bytes().to_vec()).ok())
209 }
210 let code = metadata_value(&e, $crate::GREPTIME_DB_HEADER_ERROR_CODE)
211 .and_then(|s| {
212 if let Ok(code) = s.parse::<u32>() {
213 StatusCode::from_u32(code)
214 } else {
215 None
216 }
217 })
218 .unwrap_or_else(|| match e.code() {
219 tonic::Code::Cancelled => StatusCode::Cancelled,
220 tonic::Code::DeadlineExceeded => StatusCode::DeadlineExceeded,
221 _ => StatusCode::Internal,
222 });
223
224 let msg = metadata_value(&e, $crate::GREPTIME_DB_HEADER_ERROR_MSG)
225 .unwrap_or_else(|| e.message().to_string());
226 let retry_hint = metadata_value(&e, $crate::GREPTIME_DB_HEADER_ERROR_RETRY_HINT)
227 .and_then(|s| s.parse().ok())
228 .unwrap_or($crate::ext::RetryHint::NonRetryable);
229
230 Self::$Variant {
232 code,
233 msg,
234 tonic_code: e.code(),
235 retry_hint,
236 location: location!(),
237 }
238 }
239 }
240 };
241}
242
243#[macro_export]
244macro_rules! define_into_tonic_status {
245 ($Error: ty) => {
246 impl From<$Error> for tonic::Status {
247 fn from(err: $Error) -> Self {
248 use tonic::codegen::http::{HeaderMap, HeaderValue};
249 use tonic::metadata::MetadataMap;
250 use $crate::{
251 GREPTIME_DB_HEADER_ERROR_CODE, GREPTIME_DB_HEADER_ERROR_RETRY_HINT,
252 };
253
254 common_telemetry::error!(err; "Failed to handle request");
255
256 let mut headers = HeaderMap::<HeaderValue>::with_capacity(3);
257
258 let status_code = err.status_code();
261 headers.insert(
262 GREPTIME_DB_HEADER_ERROR_CODE,
263 HeaderValue::from(status_code as u32),
264 );
265 headers.insert(
266 GREPTIME_DB_HEADER_ERROR_RETRY_HINT,
267 HeaderValue::from_static(err.retry_hint().as_str()),
268 );
269 let root_error = err.output_msg();
270
271 let metadata = MetadataMap::from_headers(headers);
272 tonic::Status::with_metadata(
273 $crate::status_code::status_to_tonic_code(status_code),
274 root_error,
275 metadata,
276 )
277 }
278 }
279 };
280}
281
282pub fn status_to_tonic_code(status_code: StatusCode) -> Code {
284 match status_code {
285 StatusCode::Success => Code::Ok,
286 StatusCode::Unknown | StatusCode::External => Code::Unknown,
287 StatusCode::Unsupported => Code::Unimplemented,
288 StatusCode::Unexpected
289 | StatusCode::IllegalState
290 | StatusCode::Internal
291 | StatusCode::PlanQuery
292 | StatusCode::EngineExecuteQuery => Code::Internal,
293 StatusCode::InvalidArguments | StatusCode::InvalidSyntax | StatusCode::RequestOutdated => {
294 Code::InvalidArgument
295 }
296 StatusCode::Cancelled => Code::Cancelled,
297 StatusCode::DeadlineExceeded => Code::DeadlineExceeded,
298 StatusCode::TableAlreadyExists
299 | StatusCode::TableColumnExists
300 | StatusCode::RegionAlreadyExists
301 | StatusCode::DatabaseAlreadyExists
302 | StatusCode::TriggerAlreadyExists
303 | StatusCode::FlowAlreadyExists => Code::AlreadyExists,
304 StatusCode::TableNotFound
305 | StatusCode::RegionNotFound
306 | StatusCode::TableColumnNotFound
307 | StatusCode::DatabaseNotFound
308 | StatusCode::UserNotFound
309 | StatusCode::TriggerNotFound
310 | StatusCode::FlowNotFound => Code::NotFound,
311 StatusCode::TableUnavailable
312 | StatusCode::StorageUnavailable
313 | StatusCode::RegionNotReady => Code::Unavailable,
314 StatusCode::RuntimeResourcesExhausted
315 | StatusCode::RateLimited
316 | StatusCode::RegionBusy
317 | StatusCode::Suspended => Code::ResourceExhausted,
318 StatusCode::UnsupportedPasswordType
319 | StatusCode::UserPasswordMismatch
320 | StatusCode::AuthHeaderNotFound
321 | StatusCode::InvalidAuthHeader => Code::Unauthenticated,
322 StatusCode::AccessDenied | StatusCode::PermissionDenied | StatusCode::RegionReadonly => {
323 Code::PermissionDenied
324 }
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use strum::IntoEnumIterator;
331
332 use super::*;
333
334 fn assert_status_code_display(code: StatusCode, msg: &str) {
335 let code_msg = format!("{code}");
336 assert_eq!(msg, code_msg);
337 }
338
339 #[test]
340 fn test_display_status_code() {
341 assert_status_code_display(StatusCode::Unknown, "Unknown");
342 assert_status_code_display(StatusCode::TableAlreadyExists, "TableAlreadyExists");
343 }
344
345 #[test]
346 fn test_from_u32() {
347 for code in StatusCode::iter() {
348 let num = code as u32;
349 assert_eq!(StatusCode::from_u32(num), Some(code));
350 }
351
352 assert_eq!(StatusCode::from_u32(10000), None);
353 }
354
355 #[test]
356 fn test_is_success() {
357 assert!(StatusCode::is_success(0));
358 assert!(!StatusCode::is_success(1));
359 assert!(!StatusCode::is_success(2));
360 assert!(!StatusCode::is_success(3));
361 }
362}