Skip to main content

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