Multiple Service Definition Traits
Having multiple service definition traits on a single server/listener doesn't seem to be supported, though there's nothing mentioned about it in the docs. Basically adding a additional trait like this to the lib
#[tarpc::service]
pub trait Ping {
/// Returns a greeting for name.
async fn ping() -> Result<(), String>;
}
and adding it to the server as well like
impl Check for HelloServer {
async fn ping(self, _: context::Context) -> Result<(), String> {
Ok(())
}
}
will cause an error with the serve() code since it doesn't know which trait you want to serve unless you specify. This can cause weird behaviors where having the same function in service definition A and B only calls the function in A and never B, or a function in B failing because it doesn't exist in A, and the server is serving A and not B. And since the function is named .serve() for all the service definitions, it's hard to tell which one if being served by the listener when looking at the code
I don't know if this is actually an issue/bug or not, whether it's gone unnoticed so far, or what. But, I thought I'd bring it up since it doesn't appear to be mentioned in the docs, and took me awhile to figure out this was the cause of my problem since I have my service definitions spread out more than I should it seems
Hi! Can you show the whole code, including the code where .serve() is called?
I used the example-service code from the repo, but it will be easier for me to say the changes than make a repo for it In the lib.rs file, I added
#[tarpc::service]
pub trait World {
/// Returns a greeting for name.
async fn hello(name: String) -> String;
}
#[tarpc::service]
pub trait Check {
/// Returns a greeting for name.
async fn ping() -> Result<(), String>;
}
In the server.rs file I added
#[derive(Clone)]
struct HelloServer(SocketAddr);
impl World for HelloServer {
async fn hello(self, _: context::Context, name: String) -> String {
let sleep_time =
Duration::from_millis(Uniform::new_inclusive(1, 10).sample(&mut thread_rng()));
time::sleep(sleep_time).await;
format!("Hello, {name}! You are connected from {}", self.0)
}
}
impl Check for HelloServer {
async fn ping(self, _: context::Context) -> Result<(), String> {
Ok(())
}
}
I didn't change anything with the serve(). It's still
.map(|channel| {
let server = HelloServer(channel.transport().peer_addr().unwrap());
channel.execute(server.serve()).for_each(spawn)
})
And this throws an error on .serve() since you have to choose which listener serve() to use. If you pick one, then hte error goes away and you use that listener trait
On the client, change the WorldClient to CheckClient, then you can use ping, however you run into an error due to the server using a different trait
This makes sense when you understand how the listener works, however since HelloServer(channel.transport().peer_addr().unwrap()); (imo) looks like it should have access to all its impl serve() functions, you can run into the issue of the server listening to one trait, and the client sending another
Hopefully this is clear enough
Got it, thanks! I guess there are two sides to this:
- improving the documentation to clarify that today only one service can use a transport, and showing some workarounds.
- the feature request to support multiple services using the same transport.
#2 is not planned to be supported, but #1 is definitely doable.
I wanted to second this. We are using tarpc for it's agnostic in-memory vs cross-server compatibility, meaning we get to abstract all services and let the creator decide what services need to be high-performance and which ones are not performance important.
The issue is that we use a handshake api to inform what services are available on each connection, but currently we need custom routers or would need different connections to serve the different services.
Documenting how the recommended approach would be a great start 😄
I'm interested in supporting multiple services on a single port. I think the building blocks provided by tarpc already support such a use case, but there is no "well-lit path" for doing so. I think tarpc should provide the well-lit path.
I sketched out a service registry that can create a service that proxies to other services. Do you think this would meet your requirements? It works like this:
let registry = ServiceRegistry.with_services((
Service {
name: "Hello",
service: HelloServer.serve(),
},
Service {
name: "Goobye",
service: GoodbyeServer.serve(),
},
));
channel.execute(registry.serve()).for_each(spawn)
A couple notes:
- Requests / responses have to be serializable because the service receives
Vec<u8>and returnsVec<u8> - All the services to register have to be known at compile time
hey @tikue ! This sounds like an awesome way of going about it. Having a ServiceRegistry seems like a great idea of service multiple services in the same place.
It reminds be a bit of Effect-Rpc, which I think does an AMAZING job at composing services.