Skip to content

Commit 90ea4a9

Browse files
weifanglabconradoplgCopilotoxarbitrage
authored
feat(tower-fallback): support custom fallback policies (#10923)
* feat(tower-fallback): support custom fallback policies Signed-off-by: weifanglab <weifanglab@outlook.com> * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Signed-off-by: weifanglab <weifanglab@outlook.com> Co-authored-by: Conrado <conradoplg@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Conrado Gouvea <conrado@zfnd.org> Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com>
1 parent b023ac7 commit 90ea4a9

5 files changed

Lines changed: 124 additions & 22 deletions

File tree

tower-fallback/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
12+
- Added `Fallback::new_with_policy()` for selecting fallback behavior based on
13+
the first service's result.
14+
815
## [0.2.42] - 2026-07-10
916

1017
### Changed

tower-fallback/src/future.rs

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,22 @@ use futures_core::ready;
1111
use pin_project::pin_project;
1212
use tower::Service;
1313

14-
use crate::BoxedError;
14+
use crate::{BoxedError, FallbackPolicy, OnError};
1515

1616
/// Future that completes either with the first service's successful response, or
1717
/// with the second service's response.
1818
#[pin_project]
19-
pub struct ResponseFuture<S1, S2, Request>
19+
pub struct ResponseFuture<S1, S2, Request, F = OnError>
2020
where
2121
S1: Service<Request>,
2222
S2: Service<Request, Response = <S1 as Service<Request>>::Response>,
23+
F: FallbackPolicy<<S1 as Service<Request>>::Response>,
24+
S1::Error: Into<BoxedError>,
2325
S2::Error: Into<BoxedError>,
2426
{
2527
#[pin]
2628
state: ResponseState<S1, S2, Request>,
29+
policy: F,
2730
}
2831

2932
#[pin_project(project_replace = __ResponseStateProjectionOwned, project = ResponseStateProj)]
@@ -51,23 +54,28 @@ where
5154
Tmp,
5255
}
5356

