Comments (12)
I updated the main issue with a suggested roadmap to implement this change while validating it along the way:
- Rename
DirectService
->Service
but keep it in thetower-direct-service
crate. - Update middleware to use
tower_direct_service::Service
instead oftower_service::Service
- Validate the API in applications like linkerd and noria.
- Replace
tower_service::Service
withtower_direct_service::Service
and releasetower-service
0.3.
from tower.
The next steps are to make the change across the tower stack in a branch. This is the same process we took for Service<Request>
v0.2. I assigned @hawkw to the issue.
from tower.
cc/ @seanmonstar @jonhoo @olix0r
from tower.
Do we have other examples of services that cannot drive an inner Service
? I'm a little worried that the Clone
fix is overly specialized to router. And in the general case, I don't think we want to have to clone things all over the place.
As a secondary concern, I don't like Clone
having an implied secondary meaning of "don't bother to call poll_service
". That seems super brittle. Maybe we could introduce an empty trait that derives from DirectService + Clone
, but even that seems hard to enforce. I can totally think of DirectService
implementations that are Clone
, but still need to have poll_service
called. Specifically, this would be because they're Clone
for some other reason, like if you want multiple independent copies of a service.
from tower.
To be clear, poll_service and others are still called, but they are called on the cloned handle in the Response future. This is already the strategy used with poll_ready.
If you know cases in which this strategy does not work, let’s discuss them.
from tower.
That implies that you can poll_service
through any of the cloned handles to the service? I don't have any DirectService
where that would work.
My concern is more the opportunity for accidental misuse here. For example, consider tokio_tower::pipeline::Client
. It would be totally reasonable for it to be Clone
if T: Clone
to make it easy to, say, set up multiple mock clients. However, each clone would be an independent Client
. That now implements DirectService + Clone
, but if you were to call
, then Clone
it, and then poll_service
your clone, that poll_service
wouldn't actually poll the instance that is servicing your request. Tying "whether or not you implement Clone
" to "it's safe to clone and poll_service
just the clone" seems super odd to me.
I also think it's unfortunate to expose poll_service
to clients all the time, making them think about whether or not they have to keep calling it. In some sense, the point of Service
now is to expose a service where the polling is taken care of for you, so you just need to worry about call
and whether or not it is ready (with poll_ready
).
from tower.
On a tangential note, I think the current contract of "you must call poll_ready
before call
" should be loosened. If you have a Service
outside a task context, the contract can't be fulfilled. I think it's still fine that a Service::call
return an error if it wasn't ready. Some of our existing Service
s should be changed to DirectService
that currently require calling poll_ready
to drive work forward. Things like Reconnect
, Balance
, etc...
from tower.
I think it's still fine that a Service::call return an error if it wasn't ready
That's probably what should happen. I don't think a panic would be good.
from tower.
DirectService
complexity
The way I'm thinking about it, there are 4 main ways to use a T: Service
.
- Server implementation (the thing that holds a socket and a
T: Service
and dispatches received requests to theT: Service
). - Clients, where a user holds a handle to a
T: Service
and issues requests and waits for the response. - "Leaf" services, which implement the service logic. For example, an "echo" service would just receive the request and respond with the same value. An HTTP "leaf" service would respond however (render a template?).
- Middleware, which are
T: Service
implementations that are implemented in terms of otherT: Service
values.
Within the "client" category, I think there are a few categories of clients.
-
Lower level proxy type things, such as linkerd. Here, I believe that we pretty much always want access to the details of
DirectService
. We are still experimenting with integratingDirectService
, but so far I believe it is going well (cc @olix0r) -
"Applications" (for a lack of a better word), which would be long lived processes that initialize a client up front, then use the same client to respond to received requests. For example, a web app that needs to issue a request to a database. The DB connection should be ready before web requests are received.
-
"Tools" (again, for a lack of a better word), which are short lived processes (think CLI tools) that create the connection, use it, then discard it.
In both the "application" and the "tool" case, I don't think users should have to manage poll_service
directly, but I also don't think that they should use T: Service
directly either.
The way I would hope a hyper client would shape up to be is roughly something like:
struct Client<T = hyper::client::Service> {
service: Buffer<T>,
}
// tower_http::Service is a "trait alias" for an HTTP Service
impl<T: tower_http::Service> Client<T> {
/// Issue an HTTP GET request
pub fn get(&self, path: &str) -> GetFuture {
// ....
}
}
Where hyper::client::Service
is some type that implements DirectService
(promoted to top level Service
). The key here is most users then don't have to worry about calling poll_ready
directly, nor do they have to worry about the Service
trait at all, they would use an ergonomic shim provided by hyper over a T: Service
. This would also allow users to bring their own T: tower_http::Service
and pass it to hyper::Client
. For example, a mock service :)
This pattern would extend beyond hyper, and in general client implementations would provide a shim like this to make the simple cases simple. Then, projects like linkerd2-proxy could use hyper::client::Service
directly and manage all the details of poll_service
and co.
Service + Clone
The idea here is not that a T: Service + Clone
means that you don't have to call poll_service
. It means that you do call poll_service
, but you call it in the response future. So, Clone
means "this is how you get a service handle for one off requests. This also means that you need to call
poll_close`, etc... before completing the response future, but in theory it would be very cheap (most likely a no-op).
Your example with Clone
returning independent clients should still work here because the intent would be to manage the entire Service
lifecycle on a per request basis. So, poll_service
and poll_close
are still called.
from tower.
What you say all makes sense. However, my concern is that we are overloading Clone
to mean "another handle to the same Service
". While that is true for things like Buffer
, it is not true for something like Client<MockTransport>
. If you clone that, you'll get a new Client
that is entirely independent of the old one (including with a dedicated transport). If you sent/received on the original MockTransport
handle you had, it wouldn't become visible to the cloned Client<MockTransport>
(well, depending on how the hypothetical MockTransport
is implemented). It's also not entirely clear how to change Client
to have a Clone
impl that shares a handle to it (and allows concurrent poll_service
calls) except by introducing a Buffer
?
from tower.
Following much discussion, some perhaps more refined thoughts:
Tower assumes that if it clones a Service
, that clone is fine to use in place of the original. That using either Service
instance is fine (as long as you poll
the same one you call
ed on of course). To me, Clone
does not carry that implication — just because you can clone something doesn't mean that the clone is "just as good" as the original. Specifically, it seems reasonable to me to have a MyService
that internally contains a connection of some sort, and which, on Clone
, creates a new MyService
that does not have an established connection. The clone would be a sort of shallow copy of MyService
hat must be "set up" before being used. Such a clone implementation makes it easy to write code that establishes a bunch of services that are all configured the same, but have dedicated connections (for load balancing for example). While it may be unorthodox, I do not believe it to be unreasonable. However, Tower assumes that if a Service
is Clone
, the clone is a full-fledged Service
that can be used instead of the original without any further action. Yet this is not expressed in the type Service + Clone
.
I'm not necessarily arguing for Clone
not being the right trait to use here. While a new trait wrapping Clone
with this added assumption would be possible, I'm also totally open to the services just explicitly documenting exactly what they expect when they clone the Service
.
Another tangentially related point that came up was that Tower assumes that cloning a Service
is cheap. This should definitely be documented somewhere.
from tower.
Closing as this avenue is no longer being considered.
from tower.
Related Issues (20)
- Publish release without pin-project (with pin-project-lite) HOT 5
- `tower::service_fn` docs don't say that you need the `util` flag HOT 3
- MQTT client adapter / framework HOT 1
- “Fan out” services? HOT 3
- Consider using `ControlFlow` for retry `Policy`.
- Extending `Building a middleware from scratch` guide
- breaking change in tower design (0.6 or beyond): first class support for async fn traits HOT 20
- unexpected behaviour of `RateLimit`
- experiment with permit based service framework HOT 5
- Tower & Warp server error
- Rate Limit Layer not Respected HOT 1
- Feature Request: implement `layer` method on `ServiceExt` HOT 2
- Port tower to futures-lite HOT 6
- Integrate GATs into tower-service HOT 1
- Add `BoxCloneSyncService` HOT 2
- Build fails with -Zbuild-std HOT 2
- Improve documentation for tower-test
- [Feature Request]: Middleware for client side which implements balancing the RPS
- How to set tower middleware dynamically
- use workspace dependencies HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from tower.