common_error/
status_code.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fmt;
16
17use strum::{AsRefStr, EnumIter, EnumString, FromRepr};
18use tonic::Code;
19
20/// Common status code for public API.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, AsRefStr, EnumIter, FromRepr)]
22pub enum StatusCode {
23    // ====== Begin of common status code ==============
24    /// Success.
25    Success = 0,
26
27    /// Unknown error.
28    Unknown = 1000,
29    /// Unsupported operation.
30    Unsupported = 1001,
31    /// Unexpected error, maybe there is a BUG.
32    Unexpected = 1002,
33    /// Internal server error.
34    Internal = 1003,
35    /// Invalid arguments.
36    InvalidArguments = 1004,
37    /// The task is cancelled (typically caller-side).
38    Cancelled = 1005,
39    /// Illegal state, can be exposed to users.
40    IllegalState = 1006,
41    /// Caused by some error originated from external system.
42    External = 1007,
43    /// The request is deadline exceeded (typically server-side).
44    DeadlineExceeded = 1008,
45    /// Service got suspended for various reason. For example, resources exceed limit.
46    Suspended = 1009,
47    // ====== End of common status code ================
48
49    // ====== Begin of SQL related status code =========
50    /// SQL Syntax error.
51    InvalidSyntax = 2000,
52    // ====== End of SQL related status code ===========
53
54    // ====== Begin of query related status code =======
55    /// Fail to create a plan for the query.
56    PlanQuery = 3000,
57    /// The query engine fail to execute query.
58    EngineExecuteQuery = 3001,
59    // ====== End of query related status code =========
60
61    // ====== Begin of catalog related status code =====
62    /// Table already exists.
63    TableAlreadyExists = 4000,
64    /// Table not found.
65    TableNotFound = 4001,
66    /// Table column not found.
67    TableColumnNotFound = 4002,
68    /// Table column already exists.
69    TableColumnExists = 4003,
70    /// Database not found.
71    DatabaseNotFound = 4004,
72    /// Region not found.
73    RegionNotFound = 4005,
74    /// Region already exists.
75    RegionAlreadyExists = 4006,
76    /// Region is read-only in current state.
77    RegionReadonly = 4007,
78    /// Region is not in a proper state to handle specific request.
79    RegionNotReady = 4008,
80    /// Region is temporarily in busy state.
81    RegionBusy = 4009,
82    /// Table is temporarily unable to handle the request.
83    TableUnavailable = 4010,
84    /// Database already exists.
85    DatabaseAlreadyExists = 4011,
86    // ====== End of catalog related status code =======
87
88    // ====== Begin of storage related status code =====
89    /// Storage is temporarily unable to handle the request.
90    StorageUnavailable = 5000,
91    /// Request is outdated, e.g., version mismatch.
92    RequestOutdated = 5001,
93    // ====== End of storage related status code =======
94
95    // ====== Begin of server related status code =====
96    /// Runtime resources exhausted, like creating threads failed.
97    RuntimeResourcesExhausted = 6000,
98
99    /// Rate limit exceeded.
100    RateLimited = 6001,
101    // ====== End of server related status code =======
102
103    // ====== Begin of auth related status code =====
104    /// User not exist.
105    UserNotFound = 7000,
106    /// Unsupported password type.
107    UnsupportedPasswordType = 7001,
108    /// Username and password does not match.
109    UserPasswordMismatch = 7002,
110    /// Not found http authorization header.
111    AuthHeaderNotFound = 7003,
112    /// Invalid http authorization header.
113    InvalidAuthHeader = 7004,
114    /// Illegal request to connect catalog-schema.
115    AccessDenied = 7005,
116    /// User is not authorized to perform the operation.
117    PermissionDenied = 7006,
118    // ====== End of auth related status code =====
119
120    // ====== Begin of flow related status code =====
121    FlowAlreadyExists = 8000,
122    FlowNotFound = 8001,
123    // ====== End of flow related status code =====
124
125    // ====== Begin of trigger related status code =====
126    TriggerAlreadyExists = 9000,
127    TriggerNotFound = 9001,
128    // ====== End of trigger related status code =====
129}
130
131impl StatusCode {
132    /// Returns `true` if `code` is success.
133    pub fn is_success(code: u32) -> bool {
134        Self::Success as u32 == code
135    }
136
137    /// Returns `true` if the error with this code is retryable.
138    pub fn is_retryable(&self) -> bool {
139        match self {
140            StatusCode::StorageUnavailable
141            | StatusCode::RuntimeResourcesExhausted
142            | StatusCode::Internal
143            | StatusCode::RegionNotReady
144            | StatusCode::TableUnavailable
145            | StatusCode::RegionBusy => true,
146
147            StatusCode::Success
148            | StatusCode::Unknown
149            | StatusCode::Unsupported
150            | StatusCode::IllegalState
151            | StatusCode::Unexpected
152            | StatusCode::InvalidArguments
153            | StatusCode::Cancelled
154            | StatusCode::DeadlineExceeded
155            | StatusCode::InvalidSyntax
156            | StatusCode::DatabaseAlreadyExists
157            | StatusCode::PlanQuery
158            | StatusCode::EngineExecuteQuery
159            | StatusCode::TableAlreadyExists
160            | StatusCode::TableNotFound
161            | StatusCode::RegionAlreadyExists
162            | StatusCode::RegionNotFound
163            | StatusCode::FlowAlreadyExists
164            | StatusCode::FlowNotFound
165            | StatusCode::TriggerAlreadyExists
166            | StatusCode::TriggerNotFound
167            | StatusCode::RegionReadonly
168            | StatusCode::TableColumnNotFound
169            | StatusCode::TableColumnExists
170            | StatusCode::DatabaseNotFound
171            | StatusCode::RateLimited
172            | StatusCode::UserNotFound
173            | StatusCode::UnsupportedPasswordType
174            | StatusCode::UserPasswordMismatch
175            | StatusCode::AuthHeaderNotFound
176            | StatusCode::InvalidAuthHeader
177            | StatusCode::AccessDenied
178            | StatusCode::PermissionDenied
179            | StatusCode::RequestOutdated
180            | StatusCode::External
181            | StatusCode::Suspended => false,
182        }
183    }
184
185    /// Returns `true` if we should print an error log for an error with
186    /// this status code.
187    pub fn should_log_error(&self) -> bool {
188        match self {
189            StatusCode::Unknown
190            | StatusCode::Unexpected
191            | StatusCode::Internal
192            | StatusCode::Cancelled
193            | StatusCode::DeadlineExceeded
194            | StatusCode::IllegalState
195            | StatusCode::EngineExecuteQuery
196            | StatusCode::StorageUnavailable
197            | StatusCode::RuntimeResourcesExhausted
198            | StatusCode::External => true,
199
200            StatusCode::Success
201            | StatusCode::Unsupported
202            | StatusCode::InvalidArguments
203            | StatusCode::InvalidSyntax
204            | StatusCode::TableAlreadyExists
205            | StatusCode::TableNotFound
206            | StatusCode::RegionAlreadyExists
207            | StatusCode::RegionNotFound
208            | StatusCode::PlanQuery
209            | StatusCode::FlowAlreadyExists
210            | StatusCode::FlowNotFound
211            | StatusCode::TriggerAlreadyExists
212            | StatusCode::TriggerNotFound
213            | StatusCode::RegionNotReady
214            | StatusCode::RegionBusy
215            | StatusCode::RegionReadonly
216            | StatusCode::TableColumnNotFound
217            | StatusCode::TableColumnExists
218            | StatusCode::DatabaseNotFound
219            | StatusCode::RateLimited
220            | StatusCode::UserNotFound
221            | StatusCode::TableUnavailable
222            | StatusCode::DatabaseAlreadyExists
223            | StatusCode::UnsupportedPasswordType
224            | StatusCode::UserPasswordMismatch
225            | StatusCode::AuthHeaderNotFound
226            | StatusCode::InvalidAuthHeader
227            | StatusCode::AccessDenied
228            | StatusCode::PermissionDenied
229            | StatusCode::RequestOutdated
230            | StatusCode::Suspended => false,
231        }
232    }
233
234    pub fn from_u32(value: u32) -> Option<Self> {
235        StatusCode::from_repr(value as usize)
236    }
237}
238
239impl fmt::Display for StatusCode {
240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241        // The current debug format is suitable to display.
242        write!(f, "{self:?}")
243    }
244}
245
246#[macro_export]
247macro_rules! define_from_tonic_status {
248    ($Error: ty, $Variant: ident) => {
249        impl From<tonic::Status> for $Error {
250            fn from(e: tonic::Status) -> Self {
251                use snafu::location;
252
253                fn metadata_value(e: &tonic::Status, key: &str) -> Option<String> {
254                    e.metadata()
255                        .get(key)
256                        .and_then(|v| String::from_utf8(v.as_bytes().to_vec()).ok())
257                }
258                let code = metadata_value(&e, $crate::GREPTIME_DB_HEADER_ERROR_CODE)
259                    .and_then(|s| {
260                        if let Ok(code) = s.parse::<u32>() {
261                            StatusCode::from_u32(code)
262                        } else {
263                            None
264                        }
265                    })
266                    .unwrap_or_else(|| match e.code() {
267                        tonic::Code::Cancelled => StatusCode::Cancelled,
268                        tonic::Code::DeadlineExceeded => StatusCode::DeadlineExceeded,
269                        _ => StatusCode::Internal,
270                    });
271
272                let msg = metadata_value(&e, $crate::GREPTIME_DB_HEADER_ERROR_MSG)
273                    .unwrap_or_else(|| e.message().to_string());
274
275                // TODO(LFC): Make the error variant defined automatically.
276                Self::$Variant {
277                    code,
278                    msg,
279                    tonic_code: e.code(),
280                    location: location!(),
281                }
282            }
283        }
284    };
285}
286
287#[macro_export]
288macro_rules! define_into_tonic_status {
289    ($Error: ty) => {
290        impl From<$Error> for tonic::Status {
291            fn from(err: $Error) -> Self {
292                use tonic::codegen::http::{HeaderMap, HeaderValue};
293                use tonic::metadata::MetadataMap;
294                use $crate::GREPTIME_DB_HEADER_ERROR_CODE;
295
296                common_telemetry::error!(err; "Failed to handle request");
297
298                let mut headers = HeaderMap::<HeaderValue>::with_capacity(2);
299
300                // If either of the status_code or error msg cannot convert to valid HTTP header value
301                // (which is a very rare case), just ignore. Client will use Tonic status code and message.
302                let status_code = err.status_code();
303                headers.insert(
304                    GREPTIME_DB_HEADER_ERROR_CODE,
305                    HeaderValue::from(status_code as u32),
306                );
307                let root_error = err.output_msg();
308
309                let metadata = MetadataMap::from_headers(headers);
310                tonic::Status::with_metadata(
311                    $crate::status_code::status_to_tonic_code(status_code),
312                    root_error,
313                    metadata,
314                )
315            }
316        }
317    };
318}
319
320/// Returns the tonic [Code] of a [StatusCode].
321pub fn status_to_tonic_code(status_code: StatusCode) -> Code {
322    match status_code {
323        StatusCode::Success => Code::Ok,
324        StatusCode::Unknown | StatusCode::External => Code::Unknown,
325        StatusCode::Unsupported => Code::Unimplemented,
326        StatusCode::Unexpected
327        | StatusCode::IllegalState
328        | StatusCode::Internal
329        | StatusCode::PlanQuery
330        | StatusCode::EngineExecuteQuery => Code::Internal,
331        StatusCode::InvalidArguments | StatusCode::InvalidSyntax | StatusCode::RequestOutdated => {
332            Code::InvalidArgument
333        }
334        StatusCode::Cancelled => Code::Cancelled,
335        StatusCode::DeadlineExceeded => Code::DeadlineExceeded,
336        StatusCode::TableAlreadyExists
337        | StatusCode::TableColumnExists
338        | StatusCode::RegionAlreadyExists
339        | StatusCode::DatabaseAlreadyExists
340        | StatusCode::TriggerAlreadyExists
341        | StatusCode::FlowAlreadyExists => Code::AlreadyExists,
342        StatusCode::TableNotFound
343        | StatusCode::RegionNotFound
344        | StatusCode::TableColumnNotFound
345        | StatusCode::DatabaseNotFound
346        | StatusCode::UserNotFound
347        | StatusCode::TriggerNotFound
348        | StatusCode::FlowNotFound => Code::NotFound,
349        StatusCode::TableUnavailable
350        | StatusCode::StorageUnavailable
351        | StatusCode::RegionNotReady => Code::Unavailable,
352        StatusCode::RuntimeResourcesExhausted
353        | StatusCode::RateLimited
354        | StatusCode::RegionBusy
355        | StatusCode::Suspended => Code::ResourceExhausted,
356        StatusCode::UnsupportedPasswordType
357        | StatusCode::UserPasswordMismatch
358        | StatusCode::AuthHeaderNotFound
359        | StatusCode::InvalidAuthHeader => Code::Unauthenticated,
360        StatusCode::AccessDenied | StatusCode::PermissionDenied | StatusCode::RegionReadonly => {
361            Code::PermissionDenied
362        }
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use strum::IntoEnumIterator;
369
370    use super::*;
371
372    fn assert_status_code_display(code: StatusCode, msg: &str) {
373        let code_msg = format!("{code}");
374        assert_eq!(msg, code_msg);
375    }
376
377    #[test]
378    fn test_display_status_code() {
379        assert_status_code_display(StatusCode::Unknown, "Unknown");
380        assert_status_code_display(StatusCode::TableAlreadyExists, "TableAlreadyExists");
381    }
382
383    #[test]
384    fn test_from_u32() {
385        for code in StatusCode::iter() {
386            let num = code as u32;
387            assert_eq!(StatusCode::from_u32(num), Some(code));
388        }
389
390        assert_eq!(StatusCode::from_u32(10000), None);
391    }
392
393    #[test]
394    fn test_is_success() {
395        assert!(StatusCode::is_success(0));
396        assert!(!StatusCode::is_success(1));
397        assert!(!StatusCode::is_success(2));
398        assert!(!StatusCode::is_success(3));
399    }
400}