Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add normalized ReportableError to errors #1559

Merged
merged 6 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion syncserver-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ authors.workspace = true
edition.workspace = true

[dependencies]
backtrace.workspace = true
cadence.workspace = true
futures.workspace = true
sha2.workspace = true
Expand All @@ -15,4 +16,3 @@ slog.workspace = true
slog-scope.workspace = true
actix-web.workspace = true
hkdf.workspace = true

28 changes: 27 additions & 1 deletion syncserver-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::{
};

use actix_web::web;
use backtrace::Backtrace;
use hkdf::Hkdf;
use sha2::Sha256;

Expand Down Expand Up @@ -59,9 +60,34 @@ macro_rules! impl_fmt_display {
}

pub trait ReportableError {
fn error_backtrace(&self) -> String;
/// Like [Error::source] but returns the source (if any) of this error as a
/// [ReportableError] if it implements the trait. Otherwise callers of this
/// method will likely subsequently call [Error::source] to return the
/// source (if any) as the parent [Error] trait.

fn reportable_source(&self) -> Option<&(dyn ReportableError + 'static)> {
None
}

/// Return a `Backtrace` for this Error if one was captured
fn backtrace(&self) -> Option<&Backtrace>;

/// Whether this error is reported to Sentry
fn is_sentry_event(&self) -> bool;

/// Errors that don't emit Sentry events (!is_sentry_event()) emit an
/// increment metric instead with this label
fn metric_label(&self) -> Option<String>;

fn tags(&self) -> Vec<(&str, String)> {
vec![]
}

/// Experimental: return key value pairs for Sentry Event's extra data
/// TODO: should probably return Vec<(&str, Value)> or Vec<(String, Value)>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// TODO: should probably return Vec<(&str, Value)> or Vec<(String, Value)>

fn extras(&self) -> Vec<(&str, String)> {
vec![]
}
}

/// Types that implement this trait can represent internal errors.
Expand Down
30 changes: 29 additions & 1 deletion syncserver-db-common/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::fmt;

use backtrace::Backtrace;
use http::StatusCode;
use syncserver_common::{from_error, impl_fmt_display};
use syncserver_common::{from_error, impl_fmt_display, ReportableError};
use thiserror::Error;

/// Error specific to any MySQL database backend. These errors are not related to the syncstorage
Expand Down Expand Up @@ -39,6 +39,34 @@ impl From<MysqlErrorKind> for MysqlError {
}
}

impl ReportableError for MysqlError {
fn reportable_source(&self) -> Option<&(dyn ReportableError + 'static)> {
// There are no further local errors, therefore no need to
// look deeper
None
}
jrconlin marked this conversation as resolved.
Show resolved Hide resolved

fn is_sentry_event(&self) -> bool {
true
}

fn metric_label(&self) -> Option<String> {
Some(
match self.kind {
MysqlErrorKind::DieselQuery(_) => "diesel_query",
MysqlErrorKind::DieselConnection(_) => "diesel_connection",
MysqlErrorKind::Pool(_) => "pool",
MysqlErrorKind::Migration(_) => "migration",
}
.to_string(),
)
}

fn backtrace(&self) -> Option<&Backtrace> {
Some(&self.backtrace)
}
}

impl_fmt_display!(MysqlError, MysqlErrorKind);

