-
Notifications
You must be signed in to change notification settings - Fork 282
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
Add backpresure capabilities to Service
#6
Conversation
Currently, `Service` does not provide a mechanism by which it can signal to the caller that it is at capacity. This commit adds a `poll_ready` function to the `Service` trait. Callers are able to first check `poll_ready` before calling `Service::call`. `poll_ready` is expected to be a hint and will be implemented in a best effort fashion. It is permitted for a `Service` to return `Ready` from `poll_ready` and the next invocation of `Service::call` fails.
The combination of |
@danburkert well, I would say that services that can't guarantee |
examples/channel_service.rs
Outdated
|
||
fn poll_ready(&mut self) -> Poll<(), Error> { | ||
self.tx.lock().unwrap().poll_ready() | ||
.map_err(|_| Error::AtCapacity) |
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 semantics of poll_ready
are that Async::NotReady
is to mean at-capacity, but the capacity can reduce in the future. So, should this use poll_ready().or(Ok(Async::NotReady)
?
/// This is a **best effort** implementation. False positives are permitted. | ||
/// It is permitted for the service to return `Ready` from a `poll_ready` | ||
/// call and the next invocation of `call` results in an error. | ||
fn poll_ready(&mut self) -> Poll<(), Self::Error>; |
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 Err
variant is to mean that the Service
is in an unrecoverable state, and will never be ready again?
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 know if it means that it is in a permanently unrecoverable state... maybe it does...
It could be used to signal shutdown maybe. I'd say it is a TBD.
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 added a list of unresolved questions to the PR.
examples/channel_service.rs
Outdated
#[derive(Debug)] | ||
pub struct ChannelService { | ||
// Send the request and a oneshot Sender to push the response into. | ||
tx: Mutex<Sender>, |
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.
This seems like a big issue. I just looked into why call
takes &self
and not &mut self
and found tokio-rs/tokio-service#9 (comment). That issue points out that taking &mut self
means chained calls require wrapping the second service in Arc<Mutex<S>>
, but this example has to use a Mutex
, and it's not even chaining. Perhaps we should re-open the &/&mut
discussion, given this example? In general it seems strange to me that Service
takes &self
when everything else in the futures stack takes &mut self
(Future::poll
, Sink::start_send
).
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.
Can you elaborate on what you see as a big issue?
Mutex
here isn't strictly necessary, but it does make it Sync
. You could use RefCell
and it would be !Sync
and honestly try_send
is safe to call w/o any coordination on a single thread but the signature takes &mut self
right now.
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.
Seems to me that it's going to be overwhelmingly common to use Mutex
or RefCell
internally in Service
impls because call
takes &self
(will Service
impls really be stateless? Not the interesting ones). I think it may be better to have call
take &mut self
, and force the caller to do external synchronization, if necessary. In this case, the caller could wrap the ChannelService
in a Mutex
/RefCell
, or just clone it as necessary.
The main argument against &mut self
, as I understand it in the linked comment, is that it makes it more difficult to create services which internally chain calls to other services. But as this example shows, &self
is making it difficult in an even simpler case.
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 think you have a valid point. The use case of chaining multiple calls together is going to be more complicated w/ back pressure handling anyway...
I think it is worth reconsidering &self
/ &mut self
. The answer isn't obviously clear to me right now.
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 main argument against
&mut self
, as I understand it in the linked comment, is that it makes it more difficult to create services which internally chain calls to other services.
If the middleware trait in #2 is merged, I feel like that would make it less important to be able to reasonably write a service that calls other services?
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.
Unfortunately I'm not too familiar with the problems and design space once we hit the service layer, so I may not be too too helpful in review here :(
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.
So far, lgtm. I've updated a project to use the Ready api and everything feels fine so far.
My 2c re: open questions:
poll_ready currently has an Err variant. What should this signify and is it needed at all?
Should the poll_ready error be the same as call?
I think so. It's basically the error that would be returned if call
were to be invoked, if it is known in advance.
Errors should, imo, signify that the Service is no longer usable.
Should there be a default poll_ready impl that always returns ready?
IMO, no. I think it's important for implementations to think about readiness explicitly.
Should Service take &mut self or &self.
idk
Eh, as soon as I wrote that i second guessed myself. By this measure, I think poll_ready's errors should be considered fatal to the service. But I'm less convinced that it should be the same error type as that of |
@olix0r I'm actually kind of leaning towards keeping them the same, that way any futures that take a request, wait until the service is ready, then send it can have a single error type vs. an enum of the |
I also just switched First, you can't just put I landed on cloning the second service into the response future. This means that you can get |
src/lib.rs
Outdated
@@ -236,7 +236,7 @@ where T: Service, | |||
} | |||
} | |||
|
|||
impl<F, R, S> NewService for F | |||
impl<F, R, E, S> NewService for F |
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.
impl<F, R, E, D> NewService for F
All: I'm actually pretty happy with the way this is currently. I would like to try to get this PR merged sooner than later with the expectation that the current state of Tower is still quite experimental and subject to change. |
I've been feeling pretty weary about this PR but I haven't found time to think it through enough to make a substantive comment. The essence of my concern is that this PR makes tower unsuitable for building business logic applications with mid-level performance needs by making their lives significantly more complicated. In other words, this doesn't seem to let you just not care about backpressure. Its possible that this is inevitable - that these users' needs cannot be squared with the needs of absolute high performance services. But the status quo is that Will try to sit down and work through the options in my head to be more constructive, but likely not until next week. |
@withoutboats could you elaborate? You aren't providing much in the way of specifics to actually work through. |
I think this is a good point, I have a couple thoughts: The simple (perhaps cop-out) response is that these application developers should be using a sync wrapper over the async implementation. Backpressure can then can be handled naturally (by blocking), but only if the underlying implementation has the capability to signal it. I do think there's an opportunity to make this easier to use with
I'd be especially interested to hear if you continue to have the same concerns you brought up in tokio-rs/tokio-service#9 (comment), given that some of the particulars have changed. |
A few things.
|
@withoutboats Any follow up thoughts on top of implementing Again, I'd like to merge this to master as it seems to panning out so far. It isn't a commitment for a release though. |
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've had a chance to review the ideas here, and talk with @withoutboats about it some as well. The insights on the filter PR helped a lot in grokking your vision of "backpressure as buffer reservation". This model is feeling really clean to me, and makes me think we should perhaps revisit Sink
along similar lines.
I was initially worried about the move to &mut
, but I believe the approach of requiring Clone
for services that are to be composed will work just fine; in practice, you just move the Arc
needed for composition today "inward", into the service itself.
One passing question: are there ever cases where you want to tell the caller "please slow down this type of request"? Do you intend to do that through an error variant for call
?
Oh, also: should we consider making |
@aturon I considered this, but I am doubtful all services could impl clone. |
@carllerche isn't that a problem for the composition story, though? Non- |
|
@carllerche would love to see a sketch of that! |
@aturon sure, it will come... |
That said, re: |
Services that don't impl |
@withoutboats I mean, you could impl an h1 client that directly wraps a socket & provides IMO that case should have |
@carllerche But that's exactly my concern, such a Service would be unusable in any use case that sequenced asynchronous events (which I believe will ultimately predominate). |
@withoutboats I don't see how it would be unusable? It's very usable in the case of sequential use. You can only have one in-flight request at the time. Either way, I would appreciate it if you could sum up the concerns and open an issue to continue discussion there vs. here? I don't consider the matter settled. |
Currently,
Service
does not provide a mechanism by which it can signal to thecaller that it is at capacity. This commit adds a
poll_ready
function to theService
trait. Callers are able to first checkpoll_ready
before callingService::call
.poll_ready
is expected to be a hint and will be implemented in a best effortfashion. It is permitted for a
Service
to returnReady
frompoll_ready
and the next invocation of
Service::call
fails.I will be adding more docs and examples before merging the PR.
Unresolved questions
poll_ready
currently has anErr
variant. What should this signify and is it needed at all?poll_ready
error be the same ascall
?poll_ready
impl that always returns ready?Service
take&mut self
or&self
.Fixes #3