This is an automated email from the ASF dual-hosted git repository.

tustvold pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/master by this push:
     new d83008bc03 Cleanup `object_store::retry` client error handling (#4915)
d83008bc03 is described below

commit d83008bc035d6bc724b79fcf363b96d1a5e11ce5
Author: Raphael Taylor-Davies <[email protected]>
AuthorDate: Wed Oct 11 07:57:59 2023 +0100

    Cleanup `object_store::retry` client error handling (#4915)
    
    * Cleanup client error handling
    
    * Clippy
    
    * Format
    
    * Update test
    
    * Review feedback
---
 object_store/src/client/retry.rs | 180 +++++++++++++++++++++------------------
 object_store/src/gcp/mod.rs      |   2 +-
 2 files changed, 96 insertions(+), 86 deletions(-)

diff --git a/object_store/src/client/retry.rs b/object_store/src/client/retry.rs
index 39a913142e..e4d246c87a 100644
--- a/object_store/src/client/retry.rs
+++ b/object_store/src/client/retry.rs
@@ -23,46 +23,50 @@ use futures::FutureExt;
 use reqwest::header::LOCATION;
 use reqwest::{Response, StatusCode};
 use snafu::Error as SnafuError;
+use snafu::Snafu;
 use std::time::{Duration, Instant};
 use tracing::info;
 
 /// Retry request error
-#[derive(Debug)]
-pub struct Error {
-    retries: usize,
-    message: String,
-    source: Option<reqwest::Error>,
-    status: Option<StatusCode>,
-}
-
-impl std::fmt::Display for Error {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(
-            f,
-            "response error \"{}\", after {} retries",
-            self.message, self.retries
-        )?;
-        if let Some(source) = &self.source {
-            write!(f, ": {source}")?;
-        }
-        Ok(())
-    }
-}
-
-impl std::error::Error for Error {
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
-        self.source.as_ref().map(|e| e as _)
-    }
+#[derive(Debug, Snafu)]
+pub enum Error {
+    #[snafu(display("Received redirect without LOCATION, this normally 
indicates an incorrectly configured region"))]
+    BareRedirect,
+
+    #[snafu(display("Client error with status {status}: {}", 
body.as_deref().unwrap_or("No Body")))]
+    Client {
+        status: StatusCode,
+        body: Option<String>,
+    },
+
+    #[snafu(display("Error after {retries} retries: {source}"))]
+    Reqwest {
+        retries: usize,
+        source: reqwest::Error,
+    },
 }
 
 impl Error {
     /// Returns the status code associated with this error if any
     pub fn status(&self) -> Option<StatusCode> {
-        self.status
+        match self {
+            Self::BareRedirect => None,
+            Self::Client { status, .. } => Some(*status),
+            Self::Reqwest { source, .. } => source.status(),
+        }
+    }
+
+    /// Returns the error body if any
+    pub fn body(&self) -> Option<&str> {
+        match self {
+            Self::Client { body, .. } => body.as_deref(),
+            Self::BareRedirect => None,
+            Self::Reqwest { .. } => None,
+        }
     }
 
     pub fn error(self, store: &'static str, path: String) -> crate::Error {
-        match self.status {
+        match self.status() {
             Some(StatusCode::NOT_FOUND) => crate::Error::NotFound {
                 path,
                 source: Box::new(self),
@@ -86,16 +90,19 @@ impl Error {
 impl From<Error> for std::io::Error {
     fn from(err: Error) -> Self {
         use std::io::ErrorKind;
-        match (&err.source, err.status()) {
-            (Some(source), _) if source.is_builder() || source.is_request() => 
{
-                Self::new(ErrorKind::InvalidInput, err)
-            }
-            (_, Some(StatusCode::NOT_FOUND)) => Self::new(ErrorKind::NotFound, 
err),
-            (_, Some(StatusCode::BAD_REQUEST)) => 
Self::new(ErrorKind::InvalidInput, err),
-            (Some(source), None) if source.is_timeout() => {
+        match &err {
+            Error::Client {
+                status: StatusCode::NOT_FOUND,
+                ..
+            } => Self::new(ErrorKind::NotFound, err),
+            Error::Client {
+                status: StatusCode::BAD_REQUEST,
+                ..
+            } => Self::new(ErrorKind::InvalidInput, err),
+            Error::Reqwest { source, .. } if source.is_timeout() => {
                 Self::new(ErrorKind::TimedOut, err)
             }
-            (Some(source), None) if source.is_connect() => {
+            Error::Reqwest { source, .. } if source.is_connect() => {
                 Self::new(ErrorKind::NotConnected, err)
             }
             _ => Self::new(ErrorKind::Other, err),
@@ -169,27 +176,21 @@ impl RetryExt for reqwest::RequestBuilder {
                     Ok(r) => match r.error_for_status_ref() {
                         Ok(_) if r.status().is_success() => return Ok(r),
                         Ok(r) if r.status() == StatusCode::NOT_MODIFIED => {
-                            return Err(Error{
-                                message: "not modified".to_string(),
-                                retries,
-                                status: Some(r.status()),
-                                source: None,
+                            return Err(Error::Client {
+                                body: None,
+                                status: StatusCode::NOT_MODIFIED,
                             })
                         }
                         Ok(r) => {
                             let is_bare_redirect = r.status().is_redirection() 
&& !r.headers().contains_key(LOCATION);
-                            let message = match is_bare_redirect {
-                                true => "Received redirect without LOCATION, 
this normally indicates an incorrectly configured region".to_string(),
+                            return match is_bare_redirect {
+                                true => Err(Error::BareRedirect),
                                 // Not actually sure if this is reachable, but 
here for completeness
-                                false => format!("request unsuccessful: {}", 
r.status()),
-                            };
-
-                            return Err(Error{
-                                message,
-                                retries,
-                                status: Some(r.status()),
-                                source: None,
-                            })
+                                false => Err(Error::Client {
+                                    body: None,
+                                    status: r.status(),
+                                })
+                            }
                         }
                         Err(e) => {
                             let status = r.status();
@@ -198,23 +199,26 @@ impl RetryExt for reqwest::RequestBuilder {
                                 || now.elapsed() > retry_timeout
                                 || !status.is_server_error() {
 
-                                // Get the response message if returned a 
client error
-                                let message = match status.is_client_error() {
+                                return Err(match status.is_client_error() {
                                     true => match r.text().await {
-                                        Ok(message) if !message.is_empty() => 
message,
-                                        Ok(_) => "No Body".to_string(),
-                                        Err(e) => format!("error getting 
response body: {e}")
+                                        Ok(body) => {
+                                            Error::Client {
+                                                body: Some(body).filter(|b| 
!b.is_empty()),
+                                                status,
+                                            }
+                                        }
+                                        Err(e) => {
+                                            Error::Reqwest {
+                                                retries,
+                                                source: e,
+                                            }
+                                        }
                                     }
-                                    false => status.to_string(),
-                                };
-
-                                return Err(Error{
-                                    message,
-                                    retries,
-                                    status: Some(status),
-                                    source: Some(e),
-                                })
-
+                                    false => Error::Reqwest {
+                                        retries,
+                                        source: e,
+                                    }
+                                });
                             }
 
                             let sleep = backoff.next();
@@ -238,16 +242,14 @@ impl RetryExt for reqwest::RequestBuilder {
                             || now.elapsed() > retry_timeout
                             || !do_retry {
 
-                            return Err(Error{
+                            return Err(Error::Reqwest {
                                 retries,
-                                message: "request error".to_string(),
-                                status: e.status(),
-                                source: Some(e),
+                                source: e,
                             })
                         }
                         let sleep = backoff.next();
                         retries += 1;
-                        info!("Encountered request error ({}) backing off for 
{} seconds, retry {} of {}", e, sleep.as_secs_f32(), retries, max_retries);
+                        info!("Encountered transport error ({}) backing off 
for {} seconds, retry {} of {}", e, sleep.as_secs_f32(), retries, max_retries);
                         tokio::time::sleep(sleep).await;
                     }
                 }
@@ -260,7 +262,7 @@ impl RetryExt for reqwest::RequestBuilder {
 #[cfg(test)]
 mod tests {
     use crate::client::mock_server::MockServer;
-    use crate::client::retry::RetryExt;
+    use crate::client::retry::{Error, RetryExt};
     use crate::RetryConfig;
     use hyper::header::LOCATION;
     use hyper::{Body, Response};
@@ -294,8 +296,11 @@ mod tests {
 
         let e = do_request().await.unwrap_err();
         assert_eq!(e.status().unwrap(), StatusCode::BAD_REQUEST);
-        assert_eq!(e.retries, 0);
-        assert_eq!(&e.message, "cupcakes");
+        assert_eq!(e.body(), Some("cupcakes"));
+        assert_eq!(
+            e.to_string(),
+            "Client error with status 400 Bad Request: cupcakes"
+        );
 
         // Handles client errors with no payload
         mock.push(
@@ -307,8 +312,11 @@ mod tests {
 
         let e = do_request().await.unwrap_err();
         assert_eq!(e.status().unwrap(), StatusCode::BAD_REQUEST);
-        assert_eq!(e.retries, 0);
-        assert_eq!(&e.message, "No Body");
+        assert_eq!(e.body(), None);
+        assert_eq!(
+            e.to_string(),
+            "Client error with status 400 Bad Request: No Body"
+        );
 
         // Should retry server error request
         mock.push(
@@ -381,7 +389,8 @@ mod tests {
         );
 
         let e = do_request().await.unwrap_err();
-        assert_eq!(e.message, "Received redirect without LOCATION, this 
normally indicates an incorrectly configured region");
+        assert!(matches!(e, Error::BareRedirect));
+        assert_eq!(e.to_string(), "Received redirect without LOCATION, this 
normally indicates an incorrectly configured region");
 
         // Gives up after the retrying the specified number of times
         for _ in 0..=retry.max_retries {
@@ -393,22 +402,23 @@ mod tests {
             );
         }
 
-        let e = do_request().await.unwrap_err();
-        assert_eq!(e.retries, retry.max_retries);
-        assert_eq!(e.message, "502 Bad Gateway");
+        let e = do_request().await.unwrap_err().to_string();
+        assert!(e.starts_with("Error after 2 retries: HTTP status server error 
(502 Bad Gateway) for url"), "{e}");
 
         // Panic results in an incomplete message error in the client
         mock.push_fn(|_| panic!());
         let r = do_request().await.unwrap();
         assert_eq!(r.status(), StatusCode::OK);
 
-        // Gives up after retrying mulitiple panics
+        // Gives up after retrying multiple panics
         for _ in 0..=retry.max_retries {
             mock.push_fn(|_| panic!());
         }
-        let e = do_request().await.unwrap_err();
-        assert_eq!(e.retries, retry.max_retries);
-        assert_eq!(e.message, "request error");
+        let e = do_request().await.unwrap_err().to_string();
+        assert!(
+            e.starts_with("Error after 2 retries: error sending request for 
url"),
+            "{e}"
+        );
 
         // Shutdown
         mock.shutdown().await
diff --git a/object_store/src/gcp/mod.rs b/object_store/src/gcp/mod.rs
index 3f5bf629d1..a0a60f27a6 100644
--- a/object_store/src/gcp/mod.rs
+++ b/object_store/src/gcp/mod.rs
@@ -1215,7 +1215,7 @@ mod test {
             .unwrap_err()
             .to_string();
         assert!(
-            err.contains("HTTP status client error (404 Not Found)"),
+            err.contains("Client error with status 404 Not Found"),
             "{}",
             err
         )

Reply via email to