Skip to content

Multiple Service Definition Traits #499

Open

Description

@MohenjoDaro

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

Activity

  1. tikue commented on Dec 3, 2024

    @tikue
    Collaborator

    Hi! Can you show the whole code, including the code where .serve() is called?

  2. MohenjoDaro commented on Dec 6, 2024

    @MohenjoDaro
    Author

    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

  3. tikue commented on Dec 6, 2024

    @tikue
    Collaborator

    Got it, thanks! I guess there are two sides to this:

    1. improving the documentation to clarify that today only one service can use a transport, and showing some workarounds.
    2. the feature request to support multiple services using the same transport.

    #2 is not planned to be supported, but #1 is definitely doable.

  4. allada commented on May 19, 2025

    @allada

    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 😄

  5. tikue commented on Sep 26, 2025

    @tikue
    Collaborator

    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 returns Vec<u8>
    • All the services to register have to be known at compile time
  6. self-assigned this
    on Sep 26, 2025
  7. added theissue type on Sep 26, 2025
  8. marked How to implement router? #462 as a duplicate of this issue on Sep 26, 2025
  9. Codys-Wright commented on Dec 16, 2025

    @Codys-Wright

    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.

  10. tikue commented on Dec 17, 2025

    @tikue
    Collaborator

    Here's my latest sketch: https://github.com/tikue/tarpc/blob/2ff1a2faa5de78cd99dfe5b34dee3583ac318cdc/tarpc/examples/registry.rs#L637

    The idea is that a channel does an initial handshake / negotiation where is requests a particular service by name. If the service is found, then the channel is upgraded to a channel for the specific service.

    This is more efficient because then there's no proxy sitting between the client and server.

    WDYT?

  11. Codys-Wright commented on Dec 26, 2025

    @Codys-Wright

    I think it's brilliant! I'm not quite advanced enough in Rust to have anything meaningful to add here, but the I'm tracking with the code and learning a lot :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions