use crate::{FirestoreTransaction, FirestoreTransactionId};
use gcloud_sdk::google::firestore::v1::WriteRequest;
use rsb_derive::Builder;
use serde::*;
use std::error::Error;
use std::fmt::Display;
use std::fmt::Formatter;
#[derive(Debug)]
pub enum FirestoreError {
SystemError(FirestoreSystemError),
DatabaseError(FirestoreDatabaseError),
DataConflictError(FirestoreDataConflictError),
DataNotFoundError(FirestoreDataNotFoundError),
InvalidParametersError(FirestoreInvalidParametersError),
SerializeError(FirestoreSerializationError),
DeserializeError(FirestoreSerializationError),
NetworkError(FirestoreNetworkError),
ErrorInTransaction(FirestoreErrorInTransaction),
CacheError(FirestoreCacheError),
}
impl FirestoreError {
pub(crate) fn invalid_parameters(field: impl Into<String>, error: impl Into<String>) -> Self {
FirestoreError::InvalidParametersError(FirestoreInvalidParametersError::new(
FirestoreInvalidParametersPublicDetails::new(field.into(), error.into()),
))
}
pub(crate) fn system_error(message: impl Into<String>) -> Self {
FirestoreError::SystemError(FirestoreSystemError::new(
FirestoreErrorPublicGenericDetails::new("SystemError".into()),
message.into(),
))
}
pub(crate) fn from_read_status(status: gcloud_sdk::tonic::Status) -> Self {
match status.code() {
gcloud_sdk::tonic::Code::DeadlineExceeded => FirestoreError::retryable_read(status),
_ => FirestoreError::from(status),
}
}
pub(crate) fn retryable_read(status: gcloud_sdk::tonic::Status) -> Self {
FirestoreError::DatabaseError(FirestoreDatabaseError::new(
FirestoreErrorPublicGenericDetails::new(format!("{:?}", status.code())),
status.to_string(),
true,
))
}
}
impl Display for FirestoreError {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match *self {
FirestoreError::SystemError(ref err) => err.fmt(f),
FirestoreError::DatabaseError(ref err) => err.fmt(f),
FirestoreError::DataConflictError(ref err) => err.fmt(f),
FirestoreError::DataNotFoundError(ref err) => err.fmt(f),
FirestoreError::InvalidParametersError(ref err) => err.fmt(f),
FirestoreError::SerializeError(ref err) => err.fmt(f),
FirestoreError::DeserializeError(ref err) => err.fmt(f),
FirestoreError::NetworkError(ref err) => err.fmt(f),
FirestoreError::ErrorInTransaction(ref err) => err.fmt(f),
FirestoreError::CacheError(ref err) => err.fmt(f),
}
}
}
impl Error for FirestoreError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match *self {
FirestoreError::SystemError(ref err) => Some(err),
FirestoreError::DatabaseError(ref err) => Some(err),
FirestoreError::DataConflictError(ref err) => Some(err),
FirestoreError::DataNotFoundError(ref err) => Some(err),
FirestoreError::InvalidParametersError(ref err) => Some(err),
FirestoreError::SerializeError(ref err) => Some(err),
FirestoreError::DeserializeError(ref err) => Some(err),
FirestoreError::NetworkError(ref err) => Some(err),
FirestoreError::ErrorInTransaction(ref err) => Some(err),
FirestoreError::CacheError(ref err) => Some(err),
}
}
}
#[derive(Debug, Eq, PartialEq, Clone, Builder, Serialize, Deserialize)]
pub struct FirestoreErrorPublicGenericDetails {
pub code: String,
}
impl Display for FirestoreErrorPublicGenericDetails {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f, "Error code: {}", self.code)
}
}
#[derive(Debug, Eq, PartialEq, Clone, Builder)]
pub struct FirestoreSystemError {
pub public: FirestoreErrorPublicGenericDetails,
pub message: String,
}
impl Display for FirestoreSystemError {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(
f,
"Firestore system/internal error: {}. {}",
self.public, self.message
)
}
}
impl std::error::Error for FirestoreSystemError {}
#[derive(Debug, Clone, Builder)]
pub struct FirestoreDatabaseError {
pub public: FirestoreErrorPublicGenericDetails,
pub details: String,
pub retry_possible: bool,
}
impl Display for FirestoreDatabaseError {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(
f,
"Database general error occurred: {}. {}. Retry possibility: {}",
self.public, self.details, self.retry_possible
)
}
}
impl std::error::Error for FirestoreDatabaseError {}
#[derive(Debug, Clone, Builder)]
pub struct FirestoreDataConflictError {
pub public: FirestoreErrorPublicGenericDetails,
pub details: String,
}
impl Display for FirestoreDataConflictError {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(
f,
"Database conflict error occurred: {}. {}",
self.public, self.details
)
}
}
impl std::error::Error for FirestoreDataConflictError {}
#[derive(Debug, Clone, Builder)]
pub struct FirestoreDataNotFoundError {
pub public: FirestoreErrorPublicGenericDetails,
pub data_detail_message: String,
}
impl Display for FirestoreDataNotFoundError {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(
f,
"Data not found error occurred: {}. {}",
self.public, self.data_detail_message
)
}
}
impl std::error::Error for FirestoreDataNotFoundError {}
#[derive(Debug, Eq, PartialEq, Clone, Builder, Serialize, Deserialize)]
pub struct FirestoreInvalidParametersPublicDetails {
pub field: String,
pub error: String,
}
impl Display for FirestoreInvalidParametersPublicDetails {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(
f,
"Invalid parameters error: {}. {}",
self.field, self.error
)
}
}
#[derive(Debug, Clone, Builder)]
pub struct FirestoreInvalidParametersError {
pub public: FirestoreInvalidParametersPublicDetails,
}
impl Display for FirestoreInvalidParametersError {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f, "{}", self.public)
}
}
impl std::error::Error for FirestoreInvalidParametersError {}
#[derive(Debug, Eq, PartialEq, Clone, Builder, Serialize, Deserialize)]
pub struct FirestoreInvalidJsonErrorPublicDetails {
pub code: String,
}
#[derive(Debug, Eq, PartialEq, Clone, Builder)]
pub struct FirestoreNetworkError {
pub public: FirestoreErrorPublicGenericDetails,
pub message: String,
}
impl Display for FirestoreNetworkError {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f, "Network error: {}. {}", self.public, self.message)
}
}
impl std::error::Error for FirestoreNetworkError {}
impl From<gcloud_sdk::error::Error> for FirestoreError {
fn from(e: gcloud_sdk::error::Error) -> Self {
FirestoreError::SystemError(FirestoreSystemError::new(
FirestoreErrorPublicGenericDetails::new(format!("{:?}", e.kind())),
format!("GCloud system error: {e}"),
))
}
}
#[cfg(feature = "admin")]
impl From<gcloud_sdk::google::rpc::Status> for FirestoreError {
fn from(status: gcloud_sdk::google::rpc::Status) -> Self {
gcloud_sdk::tonic::Status::new(gcloud_sdk::tonic::Code::from(status.code), status.message)
.into()
}
}
impl From<gcloud_sdk::tonic::Status> for FirestoreError {
fn from(status: gcloud_sdk::tonic::Status) -> Self {
match status.code() {
gcloud_sdk::tonic::Code::AlreadyExists => {
FirestoreError::DataConflictError(FirestoreDataConflictError::new(
FirestoreErrorPublicGenericDetails::new(format!("{:?}", status.code())),
format!("{status}"),
))
}
gcloud_sdk::tonic::Code::NotFound => {
FirestoreError::DataNotFoundError(FirestoreDataNotFoundError::new(
FirestoreErrorPublicGenericDetails::new(format!("{:?}", status.code())),
format!("{status}"),
))
}
gcloud_sdk::tonic::Code::Unknown => check_hyper_errors(status),
code => {
let retry_possible = match code {
gcloud_sdk::tonic::Code::Aborted
| gcloud_sdk::tonic::Code::Cancelled
| gcloud_sdk::tonic::Code::Unavailable
| gcloud_sdk::tonic::Code::ResourceExhausted => true,
gcloud_sdk::tonic::Code::DeadlineExceeded => status
.source()
.is_some_and(|source| source.is::<gcloud_sdk::RequestTimeoutElapsed>()),
_ => false,
};
FirestoreError::DatabaseError(FirestoreDatabaseError::new(
FirestoreErrorPublicGenericDetails::new(format!("{code:?}")),
format!("{status}"),
retry_possible,
))
}
}
}
}
fn check_hyper_errors(status: gcloud_sdk::tonic::Status) -> FirestoreError {
match status.source() {
Some(hyper_error) => match hyper_error.downcast_ref::<hyper::Error>() {
Some(err) if err.is_closed() => {
FirestoreError::DatabaseError(FirestoreDatabaseError::new(
FirestoreErrorPublicGenericDetails::new("CONNECTION_CLOSED".into()),
format!("Hyper error: {err}"),
true,
))
}
Some(err) if err.is_timeout() => {
FirestoreError::DatabaseError(FirestoreDatabaseError::new(
FirestoreErrorPublicGenericDetails::new("CONNECTION_TIMEOUT".into()),
format!("Hyper error: {err}"),
true,
))
}
Some(err) => FirestoreError::DatabaseError(FirestoreDatabaseError::new(
FirestoreErrorPublicGenericDetails::new(format!("{:?}", status.code())),
format!("Hyper error: {err}"),
false,
)),
_ if status.code() == gcloud_sdk::tonic::Code::Unknown
&& status.message().contains("transport error") =>
{
FirestoreError::DatabaseError(FirestoreDatabaseError::new(
FirestoreErrorPublicGenericDetails::new("CONNECTION_ERROR".into()),
format!("{status}"),
true,
))
}
_ => FirestoreError::DatabaseError(FirestoreDatabaseError::new(
FirestoreErrorPublicGenericDetails::new(format!("{:?}", status.code())),
format!("{status}"),
false,
)),
},
_ => FirestoreError::DatabaseError(FirestoreDatabaseError::new(
FirestoreErrorPublicGenericDetails::new(format!("{:?}", status.code())),
format!("{status} without root cause"),
false,
)),
}
}
impl serde::ser::Error for FirestoreError {
fn custom<T>(msg: T) -> Self
where
T: Display,
{
FirestoreError::SerializeError(FirestoreSerializationError::from_message(msg.to_string()))
}
}
impl serde::de::Error for FirestoreError {
fn custom<T>(msg: T) -> Self
where
T: Display,
{
FirestoreError::DeserializeError(FirestoreSerializationError::from_message(msg.to_string()))
}
}
#[derive(Debug, Builder)]
pub struct FirestoreSerializationError {
pub public: FirestoreErrorPublicGenericDetails,
pub message: String,
pub document_path: Option<String>,
}
impl FirestoreSerializationError {
pub fn from_message<S: AsRef<str>>(message: S) -> FirestoreSerializationError {
let message_str = message.as_ref().to_string();
FirestoreSerializationError::new(
FirestoreErrorPublicGenericDetails::new("SerializationError".to_string()),
message_str,
)
}
}
impl Display for FirestoreSerializationError {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(
f,
"Invalid serialization: {}. {}. Document path: {}",
self.public,
self.message,
self.document_path.as_deref().unwrap_or("-")
)
}
}
impl std::error::Error for FirestoreSerializationError {}
#[derive(Debug, Builder)]
pub struct FirestoreCacheError {
pub public: FirestoreErrorPublicGenericDetails,
pub message: String,
}
impl Display for FirestoreCacheError {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f, "Cache error: {}. {}", self.public, self.message)
}
}
impl std::error::Error for FirestoreCacheError {}
impl From<jiff::Error> for FirestoreError {
fn from(err: jiff::Error) -> Self {
FirestoreError::DeserializeError(FirestoreSerializationError::from_message(format!(
"Date/time error: {err}"
)))
}
}
impl From<tokio::sync::mpsc::error::SendError<gcloud_sdk::google::firestore::v1::WriteRequest>>
for FirestoreError
{
fn from(send_error: tokio::sync::mpsc::error::SendError<WriteRequest>) -> Self {
FirestoreError::NetworkError(FirestoreNetworkError::new(
FirestoreErrorPublicGenericDetails::new("SEND_STREAM_ERROR".into()),
format!("Send stream error: {send_error}"),
))
}
}
#[derive(Debug, Builder)]
pub struct FirestoreErrorInTransaction {
pub transaction_id: FirestoreTransactionId,
pub source: Box<dyn std::error::Error + Send + Sync>,
}
impl FirestoreErrorInTransaction {
pub fn permanent<E: std::error::Error + Send + Sync + 'static>(
transaction: &FirestoreTransaction,
source: E,
) -> BackoffError<FirestoreError> {
BackoffError::permanent(FirestoreError::ErrorInTransaction(
FirestoreErrorInTransaction {
transaction_id: transaction.transaction_id().clone(),
source: Box::new(source),
},
))
}
pub fn transient<E: std::error::Error + Send + Sync + 'static>(
transaction: &FirestoreTransaction,
source: E,
) -> BackoffError<FirestoreError> {
BackoffError::transient(FirestoreError::ErrorInTransaction(
FirestoreErrorInTransaction {
transaction_id: transaction.transaction_id().clone(),
source: Box::new(source),
},
))
}
pub fn retry_after<E: std::error::Error + Send + Sync + 'static>(
transaction: &FirestoreTransaction,
source: E,
retry_after: std::time::Duration,
) -> BackoffError<FirestoreError> {
BackoffError::retry_after(
FirestoreError::ErrorInTransaction(FirestoreErrorInTransaction {
transaction_id: transaction.transaction_id().clone(),
source: Box::new(source),
}),
retry_after,
)
}
}
impl Display for FirestoreErrorInTransaction {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(
f,
"Error occurred inside run transaction scope {}: {}",
hex::encode(&self.transaction_id),
self.source
)
}
}
impl std::error::Error for FirestoreErrorInTransaction {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.source.as_ref())
}
}
pub type BackoffError<E> = backoff::Error<E>;
pub(crate) fn firestore_err_to_backoff(err: FirestoreError) -> BackoffError<FirestoreError> {
match err {
FirestoreError::DatabaseError(ref db_err) if db_err.retry_possible => {
backoff::Error::transient(err)
}
other => backoff::Error::permanent(other),
}
}
pub(crate) type AnyBoxedErrResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
impl From<std::io::Error> for FirestoreError {
fn from(io_error: std::io::Error) -> Self {
FirestoreError::SystemError(FirestoreSystemError::new(
FirestoreErrorPublicGenericDetails::new(format!("{:?}", io_error.kind())),
format!("I/O error: {io_error}"),
))
}
}
#[cfg(feature = "caching-persistent")]
impl From<gcloud_sdk::prost::EncodeError> for FirestoreError {
fn from(err: gcloud_sdk::prost::EncodeError) -> Self {
FirestoreError::SerializeError(FirestoreSerializationError::new(
FirestoreErrorPublicGenericDetails::new("PrototBufEncodeError".into()),
format!("Protobuf serialization error: {err}"),
))
}
}
#[cfg(any(feature = "caching-persistent", feature = "admin"))]
impl From<gcloud_sdk::prost::DecodeError> for FirestoreError {
fn from(err: gcloud_sdk::prost::DecodeError) -> Self {
FirestoreError::SerializeError(FirestoreSerializationError::new(
FirestoreErrorPublicGenericDetails::new("PrototBufDecodeError".into()),
format!("Protobuf deserialization error: {err}"),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn a_client_timeout_is_retryable_and_a_server_deadline_is_not() {
let client_timeout = gcloud_sdk::tonic::Status::from(
gcloud_sdk::RequestTimeoutElapsed::Idle(Duration::from_secs(5)),
);
let server_deadline = gcloud_sdk::tonic::Status::deadline_exceeded("Deadline Exceeded");
for (status, retry_possible) in [(client_timeout, true), (server_deadline, false)] {
match FirestoreError::from(status) {
FirestoreError::DatabaseError(err) => {
assert_eq!(err.public.code, "DeadlineExceeded");
assert_eq!(err.retry_possible, retry_possible, "{err:?}");
}
other => panic!("expected DatabaseError, got {other:?}"),
}
}
}
#[cfg(feature = "admin")]
#[test]
fn rpc_status_maps_like_the_equivalent_tonic_status() {
let rpc_status = gcloud_sdk::google::rpc::Status {
code: gcloud_sdk::tonic::Code::FailedPrecondition as i32,
message: "precondition failed".to_string(),
details: vec![],
};
let from_rpc = FirestoreError::from(rpc_status);
let from_tonic = FirestoreError::from(gcloud_sdk::tonic::Status::new(
gcloud_sdk::tonic::Code::FailedPrecondition,
"precondition failed",
));
match (from_rpc, from_tonic) {
(
FirestoreError::DatabaseError(from_rpc),
FirestoreError::DatabaseError(from_tonic),
) => {
assert_eq!(from_rpc.public.code, from_tonic.public.code);
assert_eq!(from_rpc.public.code, "FailedPrecondition");
assert_eq!(from_rpc.retry_possible, from_tonic.retry_possible);
assert!(!from_rpc.retry_possible);
}
other => panic!("expected DatabaseError for both conversions, got {other:?}"),
}
}
#[cfg(feature = "admin")]
#[test]
fn rpc_status_marks_unavailable_as_retryable() {
let rpc_status = gcloud_sdk::google::rpc::Status {
code: gcloud_sdk::tonic::Code::Unavailable as i32,
message: "backend unavailable".to_string(),
details: vec![],
};
match FirestoreError::from(rpc_status) {
FirestoreError::DatabaseError(err) => {
assert_eq!(err.public.code, "Unavailable");
assert!(err.retry_possible);
}
other => panic!("expected DatabaseError, got {other:?}"),
}
}
}