use http::{HeaderMap, Request, Response, StatusCode};
use std::{convert::Infallible, fmt, marker::PhantomData};
pub(crate) mod grpc_errors_as_failures;
mod map_failure_class;
mod status_in_range_is_error;
pub use self::{
grpc_errors_as_failures::{
GrpcCode, GrpcEosErrorsAsFailures, GrpcErrorsAsFailures, GrpcFailureClass,
},
map_failure_class::MapFailureClass,
status_in_range_is_error::{StatusInRangeAsFailures, StatusInRangeFailureClass},
};
pub trait MakeClassifier {
type Classifier: ClassifyResponse<
FailureClass = Self::FailureClass,
ClassifyEos = Self::ClassifyEos,
>;
type FailureClass;
type ClassifyEos: ClassifyEos<FailureClass = Self::FailureClass>;
fn make_classifier<B>(&self, req: &Request<B>) -> Self::Classifier;
}
#[derive(Debug, Clone)]
pub struct SharedClassifier<C> {
classifier: C,
}
impl<C> SharedClassifier<C> {
pub fn new(classifier: C) -> Self
where
C: ClassifyResponse + Clone,
{
Self { classifier }
}
}
impl<C> MakeClassifier for SharedClassifier<C>
where
C: ClassifyResponse + Clone,
{
type FailureClass = C::FailureClass;
type ClassifyEos = C::ClassifyEos;
type Classifier = C;
fn make_classifier<B>(&self, _req: &Request<B>) -> Self::Classifier {
self.classifier.clone()
}
}
pub trait ClassifyResponse {
type FailureClass;
type ClassifyEos: ClassifyEos<FailureClass = Self::FailureClass>;
fn classify_response<B>(
self,
res: &Response<B>,
) -> ClassifiedResponse<Self::FailureClass, Self::ClassifyEos>;
fn classify_error<E>(self, error: &E) -> Self::FailureClass
where
E: fmt::Display + 'static;
fn map_failure_class<F, NewClass>(self, f: F) -> MapFailureClass<Self, F>
where
Self: Sized,
F: FnOnce(Self::FailureClass) -> NewClass,
{
MapFailureClass::new(self, f)
}
}
pub trait ClassifyEos {
type FailureClass;
fn classify_eos(self, trailers: Option<&HeaderMap>) -> Result<(), Self::FailureClass>;
fn classify_error<E>(self, error: &E) -> Self::FailureClass
where
E: fmt::Display + 'static;
fn map_failure_class<F, NewClass>(self, f: F) -> MapFailureClass<Self, F>
where
Self: Sized,
F: FnOnce(Self::FailureClass) -> NewClass,
{
MapFailureClass::new(self, f)
}
}
#[derive(Debug)]
pub enum ClassifiedResponse<FailureClass, ClassifyEos> {
Ready(Result<(), FailureClass>),
RequiresEos(ClassifyEos),
}
pub struct NeverClassifyEos<T> {
_output_ty: PhantomData<fn() -> T>,
_never: Infallible,
}
impl<T> ClassifyEos for NeverClassifyEos<T> {
type FailureClass = T;
fn classify_eos(self, _trailers: Option<&HeaderMap>) -> Result<(), Self::FailureClass> {
unreachable!()
}
fn classify_error<E>(self, _error: &E) -> Self::FailureClass
where
E: fmt::Display + 'static,
{
unreachable!()
}
}
impl<T> fmt::Debug for NeverClassifyEos<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NeverClassifyEos").finish()
}
}
#[derive(Clone, Debug, Default)]
pub struct ServerErrorsAsFailures {
_priv: (),
}
impl ServerErrorsAsFailures {
pub fn new() -> Self {
Self::default()
}
pub fn make_classifier() -> SharedClassifier<Self> {
SharedClassifier::new(Self::new())
}
}
impl ClassifyResponse for ServerErrorsAsFailures {
type FailureClass = ServerErrorsFailureClass;
type ClassifyEos = NeverClassifyEos<ServerErrorsFailureClass>;
fn classify_response<B>(
self,
res: &Response<B>,
) -> ClassifiedResponse<Self::FailureClass, Self::ClassifyEos> {
if res.status().is_server_error() {
ClassifiedResponse::Ready(Err(ServerErrorsFailureClass::StatusCode(res.status())))
} else {
ClassifiedResponse::Ready(Ok(()))
}
}
fn classify_error<E>(self, error: &E) -> Self::FailureClass
where
E: fmt::Display + 'static,
{
ServerErrorsFailureClass::Error(error.to_string())
}
}
#[derive(Debug)]
pub enum ServerErrorsFailureClass {
StatusCode(StatusCode),
Error(String),
}
impl fmt::Display for ServerErrorsFailureClass {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::StatusCode(code) => write!(f, "Status code: {}", code),
Self::Error(error) => write!(f, "Error: {}", error),
}
}
}
#[cfg(test)]
mod usable_for_retries {
#![allow(dead_code)]
use std::fmt;
use http::{Request, Response};
use tower::retry::Policy;
use super::{ClassifiedResponse, ClassifyResponse};
trait IsRetryable {
fn is_retryable(&self) -> bool;
}
#[derive(Clone)]
struct RetryBasedOnClassification<C> {
classifier: C,
}
impl<ReqB, ResB, E, C> Policy<Request<ReqB>, Response<ResB>, E> for RetryBasedOnClassification<C>
where
C: ClassifyResponse + Clone,
E: fmt::Display + 'static,
C::FailureClass: IsRetryable,
ResB: http_body::Body,
Request<ReqB>: Clone,
E: std::error::Error + 'static,
{
type Future = std::future::Ready<()>;
fn retry(
&mut self,
_req: &mut Request<ReqB>,
res: &mut Result<Response<ResB>, E>,
) -> Option<Self::Future> {
match res {
Ok(res) => {
if let ClassifiedResponse::Ready(class) =
self.classifier.clone().classify_response(res)
{
if class.err()?.is_retryable() {
return Some(std::future::ready(()));
}
}
None
}
Err(err) => self
.classifier
.clone()
.classify_error(err)
.is_retryable()
.then(|| std::future::ready(())),
}
}
fn clone_request(&mut self, req: &Request<ReqB>) -> Option<Request<ReqB>> {
Some(req.clone())
}
}
}