-
Notifications
You must be signed in to change notification settings - Fork 283
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
rewrite tower::filter
#508
Conversation
Signed-off-by: Eliza Weisman <eliza@buoyant.io>
Signed-off-by: Eliza Weisman <eliza@buoyant.io>
Signed-off-by: Eliza Weisman <eliza@buoyant.io>
macro_rules! opaque_future { | ||
($(#[$m:meta])* pub type $name:ident<$($param:ident),+> = $actual:ty;) => { | ||
#[pin_project::pin_project] | ||
$(#[$m])* | ||
pub struct $name<$($param),+>(#[pin] pub(crate) $actual); | ||
|
||
impl<$($param),+> std::fmt::Debug for $name<$($param),+> { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
f.debug_tuple(stringify!($name)).field(&format_args!("...")).finish() | ||
} | ||
} | ||
|
||
impl<$($param),+> std::future::Future for $name<$($param),+> | ||
where | ||
$actual: std::future::Future, | ||
{ | ||
type Output = <$actual as std::future::Future>::Output; | ||
#[inline] | ||
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output> { | ||
self.project().0.poll(cx) | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea behind this thing is that we probably don't want to expose type aliases for futures_util
futures in public APIs, for stability reasons. Since type aliases don't hide the aliased type, users could refer to these as the nested chain of futures_util
combinator types. Thus, if we change the specific combinator futures, that's potentially a breaking change.
I'll wrap other exposed combinator futures in opaque_future!
when this merges.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
@@ -239,6 +239,42 @@ impl<L> ServiceBuilder<L> { | |||
self.layer(crate::timeout::TimeoutLayer::new(timeout)) | |||
} | |||
|
|||
/// Conditionally reject requests based on `predicate`. | |||
/// | |||
/// `predicate` must implement the [`Predicate`] trait. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// `predicate` must implement the [`Predicate`] trait. | |
/// - `predicate` must implement the [`Predicate`] trait. |
minor nit: feel like this reads a bit better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i don't think it makes sense to use a bulleted list with only one entry?
|
||
/// Conditionally reject requests based on an asynchronous `predicate`. | ||
/// | ||
/// `predicate` must implement the [`AsyncPredicate`] trait. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// `predicate` must implement the [`AsyncPredicate`] trait. | |
/// - `predicate` must implement the [`AsyncPredicate`] trait. |
Signed-off-by: Eliza Weisman <eliza@buoyant.io>
Signed-off-by: Eliza Weisman <eliza@buoyant.io>
For stability reasons, we probably don't want to expose future combinator types from the `futures_util` crate in public APIs. If we were to change which combinators are used to implement these futures, or if `futures_util` made breaking changes, this could cause API breakage. This branch wraps all publicly exposed `futures_util` types in the `opaque_future!` macro added in #508, to wrap them in newtypes that hide their internal details. This way, we can change these futures' internals whenever we like.
* make all combinator futures opaque For stability reasons, we probably don't want to expose future combinator types from the `futures_util` crate in public APIs. If we were to change which combinators are used to implement these futures, or if `futures_util` made breaking changes, this could cause API breakage. This branch wraps all publicly exposed `futures_util` types in the `opaque_future!` macro added in #508, to wrap them in newtypes that hide their internal details. This way, we can change these futures' internals whenever we like. Signed-off-by: Eliza Weisman <eliza@buoyant.io>
Tower provides a `tower::filter` utility for conditionally rejecting some requests and accepting others. Rather than using `tower`'s filter, we have a similar `linkerd_stack::request_filter`, due to two main issues with the filtering API in tower 0.3 and earlier: * `tower::filter`'s predicates returned futures, which we don't need --- all our filtering predicates are purely sunchronous * `tower::filter` didn't allow filters to return any error of their choosing --- they had to return a specific filter-rejected error. This doesn't work with other code where we downcast error types, like switch/fallback. However, Tower 0.4 includes a rewritten `Filter` API that no longer has these issues (tower-rs/tower#508). Therefore, we can switch to using the upstream implementation. This branch replaces `RequestFilter` with `tower::filter::Filter`. It was necessary to add an impl of `NewService` for `Filter`. This required an upstream change to expose access to the wrapped service, so that its `new_service` method can be called when it's a `NewService`, and to expose access to the `Predicate`. Therefore, this PR depends on tower-rs/tower#521 and tower-rs/tower#522, and introduces a Git dependency. The git dep can be removed when Tower 0.4.3 is released. Signed-off-by: Eliza Weisman <eliza@buoyant.io>
Motivation
It was pointed out that there is currently some overlap between the
try_with
Service
combinator andtower::filter
middleware (see #499 (comment) ).try_with
synchronously maps from aRequest
->Result<DifferentRequest, Error>
, whiletower::filter
asynchronously maps from a
&Request
to aResult<(), Error>
. Thekey differences are: -
try_with
takes a request by value, and allowsthe predicate to return a different request value -
try_with
alsopermits changing the type of the request -
try_with
is synchronous,while
tower::filter
is asynchronous -tower::filter
has aPredicate
trait, which can be implemented by more than just functions.For example, a struct with a
HashSet
could implementPredicate
byfailing requests that match the values in the hashset.
It definitely seems like there's demand for both synchronous and
asynchronous request filtering. However, the APIs we have currently
differ pretty significantly. It would be nice to make them more
consistent with each other.
As an aside,
tower::filter
does not seem all that widely used.Meanwhile,
linkerd2-proxy
defines its ownRequestFilter
middleware,using a predicate trait that's essentially in between
tower::filter
andServiceExt::try_with
: - it's synchronous, liketry_with
- it allowsmodifying the type of the request, like
try_with
- it uses a trait forpredicates, rather than a
Fn
, liketower::filter
- it uses a similarnaming scheme to
tower::filter
("filtering" rather than "with"/"map").Solution
This branch rewrites
tower::filter
to make the following changes:AsyncFilter
typeand an
AsyncPredicate
trait are available for predicates returningfutures.
Request
type, allowingFilter
andAsyncFilter
to subsumetry_map_request
.into
BoxError
s.Closes #502