Skip to main content

scopedb_client/
error.rs

1// Copyright 2024 ScopeDB, Inc.
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;
16use std::time::Duration;
17
18use reqwest::StatusCode;
19use uuid::Uuid;
20
21use crate::protocol::AppendErrorDetails;
22use crate::protocol::AppendState;
23use crate::protocol::StatementErrorDetails;
24
25/// ErrorKind is all kinds of Error of ScopeDB client.
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
27#[non_exhaustive]
28pub enum ErrorKind {
29    /// The client don't know what happened here, and no actions other than just
30    /// returning it back.
31    Unexpected,
32
33    /// The config for ScopeDB client is invalid.
34    ConfigInvalid,
35
36    /// A table append was rejected or its commit outcome is unknown.
37    AppendRowsFailed,
38
39    /// A statement reached a failed terminal state.
40    StatementFailed,
41
42    /// A statement reached the cancelled terminal state.
43    StatementCancelled,
44
45    /// A declarative catalog operation failed.
46    CatalogFailed,
47}
48
49/// The client-side reason an in-flight write was interrupted.
50#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
51#[serde(rename_all = "snake_case")]
52#[non_exhaustive]
53pub enum WriteErrorReason {
54    /// A client or transport deadline expired.
55    Timeout,
56}
57
58impl fmt::Display for WriteErrorReason {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        f.write_str(match self {
61            Self::Timeout => "timeout",
62        })
63    }
64}
65
66/// Details of an interrupted write. An unknown outcome may have committed and
67/// must not be automatically replayed.
68#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
69#[non_exhaustive]
70pub struct WriteErrorDetails {
71    pub reason: WriteErrorReason,
72    pub outcome: AppendState,
73}
74
75impl ErrorKind {
76    /// Convert self into static str.
77    pub fn into_static(self) -> &'static str {
78        self.into()
79    }
80}
81
82impl fmt::Display for ErrorKind {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(f, "{}", self.into_static())
85    }
86}
87
88impl From<ErrorKind> for &'static str {
89    fn from(v: ErrorKind) -> &'static str {
90        match v {
91            ErrorKind::Unexpected => "Unexpected",
92            ErrorKind::ConfigInvalid => "ConfigInvalid",
93            ErrorKind::AppendRowsFailed => "AppendRowsFailed",
94            ErrorKind::StatementFailed => "StatementFailed",
95            ErrorKind::StatementCancelled => "StatementCancelled",
96            ErrorKind::CatalogFailed => "CatalogFailed",
97        }
98    }
99}
100
101#[derive(Clone, Copy, Debug, PartialEq, Eq)]
102enum ErrorStatus {
103    /// Permanent means without external changes, the error never changes.
104    ///
105    /// For example, underlying services returns a not found error.
106    ///
107    /// Users SHOULD never retry this operation.
108    Permanent,
109    /// Temporary means this error is returned for temporary.
110    ///
111    /// For example, underlying services is rate limited or unavailable for temporary.
112    ///
113    /// Users CAN retry the operation to resolve it.
114    Temporary,
115    /// Persistent means the error is not classified as retryable.
116    ///
117    /// For example, a retry budget was exhausted or a write has an unknown outcome.
118    ///
119    /// Append streams may explicitly replay transient unknown outcomes under
120    /// their delivery policy. Client timeouts are never automatically replayed.
121    Persistent,
122}
123
124#[derive(Debug, Default, Clone)]
125struct HttpErrorMetadata {
126    status: Option<StatusCode>,
127    request_id: Option<String>,
128    retry_after: Option<Duration>,
129    retryable: Option<bool>,
130}
131
132#[derive(Debug, Clone)]
133struct ApiErrorDetails {
134    code: Option<String>,
135    details: Option<serde_json::Value>,
136}
137
138#[derive(Debug, Default, Clone)]
139struct StatementErrorMetadata {
140    id: Option<Uuid>,
141    details: Option<StatementErrorDetails>,
142}
143
144impl fmt::Display for ErrorStatus {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        match self {
147            ErrorStatus::Permanent => write!(f, "permanent"),
148            ErrorStatus::Temporary => write!(f, "temporary"),
149            ErrorStatus::Persistent => write!(f, "persistent"),
150        }
151    }
152}
153
154/// A failure with a message, optional HTTP diagnostics, and operation details.
155///
156/// Start with [`Self::message`], [`Self::http_status`], and [`Self::request_id`].
157/// Use [`Self::statement_details`] for a failed query, [`Self::append_details`]
158/// for a rejected or uncertain append, and [`Self::write_details`] for a write
159/// timeout. A timed-out write may already have committed; reconcile it before
160/// replaying.
161///
162/// [`Self::is_retryable`] describes the failed operation. For a query that was
163/// already submitted, resume its [`Self::statement_id`] through
164/// [`crate::Client::statement_handle`] instead of submitting another query.
165#[derive(Clone)]
166pub struct Error {
167    kind: ErrorKind,
168    message: String,
169
170    status: ErrorStatus,
171    context: Vec<(&'static str, String)>,
172    api_error: Option<Box<ApiErrorDetails>>,
173    append_details: Option<AppendErrorDetails>,
174    write_details: Option<WriteErrorDetails>,
175    statement_metadata: Option<Box<StatementErrorMetadata>>,
176    http_metadata: Option<Box<HttpErrorMetadata>>,
177
178    source: Option<std::sync::Arc<anyhow::Error>>,
179}
180
181impl fmt::Display for Error {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        write!(f, "{} ({})", self.kind, self.status)?;
184
185        if !self.context.is_empty() {
186            write!(f, ", context: {{ ")?;
187            write!(
188                f,
189                "{}",
190                self.context
191                    .iter()
192                    .map(|(k, v)| format!("{k}: {v}"))
193                    .collect::<Vec<_>>()
194                    .join(", ")
195            )?;
196            write!(f, " }}")?;
197        }
198
199        if !self.message.is_empty() {
200            write!(f, " => {}", self.message)?;
201        }
202
203        if let Some(code) = self.code() {
204            write!(f, ", code: {code}")?;
205        }
206        if let Some(id) = self.statement_id() {
207            write!(f, ", statement_id: {id}")?;
208        }
209        if let Some(status) = self.http_status() {
210            write!(f, ", http_status: {}", status.as_u16())?;
211        }
212        if let Some(request_id) = self.request_id() {
213            write!(f, ", request_id: {request_id}")?;
214        }
215        if let Some(retry_after) = self.retry_after() {
216            write!(f, ", retry_after: {retry_after:?}")?;
217        }
218
219        if let Some(details) = self.write_details() {
220            write!(
221                f,
222                ", write_reason: {}, write_outcome: {}",
223                details.reason, details.outcome
224            )?;
225        }
226
227        if let Some(source) = &self.source {
228            write!(f, ", source: {source}")?;
229        }
230
231        Ok(())
232    }
233}
234
235impl fmt::Debug for Error {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        // If alternate has been specified, we will print like Debug.
238        if f.alternate() {
239            let mut de = f.debug_struct("Error");
240            de.field("kind", &self.kind);
241            de.field("message", &self.message);
242            de.field("api_error", &self.api_error);
243            de.field("status", &self.status);
244            de.field("context", &self.context);
245            de.field("append_details", &self.append_details);
246            de.field("write_details", &self.write_details);
247            de.field("statement_id", &self.statement_id());
248            de.field("statement_details", &self.statement_details());
249            de.field("http_metadata", &self.http_metadata);
250            de.field("source", &self.source);
251            return de.finish();
252        }
253
254        write!(f, "{} ({})", self.kind, self.status)?;
255        if !self.message.is_empty() {
256            write!(f, " => {}", self.message)?;
257        }
258        if let Some(code) = self.code() {
259            write!(f, ", code: {code}")?;
260        }
261        if let Some(id) = self.statement_id() {
262            write!(f, ", statement_id: {id}")?;
263        }
264        if let Some(status) = self.http_status() {
265            write!(f, ", http_status: {}", status.as_u16())?;
266        }
267        if let Some(request_id) = self.request_id() {
268            write!(f, ", request_id: {request_id}")?;
269        }
270        if let Some(retry_after) = self.retry_after() {
271            write!(f, ", retry_after: {retry_after:?}")?;
272        }
273        if let Some(details) = self.write_details() {
274            write!(
275                f,
276                ", write_reason: {}, write_outcome: {}",
277                details.reason, details.outcome
278            )?;
279        }
280        writeln!(f)?;
281
282        if !self.context.is_empty() {
283            writeln!(f)?;
284            writeln!(f, "Context:")?;
285            for (k, v) in self.context.iter() {
286                writeln!(f, "   {k}: {v}")?;
287            }
288        }
289        if let Some(source) = &self.source {
290            writeln!(f)?;
291            writeln!(f, "Source:")?;
292            writeln!(f, "   {source:#}")?;
293        }
294
295        Ok(())
296    }
297}
298
299impl std::error::Error for Error {
300    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
301        self.source.as_ref().map(|v| v.as_ref().as_ref())
302    }
303}
304
305impl Error {
306    /// Create a new Error with error kind and message.
307    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
308        Self {
309            kind,
310            message: message.into(),
311
312            status: ErrorStatus::Permanent,
313            context: Vec::default(),
314            api_error: None,
315            append_details: None,
316            write_details: None,
317            statement_metadata: None,
318            http_metadata: None,
319            source: None,
320        }
321    }
322
323    /// Add more context in error.
324    pub fn with_context(mut self, key: &'static str, value: impl ToString) -> Self {
325        self.context.push((key, value.to_string()));
326        self
327    }
328
329    /// Set source for error.
330    ///
331    /// # Notes
332    ///
333    /// If the source has been set, we will raise a panic here.
334    pub fn set_source(mut self, src: impl Into<anyhow::Error>) -> Self {
335        debug_assert!(self.source.is_none(), "the source error has been set");
336
337        self.source = Some(std::sync::Arc::new(src.into()));
338        self
339    }
340
341    pub(crate) fn set_write_timeout(mut self) -> Self {
342        self.write_details = Some(WriteErrorDetails {
343            reason: WriteErrorReason::Timeout,
344            outcome: AppendState::Unknown,
345        });
346        self
347    }
348
349    /// Returns client timeout details for a table append after sending began.
350    /// Existing error kinds, sources, and HTTP diagnostics remain available.
351    pub fn write_details(&self) -> Option<&WriteErrorDetails> {
352        self.write_details.as_ref()
353    }
354
355    /// Return structured table-append failure details, when available.
356    pub fn append_details(&self) -> Option<&AppendErrorDetails> {
357        self.append_details.as_ref()
358    }
359
360    pub(crate) fn set_append_details(mut self, details: AppendErrorDetails) -> Self {
361        self.append_details = Some(details);
362        self
363    }
364
365    /// Return structured statement failure details, when available.
366    pub fn statement_details(&self) -> Option<&StatementErrorDetails> {
367        self.statement_metadata
368            .as_ref()
369            .and_then(|m| m.details.as_ref())
370    }
371
372    pub(crate) fn set_statement_details(mut self, details: StatementErrorDetails) -> Self {
373        self.statement_metadata
374            .get_or_insert_with(Default::default)
375            .details = Some(details);
376        self
377    }
378
379    /// Returns the statement identifier, when known. Use [`crate::Client::statement_handle`]
380    /// to inspect the existing execution instead of resubmitting it. An identifier
381    /// supplied before submission does not confirm that the server accepted it.
382    pub fn statement_id(&self) -> Option<Uuid> {
383        self.statement_metadata.as_ref().and_then(|m| m.id)
384    }
385
386    pub(crate) fn set_statement_id(mut self, id: Uuid) -> Self {
387        self.statement_metadata
388            .get_or_insert_with(Default::default)
389            .id = Some(id);
390        self
391    }
392
393    pub(crate) fn set_http_status(mut self, status: StatusCode) -> Self {
394        self.http_metadata_mut().status = Some(status);
395        self
396    }
397
398    pub(crate) fn set_request_id(mut self, request_id: String) -> Self {
399        self.http_metadata_mut().request_id = Some(request_id);
400        self
401    }
402
403    pub(crate) fn set_retry_after(mut self, retry_after: Duration) -> Self {
404        self.http_metadata_mut().retry_after = Some(retry_after);
405        self
406    }
407
408    pub(crate) fn set_server_retryable(mut self, retryable: Option<bool>) -> Self {
409        self.http_metadata_mut().retryable = retryable;
410        self
411    }
412
413    pub(crate) fn server_retryable(&self) -> Option<bool> {
414        self.http_metadata
415            .as_ref()
416            .and_then(|metadata| metadata.retryable)
417    }
418
419    fn http_metadata_mut(&mut self) -> &mut HttpErrorMetadata {
420        self.http_metadata
421            .get_or_insert_with(|| Box::new(HttpErrorMetadata::default()))
422    }
423
424    /// Set permanent status for error.
425    pub fn set_permanent(mut self) -> Self {
426        self.status = ErrorStatus::Permanent;
427        self
428    }
429
430    /// Set temporary status for error.
431    ///
432    /// By set temporary, we indicate this error is retryable.
433    pub fn set_temporary(mut self) -> Self {
434        self.status = ErrorStatus::Temporary;
435        self
436    }
437
438    /// Set persistent status for error.
439    ///
440    /// By setting persistent, we indicate the retry should be stopped.
441    pub fn set_persistent(mut self) -> Self {
442        self.status = ErrorStatus::Persistent;
443        self
444    }
445
446    /// Return the kind of this error.
447    pub fn kind(&self) -> ErrorKind {
448        self.kind
449    }
450
451    /// Returns the server error code, including codes introduced by newer servers.
452    pub fn code(&self) -> Option<&str> {
453        self.api_error
454            .as_deref()
455            .and_then(|e| e.code.as_deref())
456            .or_else(|| self.statement_details().map(|e| e.code.as_str()))
457    }
458
459    /// Returns the original structured server details, when provided.
460    pub fn details(&self) -> Option<&serde_json::Value> {
461        self.api_error
462            .as_deref()
463            .and_then(|e| e.details.as_ref())
464            .or_else(|| self.statement_details().and_then(|e| e.details.as_ref()))
465    }
466
467    pub(crate) fn set_api_error(
468        mut self,
469        code: Option<String>,
470        details: Option<serde_json::Value>,
471    ) -> Self {
472        if code.is_some() || details.is_some() {
473            self.api_error = Some(Box::new(ApiErrorDetails { code, details }));
474        }
475        self
476    }
477
478    /// Return the error message.
479    pub fn message(&self) -> &str {
480        &self.message
481    }
482
483    /// Return the HTTP response status when the error came from a server.
484    pub fn http_status(&self) -> Option<StatusCode> {
485        self.http_metadata
486            .as_ref()
487            .and_then(|metadata| metadata.status)
488    }
489
490    /// Return the request identifier supplied by ScopeDB or an intermediary.
491    pub fn request_id(&self) -> Option<&str> {
492        self.http_metadata
493            .as_ref()
494            .and_then(|metadata| metadata.request_id.as_deref())
495    }
496
497    /// Return the delay suggested by the HTTP `Retry-After` header.
498    pub fn retry_after(&self) -> Option<Duration> {
499        self.http_metadata
500            .as_ref()
501            .and_then(|metadata| metadata.retry_after)
502    }
503
504    /// Return whether retrying this operation is currently appropriate.
505    /// For statement handles, retry on the same handle. Errors from `query()` or
506    /// `execute()` after submission never indicate that resubmitting is safe.
507    pub fn is_retryable(&self) -> bool {
508        self.is_temporary()
509    }
510
511    /// Check if this error is temporary.
512    pub fn is_temporary(&self) -> bool {
513        self.status == ErrorStatus::Temporary
514    }
515
516    /// Check if this error is permanent.
517    pub fn is_permanent(&self) -> bool {
518        self.status == ErrorStatus::Permanent
519    }
520
521    /// Check if this error is persistent.
522    pub fn is_persistent(&self) -> bool {
523        self.status == ErrorStatus::Persistent
524    }
525}