-
Notifications
You must be signed in to change notification settings - Fork 19
Conversation
A middleware is a function from Service to Service. Two combinators are provided: * Service::wrap (equivalent to function application) * Middleware::chain (equivalent to function composition)
Looking good, could you elaborate more on |
I suspect there may be other use cases (in protocols with a different messaging structure from HTTP), but my own use case comes from an MVC-like framework I'm experimenting with. It makes sense for the Index "controller" to return a Stream, rather than a Future, because it returns multiple domain objects. That would be a StreamService. However the view layer which transforms that stream of domain objects into an http::Response needs to ultimately return a future (because that's contract a server implementation like Hyper would expect) which is why the (Depending on the view layer, its possible that the body could still be streaming since an http Response's body can be streaming, but that;s just a method of reducing a stream to a future.) |
Generally, I have been modeling this as a response future that is composed
of a stream.
…On Tue, Mar 28, 2017 at 2:24 PM withoutboats ***@***.***> wrote:
I suspect there may be other use cases (in protocols with a different
messaging structure from HTTP), but my own use case comes from an MVC-like
framework I'm experimenting with. It makes sense for the Index "controller"
to return a Stream, rather than a Future, because it returns multiple
domain objects. That would be a StreamService.
However the view layer which transforms that stream of domain objects into
an http::Response needs to ultimately return a future (because that's
contract a server implementation like Hyper would expect) which is why the
StreamReduce trait is added
(Depending on the view layer, its possible that the body could still be
streaming since an http Response's body can be streaming, but that;s just a
method of reducing a stream to a future.)
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#19 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAAYJMO-csGqWbVLpLytfKqUsy6nRmpVks5rqXqggaJpZM4MrIW2>
.
|
@carllerche To make sure I understand, you mean something like how the Body type of hyper::Response is a Stream? I'd like to be able to let users just write something like: fn index() -> impl Stream<Item = T, Error = Error> And then a In addition to being easier for end applications, by distinguishing |
I don't have much context into why you want your index to return a stream, so I can't really common on whether or not it is a good design. However, I do feel very strongly that I want to keep the core abstraction as minimal as possible. I am very strongly leaning against adding a second core trait at this point and would rather explore ways to keep the current For example, would there be a way to use trait aliasing here? It also could just be that your index fn is not a Service (it doesn't take a request), so whatever calls |
What we'd need are what I've called "associated traits," which are sort of like a Haskell feature called "constraint kinds": trait Service {
type Request;
type Response;
type Error;
trait Outcome<T, E>;
fn call(&self, req: Self::Request) -> impl Self::Outcome<Self::Response, Self::Error>;
}
trait FutureService = Service<Outcome<T, E> = Future<Item = T, Error = E>>;
trait StreamService = Service<Outcome<T, E> = Stream<Item = T, Error = E>>; This isn't in the near future.
What does this mean to you and why do you feel it is very important? |
The index actually takes an But a clearer example is probably fetching a to many relationship, e.g. |
Okay I think I've been convinced that this is largely a better design for streaming services: pub struct Response<H, B> {
pub header: H,
pub body: B,
}
pub trait StreamingService {
type Request;
type Header;
type Member;
type Error;
type Body: Stream<Item = Self::Member, Error = Self::Error>;
type Future: Future<Item = Response<Self::Header, Self::Body>, Error = Self::Error>;
fn call(&self, req: Self::Request) -> Self::Future;
}
impl<S: StreamingService> Service for S {
type Request = S::Request;
type Response = Response<S::Header, S::Body>;
type Error = S::Error;
type Future = S::Future;
fn call(&self, req: Self::Request) -> Self::Future {
StreamingService::call(self, req)
}
} Advantages
DisadvantageThe primary disadvantage is that being agnostic about what the response is can be a footgun. Middleware can mistakenly assume that The most obvious is to define an auto trait like However, with specialization, it should be possible for conscious middleware authors to write their middleware for @carllerche @aturon thoughts? |
I really like the new version of my_service.wrap(| &service, request | {
foo(service.call(bar(request)))
}) Regarding the my_service.wrap(| &service, request | {
service.call(request).and_then(|response| {
// .. do stuff here
})
}) The same goes for |
|
I still do not understand what is gained by special casing a streaming service. Could this be explained further? |
@carllerche Can we talk in terms of concrete trade offs? I don't agree with the framing that this is 'special casing.' People are going to write streaming services - indeed, they already appear in tokio_proto for example, hyper can correctly handle if the service has a streaming body, etc. If we provide no abstraction, all of those streaming services will not share a protocol that a middleware author can use to manipulate them. A service which has a streaming response is significantly different from one which has a fully evaluated response & some middleware will be incorrect if it can't make the distinction. Some middleware will want to manipulate the stream, and needs access to it. This makes it possible to write those middleware correctly and abstractly. Meanwhile, in the most recent proposal they all implement |
I guess, I should clarify that I don't see how the pros of special casing streaming service come close to the cons. So, w/ the Maybe we should start by listing out specific examples of middleware that need to know about the streaming body? |
The only difference in definition is that Hyper's response body is in an |
Also if we just had ATC, there would be no pub trait StreamingService {
type Request;
type Header;
type Member;
type Error;
type Body: Stream<Item = Self::Member, Error = Self::Error>;
type Response<H, B>;
type Future: Future<Item = Self::Response<Self::Header, Self::Body>, Error = Self::Error>;
fn call(&self, req: Self::Request) -> Self::Future;
} EDIT: We can get the same effect by adding a pub trait StreamingService {
type Request;
type Header;
type Member;
type Error;
type Response: StreamingResponse<Self::Header, Self::Member, Self::Error>;
type Future: Future<Item = Self::Response, Error = Self::Error>;
fn call(&self, req: Self::Request) -> Self::Future;
} Definitions: First, IMO ideal, purely structural, with fields in traits: pub trait StreamingResponse<H, M, E> {
type Body: Stream<Item = M, Error = E>;
struct {
header: H,
body: Option<Self::Body>,
}
} On stable, today: pub trait StreamingResponse<H, M, E> {
type Body: Stream<Item = M, Error = E>;
fn new(headers: H, body: Option<Self::Body>) -> Self;
fn members(self) -> (H, Option<Self::Body>);
fn members_ref(&self) -> (&H, Option<&Self::Body>);
fn members_mut(&self) -> (&mut H, Option<&mut Self::Body>);
} EDIT2: For demonstration, implementation signature of StreamingResponse for hyper::Response. impl StreamingResponse<MessageHead<StatusCode>, Chunk, Error> for Response {
type Body = Body;
} |
@withoutboats I am familiar w/ how this exists in rack and the discussion around it. However, I am trying to ask about a list of middleware to better understand the problem. The example listed in the blog post is the only middleware that I can personally think of that needs to be a) generic b) hook into the end of the body stream. Most other middleware that I can think of doesn't care about the end of the body stream or can be implemented to specifically know about streaming bodies (are pretty concrete). Also, if there is a response trait, why would a streaming service trait still be needed? Middleware could be implemented to take an upstream |
In general, I still don't have a good sense of what the exact problem being targeted is vs. a vaguer "streaming middleware is hard" problem. |
This is a good point, its probably enough to provide a trait for a streaming response. |
I wonder if somehow there would be a way to avoid a StreamingResponse type in favor of using conversion traits in std... something like: trait StreamingResponse<Head, Body> = From<(Head, Option<Body>)> + Into<(Head, Option<Body>)>` or whatever the trait alias syntax is... then types that are "streaming" can implement the conversions w/o needing any third party traits. |
We'd also want AsRef and AsMut, but they don't work in general because they return |
Can you elaborate on why They could also be asked for separately (unrelated to the into "parts") |
For example, the timing middleware case doesn't actually need AsRef / AsMut... It would split the response (using I would be interested in a more concrete case that would need both "splitting" + |
My comment was not based on a particular anticipated use case, just what it would mean to define isomorphy to |
I'm kinda coming around to your way of thinking about this in general. I guess a full structural definition could be formed from: trait StreamingResponse<Head, Body> = From<(Head, Option<Body>)> + Into<(Head, Option<Body>)> +
AsRef<Head> + AsRef<Option<Body>> + AsMut<Head> + AsMut<Option<Body>>; |
One unknown right now is exactly how to implement a piece of middleware that doesn't care if the upstream service is streaming or not (i.e. the timer middleware). Using the above pattern, in the streaming case, the timer middleware would split the response, decorate the body to finish the timing logic once the body is done streaming, then take the new stream and recreate the response (using So, the question is then how does the timer middleware handle non streaming responses? Are non-streaming responses still expected to implement Anyway, I think we should try to actually implement the timer middleware to make sure it works for upstream services that have streaming bodies and don't have streaming bodies. Hopefully that makes sense. |
@carllerche My intent was to solve that with specialization. The non-streaming response would be the default: |
Closing in favor of #21 |
Middleware
This PR adds a Middleware trait (different from the Middleware trait proposed in #17). This trait is intended to be the lowest common denominator of different middleware shapes: its literally isomorphic to
FnOnce(Service) -> Service
:This supports the following operations:
wrap
(analogous toapply
).Service -> Middleware -> Service
. This is user supplied on each middleware trait, and Service contains a default method to do it as well.chain
(analogous tocompose
).Middleware -> Middleware -> Middleware
. Combine two middleware to create a new middlware. This is provided through a default method on Middleware.Users therefore have a fairly convenient API of
middleware.chain(middleware).wrap(service)
orservice.wrap(middleware).wrap(middleware)
, depending on what values they have in context at that moment.Streams
This also adds
StreamService
andStreamMiddleware
, analogous toService
andMiddleware
except that they yield streams of responses instead of futures. Literally some trait bounds are the only difference between these traits and their non-stream analogs (note: use case for ConstraintKinds someday?).Stream Reducers
The last piece of this is the
StreamReduce
trait, which is similar to a middleware except that it is a function ofStreamService -> Service
. This way you can deal with a stream at one layer of the stack & collapse it into a future at another layer.Stream reducers can be composed with both StreamMiddleware and Middleware to produce another stream reducer.
Future extensions
Given that this has
Service -> Service
,StreamService -> StreamService
, andStreamService -> Service
, its clearly missing aService -> StreamService
(ServiceUnfold
?). Time will tell if that's actually a useful trait.The middleware definition here is the "LCD," probable we will want to explore different higher level abstractions on top of it for "before / after / around" middleware patterns. But this seems like the baseline that any of those will have to be built on top of.