from_error!(
Expand Down
4 changes: 2 additions & 2 deletions syncserver/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,8 @@ from_error!(HawkError, ApiError, ApiErrorKind::Hawk);
from_error!(ValidationError, ApiError, ApiErrorKind::Validation);

impl ReportableError for ApiError {
fn error_backtrace(&self) -> String {
format!("{:#?}", self.backtrace)
fn backtrace(&self) -> Option<&Backtrace> {
Some(&self.backtrace)
}

fn is_sentry_event(&self) -> bool {
Expand Down
29 changes: 22 additions & 7 deletions syncserver/src/tokenserver/extractors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ impl TokenserverRequest {
// always include a client state.
if !self.user.client_state.is_empty() && self.auth_data.client_state.is_empty() {
let error_message = "Unacceptable client-state value empty string".to_owned();
return Err(TokenserverError::invalid_client_state(error_message));
return Err(TokenserverError::invalid_client_state(error_message, None));
}

// The client state on the request must not have been used in the past.
Expand All @@ -114,7 +114,10 @@ impl TokenserverRequest {
{
let error_message = "Unacceptable client-state value stale value".to_owned();
warn!("Client attempted stale value"; "uid"=> self.user.uid, "client_state"=> self.user.client_state.clone());
return Err(TokenserverError::invalid_client_state(error_message));
return Err(TokenserverError::invalid_client_state(
error_message,
Some(Box::new(vec![("is_stale", "true".to_owned())])),
));
}

// If the client state on the request differs from the most recently-used client state, it must
Expand All @@ -124,7 +127,7 @@ impl TokenserverRequest {
{
let error_message =
"Unacceptable client-state value new value with no generation change".to_owned();
return Err(TokenserverError::invalid_client_state(error_message));
return Err(TokenserverError::invalid_client_state(error_message, None));
}

// If the client state on the request differs from the most recently-used client state, it must
Expand All @@ -135,7 +138,7 @@ impl TokenserverRequest {
let error_message =
"Unacceptable client-state value new value with no keys_changed_at change"
.to_owned();
return Err(TokenserverError::invalid_client_state(error_message));
return Err(TokenserverError::invalid_client_state(error_message, None));
}

// The generation on the request cannot be earlier than the generation stored on the user
Expand Down Expand Up @@ -1237,7 +1240,13 @@ mod tests {

let error = tokenserver_request.validate().unwrap_err();
let error_message = "Unacceptable client-state value stale value".to_owned();
assert_eq!(error, TokenserverError::invalid_client_state(error_message));
assert_eq!(
error,
TokenserverError::invalid_client_state(
error_message,
Some(Box::new(vec![("is_stale", "true".to_owned())]))
)
);
}

#[actix_rt::test]
Expand Down Expand Up @@ -1275,7 +1284,10 @@ mod tests {
let error = tokenserver_request.validate().unwrap_err();
let error_message =
"Unacceptable client-state value new value with no generation change".to_owned();
assert_eq!(error, TokenserverError::invalid_client_state(error_message));
assert_eq!(
error,
TokenserverError::invalid_client_state(error_message, None),
);
}

#[actix_rt::test]
Expand Down Expand Up @@ -1313,7 +1325,10 @@ mod tests {
let error = tokenserver_request.validate().unwrap_err();
let error_message =
"Unacceptable client-state value new value with no keys_changed_at change".to_owned();
assert_eq!(error, TokenserverError::invalid_client_state(error_message));
assert_eq!(
error,
TokenserverError::invalid_client_state(error_message, None)
);
}

fn extract_body_as_str(sresponse: ServiceResponse) -> String {
Expand Down
9 changes: 6 additions & 3 deletions syncserver/src/web/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ fn verify_hmac(info: &[u8], key: &[u8], expected: &[u8]) -> ApiResult<()> {

#[cfg(test)]
mod tests {
use std::fmt::{self, Display, Formatter};

use super::{HawkPayload, Secrets};

#[test]
Expand Down Expand Up @@ -532,9 +534,10 @@ mod tests {
}
}

impl ToString for HawkHeader {
fn to_string(&self) -> String {
format!(
impl Display for HawkHeader {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
write!(
fmt,
"Hawk id=\"{}\", mac=\"{}\", nonce=\"{}\", ts=\"{}\"",
self.id, self.mac, self.nonce, self.ts
)
Expand Down
2 changes: 1 addition & 1 deletion syncserver/src/web/middleware/sentry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ where
E: ReportableError + StdError,
{
let mut exception = exception_from_error(err);
exception.stacktrace = parse_stacktrace(&err.error_backtrace());
exception.stacktrace = parse_stacktrace(&format!("{:?}", &err.backtrace()));
exception
}

Expand Down
8 changes: 4 additions & 4 deletions syncserver/src/web/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ impl DbTransactionPool {
/// transaction is rolled back. If the action succeeds, the transaction is
/// NOT committed. Further processing is required before we are sure the
/// action has succeeded (ex. check HTTP response for internal error).
async fn transaction_internal<'a, A: 'a, R, F>(
&'a self,
async fn transaction_internal<'a, A, R, F>(
&self,
request: HttpRequest,
action: A,
) -> Result<(R, Box<dyn Db<Error = DbError>>), ApiError>
Expand Down Expand Up @@ -90,7 +90,7 @@ impl DbTransactionPool {
}

/// Perform an action inside of a DB transaction.
pub async fn transaction<'a, A: 'a, R, F>(
pub async fn transaction<'a, A, R, F>(
&'a self,
request: HttpRequest,
action: A,
Expand All @@ -108,7 +108,7 @@ impl DbTransactionPool {

/// Perform an action inside of a DB transaction. This method will rollback
/// if the HTTP response is an error.
pub async fn transaction_http<'a, A: 'a, F>(
pub async fn transaction_http<'a, A, F>(
&'a self,
request: HttpRequest,
action: A,
Expand Down
10 changes: 7 additions & 3 deletions syncstorage-db-common/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,19 +93,23 @@ impl DbErrorIntrospect for SyncstorageDbError {
}

impl ReportableError for SyncstorageDbError {
fn reportable_source(&self) -> Option<&(dyn ReportableError + 'static)> {
None
}

fn is_sentry_event(&self) -> bool {
!matches!(&self.kind, SyncstorageDbErrorKind::Conflict)
}

fn metric_label(&self) -> Option<String> {
match &self.kind {
match self.kind {
SyncstorageDbErrorKind::Conflict => Some("storage.conflict".to_owned()),
_ => None,
}
}

fn error_backtrace(&self) -> String {
format!("{:#?}", self.backtrace)
fn backtrace(&self) -> Option<&Backtrace> {
Some(&self.backtrace)
}
}

Expand Down
14 changes: 10 additions & 4 deletions syncstorage-db-common/src/params.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
//! Parameter types for database methods.
use std::{collections::HashMap, num::ParseIntError, str::FromStr};
use core::fmt;
use std::{
collections::HashMap,
fmt::{Display, Formatter},
num::ParseIntError,
str::FromStr,
};

use diesel::Queryable;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -61,10 +67,10 @@ pub struct Offset {
pub offset: u64,
}

impl ToString for Offset {
fn to_string(&self) -> String {
impl Display for Offset {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
// issue559: Disable ':' support for now.
self.offset.to_string()
write!(fmt, "{:?}", self.offset)
jrconlin marked this conversation as resolved.
Show resolved Hide resolved
/*
match self.timestamp {
None => self.offset.to_string(),
Expand Down
3 changes: 3 additions & 0 deletions syncstorage-mysql/src/diesel_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,16 @@ impl QueryFragment<Mysql> for LockInShareMode {
}
}

// TODO: can we kill this?
jrconlin marked this conversation as resolved.
Show resolved Hide resolved
#[allow(dead_code)]
/// Emit 'ON DUPLICATE KEY UPDATE'
pub trait IntoDuplicateValueClause {
type ValueClause;

fn into_value_clause(self) -> Self::ValueClause;
}

#[allow(dead_code)]
pub trait OnDuplicateKeyUpdateDsl<T, U, Op, Ret> {
fn on_duplicate_key_update<X>(self, expression: X) -> OnDuplicateKeyUpdate<T, U, Op, Ret, X>
where
Expand Down
30 changes: 23 additions & 7 deletions syncstorage-mysql/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,23 +92,39 @@ impl DbErrorIntrospect for DbError {
}

impl ReportableError for DbError {
fn reportable_source(&self) -> Option<&(dyn ReportableError + 'static)> {
Some(match &self.kind {
DbErrorKind::Common(e) => e,
DbErrorKind::Mysql(e) => e,
})
}

fn is_sentry_event(&self) -> bool {
match &self.kind {
DbErrorKind::Common(e) => e.is_sentry_event(),
_ => true,
DbErrorKind::Mysql(e) => e.is_sentry_event(),
}
}

fn metric_label(&self) -> Option<String> {
if let DbErrorKind::Common(e) = &self.kind {
e.metric_label()
} else {
None
match &self.kind {
DbErrorKind::Common(e) => e.metric_label(),
DbErrorKind::Mysql(e) => e.metric_label(),
}
}

fn error_backtrace(&self) -> String {
format!("{:#?}", self.backtrace)
fn backtrace(&self) -> Option<&Backtrace> {
match &self.kind {
DbErrorKind::Common(e) => e.backtrace(),
DbErrorKind::Mysql(e) => e.backtrace(),
}
}

fn tags(&self) -> Vec<(&str, String)> {
match &self.kind {
DbErrorKind::Common(e) => e.tags(),
DbErrorKind::Mysql(e) => e.tags(),
}
}
}

Expand Down
3 changes: 1 addition & 2 deletions syncstorage-mysql/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,7 @@ impl MysqlDb {
.session
.borrow()
.coll_locks
.get(&(user_id as u32, collection_id))
.is_some()
.contains_key(&(user_id as u32, collection_id))
{
return Ok(());
}
Expand Down
Loading