54-
impl<S1, S2, Request> ResponseFuture<S1, S2, Request>
57+
impl<S1, S2, Request, F> ResponseFuture<S1, S2, Request, F>
5558
where
5659
S1: Service<Request>,
5760
S2: Service<Request, Response = <S1 as Service<Request>>::Response>,
61+
F: FallbackPolicy<<S1 as Service<Request>>::Response>,
62+
S1::Error: Into<BoxedError>,
5863
S2::Error: Into<BoxedError>,
5964
{
60-
pub(crate) fn new(fut: S1::Future, req: Request, svc2: S2) -> Self {
65+
pub(crate) fn new(fut: S1::Future, req: Request, svc2: S2, policy: F) -> Self {
6166
ResponseFuture {
6267
state: ResponseState::PollResponse1 { fut, req, svc2 },
68+
policy,
6369
}
6470
}
6571
}
6672

67-
impl<S1, S2, Request> Future for ResponseFuture<S1, S2, Request>
73+
impl<S1, S2, Request, F> Future for ResponseFuture<S1, S2, Request, F>
6874
where
6975
S1: Service<Request>,
7076
S2: Service<Request, Response = <S1 as Service<Request>>::Response>,
77+
F: FallbackPolicy<<S1 as Service<Request>>::Response>,
78+
S1::Error: Into<BoxedError>,
7179
S2::Error: Into<BoxedError>,
7280
{
7381
type Output = Result<<S1 as Service<Request>>::Response, BoxedError>;
@@ -83,19 +91,22 @@ where
8391
// only returns Pending when a future or service returns Pending.
8492
loop {
8593
match this.state.as_mut().project() {
86-
ResponseStateProj::PollResponse1 { fut, .. } => match ready!(fut.poll(cx)) {
87-
Ok(rsp) => return Poll::Ready(Ok(rsp)),
88-
Err(_) => {
89-
tracing::debug!("got error from svc1, retrying on svc2");
94+
ResponseStateProj::PollResponse1 { fut, .. } => {
95+
let result = ready!(fut.poll(cx)).map_err(Into::into);
96+
97+
if this.policy.should_fallback(&result) {
98+
tracing::debug!("fallback policy selected svc2");
9099
if let __ResponseStateProjectionOwned::PollResponse1 { req, svc2, .. } =
91100
this.state.as_mut().project_replace(ResponseState::Tmp)
92101
{
93102
this.state.set(ResponseState::PollReady2 { req, svc2 });
94103
} else {
95104
unreachable!();
96105
}
106+
} else {
107+
return Poll::Ready(result);
97108
}
98-
},
109+
}
99110
ResponseStateProj::PollReady2 { svc2, .. } => match ready!(svc2.poll_ready(cx)) {
100111
Err(e) => return Poll::Ready(Err(e.into())),
101112
Ok(()) => {
@@ -119,10 +130,12 @@ where
119130
}
120131
}
121132

122-
impl<S1, S2, Request> Debug for ResponseFuture<S1, S2, Request>
133+
impl<S1, S2, Request, F> Debug for ResponseFuture<S1, S2, Request, F>
123134
where
124135
S1: Service<Request>,
125136
S2: Service<Request, Response = <S1 as Service<Request>>::Response>,
137+
F: FallbackPolicy<<S1 as Service<Request>>::Response>,
138+
S1::Error: Into<BoxedError>,
126139
Request: Debug,
127140
S1::Future: Debug,
128141
S2: Debug,

tower-fallback/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! A service combinator that sends requests to a first service, then retries
2-
//! processing on a second fallback service if the first service errors.
2+
//! processing on a second fallback service if the first service errors, or if a
3+
//! custom fallback policy selects the fallback service.
34
//!
45
//! Fallback designs have [a number of downsides][aws-fallback] but may be useful
56
//! in some cases. For instance, when using batch verification, the `Fallback`
@@ -13,7 +14,7 @@
1314
pub mod future;
1415
mod service;
1516

16-
pub use self::service::Fallback;
17+
pub use self::service::{Fallback, FallbackPolicy, OnError};
1718

1819
/// A boxed type-erased `std::error::Error` that can be sent between threads.
1920
pub type BoxedError = Box<dyn std::error::Error + Send + Sync + 'static>;

tower-fallback/src/service.rs

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,53 +5,102 @@ use tower::Service;
55
use super::future::ResponseFuture;
66
use crate::BoxedError;
77

8-
/// Provides fallback processing on a second service if the first service returned an error.
8+
/// Decides if a [`Fallback`] service should call its fallback service.
9+
pub trait FallbackPolicy<Response> {
10+
/// Returns `true` if the fallback service should handle this request.
11+
fn should_fallback(&self, result: &Result<Response, BoxedError>) -> bool;
12+
}
13+
14+
impl<Response, F> FallbackPolicy<Response> for F
15+
where
16+
F: Fn(&Result<Response, BoxedError>) -> bool,
17+
{
18+
fn should_fallback(&self, result: &Result<Response, BoxedError>) -> bool {
19+
self(result)
20+
}
21+
}
22+
23+
/// The default fallback policy.
24+
///
25+
/// Falls back whenever the first service returns an error.
26+
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
27+
pub struct OnError;
28+
29+
impl<Response> FallbackPolicy<Response> for OnError {
30+
fn should_fallback(&self, result: &Result<Response, BoxedError>) -> bool {
31+
result.is_err()
32+
}
33+
}
34+
35+
/// Provides fallback processing on a second service if its fallback policy selects it.
936
#[derive(Debug)]
10-
pub struct Fallback<S1, S2>
37+
pub struct Fallback<S1, S2, F = OnError>
1138
where
1239
S2: Clone,
1340
{
1441
svc1: S1,
1542
svc2: S2,
43+
policy: F,
1644
}
1745

18-
impl<S1: Clone, S2: Clone> Clone for Fallback<S1, S2> {
46+
impl<S1: Clone, S2: Clone, F: Clone> Clone for Fallback<S1, S2, F> {
1947
fn clone(&self) -> Self {
2048
Self {
2149
svc1: self.svc1.clone(),
2250
svc2: self.svc2.clone(),
51+
policy: self.policy.clone(),
2352
}
2453
}
2554
}
2655

27-
impl<S1, S2: Clone> Fallback<S1, S2> {
56+
impl<S1, S2: Clone> Fallback<S1, S2, OnError> {
2857
/// Creates a new `Fallback` wrapping a pair of services.
2958
///
3059
/// Requests are processed on `svc1`, and retried on `svc2` if `svc1` errored.
3160
pub fn new(svc1: S1, svc2: S2) -> Self {
32-
Self { svc1, svc2 }
61+
Self {
62+
svc1,
63+
svc2,
64+
policy: OnError,
65+
}
66+
}
67+
}
68+
69+
impl<S1, S2: Clone, F: Clone> Fallback<S1, S2, F> {
70+
/// Creates a new `Fallback` wrapping a pair of services with a custom fallback policy.
71+
///
72+
/// Requests are processed on `svc1`, and retried on `svc2` if `policy` returns `true`
73+
/// for `svc1`'s result.
74+
pub fn new_with_policy(svc1: S1, svc2: S2, policy: F) -> Self {
75+
Self { svc1, svc2, policy }
3376
}
3477
}
3578

36-
impl<S1, S2, Request> Service<Request> for Fallback<S1, S2>
79+
impl<S1, S2, F, Request> Service<Request> for Fallback<S1, S2, F>
3780
where
3881
S1: Service<Request>,
3982
S2: Service<Request, Response = <S1 as Service<Request>>::Response>,
83+
F: FallbackPolicy<<S1 as Service<Request>>::Response> + Clone,
4084
S1::Error: Into<BoxedError>,
4185
S2::Error: Into<BoxedError>,
4286
S2: Clone,
4387
Request: Clone,
4488
{
4589
type Response = <S1 as Service<Request>>::Response;
4690
type Error = BoxedError;
47-
type Future = ResponseFuture<S1, S2, Request>;
91+
type Future = ResponseFuture<S1, S2, Request, F>;
4892

4993
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
5094
self.svc1.poll_ready(cx).map_err(Into::into)
5195
}
5296

5397
fn call(&mut self, request: Request) -> Self::Future {
5498
let request2 = request.clone();
55-
ResponseFuture::new(self.svc1.call(request), request2, self.svc2.clone())
99+
ResponseFuture::new(
100+
self.svc1.call(request),
101+
request2,
102+
self.svc2.clone(),
103+
self.policy.clone(),
104+
)
56105
}
57106
}

tower-fallback/tests/fallback.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Tests for tower-fallback
22
33
use tower::{service_fn, Service, ServiceExt};
4-
use tower_fallback::Fallback;
4+
use tower_fallback::{BoxedError, Fallback};
55

66
#[tokio::test]
77
async fn fallback() {
@@ -30,3 +30,35 @@ async fn fallback() {
3030
assert_eq!(svc.ready().await.unwrap().call(11).await.unwrap(), 111);
3131
assert!(svc.ready().await.unwrap().call(21).await.is_err());
3232
}
33+
34+
#[tokio::test]
35+
async fn custom_fallback_policy() {
36+
let _init_guard = zebra_test::init();
37+
38+
let svc1 = service_fn(|val: u64| async move {
39+
if val < 10 {
40+
Ok::<_, &'static str>(Some(val))
41+
} else {
42+
Ok(None)
43+
}
44+
});
45+
let svc2 = service_fn(|val: u64| async move {
46+
if val < 20 {
47+
Ok(Some(100 + val))
48+
} else {
49+
Err("too big value on svc2")
50+
}
51+
});
52+
53+
let mut svc =
54+
Fallback::new_with_policy(svc1, svc2, |result: &Result<Option<u64>, BoxedError>| {
55+
matches!(result, Ok(None))
56+
});
57+
58+
assert_eq!(svc.ready().await.unwrap().call(1).await.unwrap(), Some(1));
59+
assert_eq!(
60+
svc.ready().await.unwrap().call(11).await.unwrap(),
61+
Some(111)
62+
);
63+
assert!(svc.ready().await.unwrap().call(21).await.is_err());
64+
}

0 commit comments

Comments
 (0)