Skip to main content

autopush_common/db/bigtable/bigtable_client/
error.rs

1use std::fmt::{self, Display};
2
3use actix_web::http::StatusCode;
4use deadpool::managed::{PoolError, TimeoutType};
5use thiserror::Error;
6
7use crate::errors::ReportableError;
8
9#[derive(PartialEq, Eq, Debug)]
10pub enum MutateRowStatus {
11    OK,
12    Cancelled,
13    Unknown,
14    InvalidArgument,
15    DeadlineExceeded,
16    NotFound,
17    AlreadyExists,
18    PermissionDenied,
19    ResourceExhausted,
20    FailedPrecondition,
21    Aborted,
22    OutOfRange,
23    Unimplemented,
24    Internal,
25    Unavailable,
26    DataLoss,
27    Unauthenticated,
28}
29
30impl MutateRowStatus {
31    pub fn is_ok(&self) -> bool {
32        self == &Self::OK
33    }
34}
35
36impl From<i32> for MutateRowStatus {
37    fn from(v: i32) -> Self {
38        match v {
39            0 => Self::OK,
40            1 => Self::Cancelled,
41            2 => Self::Unknown,
42            3 => Self::InvalidArgument,
43            4 => Self::DeadlineExceeded,
44            5 => Self::NotFound,
45            6 => Self::AlreadyExists,
46            7 => Self::PermissionDenied,
47            8 => Self::ResourceExhausted,
48            9 => Self::FailedPrecondition,
49            10 => Self::Aborted,
50            11 => Self::OutOfRange,
51            12 => Self::Unimplemented,
52            13 => Self::Internal,
53            14 => Self::Unavailable,
54            15 => Self::DataLoss,
55            16 => Self::Unauthenticated,
56            _ => Self::Unknown,
57        }
58    }
59}
60
61impl Display for MutateRowStatus {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        f.write_str(match self {
64            MutateRowStatus::OK => "Ok",
65            MutateRowStatus::Cancelled => "Cancelled",
66            MutateRowStatus::Unknown => "Unknown",
67            MutateRowStatus::InvalidArgument => "Invalid Argument",
68            MutateRowStatus::DeadlineExceeded => "Deadline Exceeded",
69            MutateRowStatus::NotFound => "Not Found",
70            MutateRowStatus::AlreadyExists => "Already Exists",
71            MutateRowStatus::PermissionDenied => "Permission Denied",
72            MutateRowStatus::ResourceExhausted => "Resource Exhausted",
73            MutateRowStatus::FailedPrecondition => "Failed Precondition",
74            MutateRowStatus::Aborted => "Aborted",
75            MutateRowStatus::OutOfRange => "Out of Range",
76            MutateRowStatus::Unimplemented => "Unimplemented",
77            MutateRowStatus::Internal => "Internal",
78            MutateRowStatus::Unavailable => "Unavailable",
79            MutateRowStatus::DataLoss => "Data Loss",
80            MutateRowStatus::Unauthenticated => "Unauthenticated",
81        })
82    }
83}
84
85impl MutateRowStatus {
86    pub fn status(&self) -> StatusCode {
87        match self {
88            MutateRowStatus::OK => StatusCode::OK,
89            // Some of these were taken from the java-bigtable-hbase retry handlers
90            MutateRowStatus::Aborted
91            | MutateRowStatus::DeadlineExceeded
92            | MutateRowStatus::Internal
93            | MutateRowStatus::ResourceExhausted
94            | MutateRowStatus::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
95            _ => StatusCode::INTERNAL_SERVER_ERROR,
96        }
97    }
98}
99
100#[derive(Debug, Error)]
101pub enum BigTableError {
102    #[error("Invalid Row Response: {0}")]
103    InvalidRowResponse(#[source] tonic::Status),
104
105    #[error("Invalid Chunk")]
106    InvalidChunk(String),
107
108    #[error("BigTable read error: {0}")]
109    Read(#[source] tonic::Status),
110
111    #[error("BigTable write timestamp error: {0}")]
112    WriteTime(#[source] std::time::SystemTimeError),
113
114    #[error("Bigtable write error: {0}")]
115    Write(#[source] tonic::Status),
116
117    #[error("BigTable connection error: {0}")]
118    Connect(#[source] tonic::transport::Error),
119
120    #[error("BigTable authentication error: {0}")]
121    Auth(#[source] gcp_auth::Error),
122
123    #[error("Bigtable RPC attempt exceeded its deadline")]
124    AttemptTimeout,
125
126    #[error("Bigtable request preparation exceeded its deadline")]
127    PreSendTimeout,
128
129    #[error("Bigtable operation exhausted its total retry budget")]
130    OperationTimeout,
131
132    /// Return a GRPC status code and any message.
133    /// See https://grpc.github.io/grpc/core/md_doc_statuscodes.html
134    #[error("Bigtable status response: {0:?}")]
135    Status(MutateRowStatus, String),
136
137    /// General Pool errors
138    #[error("Pool Error: {0}")]
139    Pool(Box<PoolError<BigTableError>>),
140
141    /// Timeout occurred while getting a pooled logical client handle.
142    #[error("Pool Timeout: {0:?}")]
143    PoolTimeout(TimeoutType),
144
145    #[error("BigTable config error: {0}")]
146    Config(String),
147
148    #[error("Circuit breaker open: BigTable temporarily unavailable")]
149    CircuitBreakerOpen,
150}
151
152impl BigTableError {
153    pub fn status(&self) -> StatusCode {
154        match self {
155            BigTableError::PoolTimeout(_)
156            | BigTableError::CircuitBreakerOpen
157            | BigTableError::AttemptTimeout
158            | BigTableError::PreSendTimeout
159            | BigTableError::OperationTimeout => StatusCode::SERVICE_UNAVAILABLE,
160            BigTableError::Status(e, _) => e.status(),
161            _ => StatusCode::INTERNAL_SERVER_ERROR,
162        }
163    }
164}
165
166impl ReportableError for BigTableError {
167    fn is_sentry_event(&self) -> bool {
168        #[allow(clippy::match_like_matches_macro)]
169        match self {
170            BigTableError::PoolTimeout(_)
171            | BigTableError::CircuitBreakerOpen
172            | BigTableError::AttemptTimeout
173            | BigTableError::PreSendTimeout
174            | BigTableError::OperationTimeout => false,
175            _ => true,
176        }
177    }
178
179    fn metric_label(&self) -> Option<&'static str> {
180        let err = match self {
181            BigTableError::InvalidRowResponse(_) => "storage.bigtable.error.invalid_row_response",
182            BigTableError::InvalidChunk(_) => "storage.bigtable.error.invalid_chunk",
183            BigTableError::Read(_) => "storage.bigtable.error.read",
184            BigTableError::Write(_) => "storage.bigtable.error.write",
185            BigTableError::Connect(_) => "storage.bigtable.error.connect",
186            BigTableError::Auth(_) => "storage.bigtable.error.auth",
187            BigTableError::AttemptTimeout => "storage.bigtable.error.attempt_timeout",
188            BigTableError::PreSendTimeout => "storage.bigtable.error.pre_send_timeout",
189            BigTableError::OperationTimeout => "storage.bigtable.error.operation_timeout",
190            BigTableError::Status(_, _) => "storage.bigtable.error.status",
191            BigTableError::WriteTime(_) => "storage.bigtable.error.writetime",
192            BigTableError::Pool(_) => "storage.bigtable.error.pool",
193            BigTableError::PoolTimeout(_) => "storage.bigtable.error.pool_timeout",
194            BigTableError::Config(_) => "storage.bigtable.error.config",
195            BigTableError::CircuitBreakerOpen => "storage.bigtable.error.circuit_breaker",
196        };
197        Some(err)
198    }
199
200    fn tags(&self) -> Vec<(&str, String)> {
201        #[allow(clippy::match_like_matches_macro)]
202        match self {
203            BigTableError::PoolTimeout(tt) => vec![("type", format!("{tt:?}").to_lowercase())],
204            _ => vec![],
205        }
206    }
207
208    fn extras(&self) -> Vec<(&str, String)> {
209        match self {
210            BigTableError::InvalidRowResponse(s) => vec![("error", s.to_string())],
211            BigTableError::InvalidChunk(s) => vec![("error", s.to_string())],
212            BigTableError::Read(s) => vec![("error", s.to_string())],
213            BigTableError::Write(s) => vec![("error", s.to_string())],
214            BigTableError::Connect(e) => vec![("error", e.to_string())],
215            BigTableError::Auth(e) => vec![("error", e.to_string())],
216            BigTableError::Status(code, s) => {
217                vec![("code", code.to_string()), ("error", s.to_string())]
218            }
219            BigTableError::WriteTime(s) => vec![("error", s.to_string())],
220            BigTableError::Pool(e) => vec![("error", e.to_string())],
221            _ => vec![],
222        }
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn expected_timeout_failures_are_metrics_only() {
232        assert!(!BigTableError::AttemptTimeout.is_sentry_event());
233        assert!(!BigTableError::PreSendTimeout.is_sentry_event());
234        assert!(!BigTableError::OperationTimeout.is_sentry_event());
235        assert!(BigTableError::Read(tonic::Status::internal("boom")).is_sentry_event());
236    }
237}