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

rewrite tower::filter #508

Merged
merged 7 commits into from
Jan 6, 2021
Merged

rewrite tower::filter #508

merged 7 commits into from
Jan 6, 2021

Conversation

hawkw
Copy link
Member

@hawkw hawkw commented Jan 6, 2021

Motivation

It was pointed out that there is currently some overlap between the
try_with Service combinator and tower::filter middleware (see #499 (comment) ).
try_with synchronously maps from a Request ->
Result<DifferentRequest, Error>, while tower::filter
asynchronously maps from a &Request to a Result<(), Error>. The
key differences are: - try_with takes a request by value, and allows
the predicate to return a different request value - try_with also
permits changing the type of the request - try_with is synchronous,
while tower::filter is asynchronous - tower::filter has a
Predicate trait, which can be implemented by more than just functions.
For example, a struct with a HashSet could implement Predicate by
failing 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 own RequestFilter middleware,
using a predicate trait that's essentially in between tower::filter and
ServiceExt::try_with: - it's synchronous, like try_with - it allows
modifying the type of the request, like try_with - it uses a trait for
predicates, rather than a Fn, like tower::filter - it uses a similar
naming scheme to tower::filter ("filtering" rather than "with"/"map").

Solution

This branch rewrites tower::filter to make the following changes:

  • Predicates are synchronous by default. A separate AsyncFilter type
    and an AsyncPredicate trait are available for predicates returning
    futures.
  • Predicates may now return a new Request type, allowing Filter and
    AsyncFilter to subsume try_map_request.
  • Predicates may now return any error type, and errors are now converted
    into BoxErrors.

Closes #502

Signed-off-by: Eliza Weisman <eliza@buoyant.io>
Signed-off-by: Eliza Weisman <eliza@buoyant.io>
Signed-off-by: Eliza Weisman <eliza@buoyant.io>
Signed-off-by: Eliza Weisman <eliza@buoyant.io>
Comment on lines +1 to +24
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)
}
}
}
}
Copy link
Member Author

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.

tower/Cargo.toml Outdated Show resolved Hide resolved
Copy link
Member

@LucioFranco LucioFranco left a 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.
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
/// `predicate` must implement the [`Predicate`] trait.
/// - `predicate` must implement the [`Predicate`] trait.

minor nit: feel like this reads a bit better?

Copy link
Member Author

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.
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
/// `predicate` must implement the [`AsyncPredicate`] trait.
/// - `predicate` must implement the [`AsyncPredicate`] trait.

tower/src/filter/mod.rs Outdated Show resolved Hide resolved
Signed-off-by: Eliza Weisman <eliza@buoyant.io>
Signed-off-by: Eliza Weisman <eliza@buoyant.io>
@hawkw hawkw merged commit 2a7d47a into master Jan 6, 2021
@hawkw hawkw deleted the eliza/new-filter branch January 6, 2021 20:18
hawkw added a commit that referenced this pull request Jan 6, 2021
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.
hawkw added a commit that referenced this pull request Jan 6, 2021
* 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>
olix0r pushed a commit to linkerd/linkerd2-proxy that referenced this pull request Jan 14, 2021
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

decide the future of tower::filter
3 participants