Skip to content

Design for the final form of the Service trait #836

@ripytide

Description

@ripytide

I think now that return type notation (RTN), which allows using async functions in public traits, is making decent progress (see rust-lang/rust#138424 and rust-lang/rust#109417) we should wait for that to be ready and then move to the ultimate and final form of the Service trait in one swoop. We should agree on what it would look like here so that we are ready for when the time comes.

There are two main contenders at the moment: Permits vs Tokens. Both can be blanket implemented for a simpler async call(request: Request) -> Result<Response, Error> trait.

use std::{convert::Infallible, marker::PhantomData};

mod concurrency_limit_token;
mod concurrency_limit_permit;

// a generalization of service which allow back-propagation
pub trait TokenService<Request> {
    type Response;
    type Error;
    type Token;
    // separate error types allow more expressiveness
    type ReadyError;
    type UnreadyError;

    // arm the service
    async fn ready(&mut self) -> Result<Self::Token, Self::ReadyError>;

    // disarm the service
    async fn unready(&mut self, token: Self::Token) -> Result<(), (Self::Token, Self::UnreadyError)>;

    // can't call it call() again otherwise you'd have ambiguity with Service::call()
    async fn call_with_token(
        &mut self,
        request: Request,
        token: Self::Token,
    ) -> Result<Self::Response, Self::Error>;
}

pub trait PermitService<Request> {
    type Response;
    type Error;
    type PermitError;

    type Permit<'a>: Permit<Request, Response = Self::Response, Error = Self::Error>
    where
        Self: 'a;

    async fn ready(&self) -> Result<Self::Permit<'_>, Self::PermitError>;
}

pub trait Permit<Request> {
    type Response;
    type Error;

    async fn call(self, request: Request) -> Result<Self::Response, Self::Error>;
}

pub trait SimpleService<Request> {
    type Response;
    type Error;

    async fn call(&self, request: Request) -> Result<Self::Response, Self::Error>;
}

impl<Request, S> PermitService<Request> for S
where
    S: SimpleService<Request>,
{
    type Response = S::Response;
    type Error = S::Error;
    type PermitError = Infallible;

    type Permit<'a>
        = SimplePermit<'a, Self, Self::Response, Self::Error>
    where
        Self: 'a;

    async fn ready<'a>(&'a self) -> Result<Self::Permit<'a>, Self::PermitError> {
        Ok(SimplePermit {
            simple_service: self,
            response: PhantomData,
            error: PhantomData,
        })
    }
}

pub struct SimplePermit<'a, S, Response, Error> {
    simple_service: &'a S,
    response: PhantomData<Response>,
    error: PhantomData<Error>,
}
impl<'a, Request, S, Response, Error> Permit<Request> for SimplePermit<'a, S, Response, Error>
where
    S: SimpleService<Request, Response = Response, Error = Error>,
{
    type Response = Response;
    type Error = Error;

    async fn call(self, request: Request) -> Result<Self::Response, Self::Error> {
        self.simple_service.call(request).await
    }
}

pub struct SimpleToken;

impl<S, Request> TokenService<Request> for S
where
    S: SimpleService<Request>,
{
    type Response = S::Response;
    type Error = S::Error;
    type Token = SimpleToken;
    type ReadyError = Infallible;
    type UnreadyError = Infallible;

    async fn ready(&mut self) -> Result<Self::Token, Self::ReadyError> {
        Ok(SimpleToken)
    }

    async fn unready(&mut self, _token: Self::Token) -> Result<(), (Self::Token, Self::UnreadyError)> {
        Ok(())
    }

    async fn call_with_token(
        &mut self,
        request: Request,
        _token: Self::Token,
    ) -> Result<Self::Response, Self::Error> {
        self.call(request).await
    }
}

mod tests {
    use super::*;

    struct TestSimpleService;

    impl SimpleService<u8> for TestSimpleService {
        type Response = u8;
        type Error = Infallible;

        async fn call(&self, request: u8) -> Result<Self::Response, Self::Error> {
            Ok(request * 2)
        }
    }

    async fn tests() {
        let mut service = TestSimpleService;

        assert_eq!(
            <TestSimpleService as PermitService::<u8>>::ready(&service)
                .await
                .unwrap()
                .call(2)
                .await
                .unwrap(),
            4
        );

        let token = <TestSimpleService as TokenService<u8>>::ready(&mut service)
            .await
            .unwrap();
        assert_eq!(service.call_with_token(2, token).await.unwrap(), 4);
    }
}

Unresolved design questions:

  • Is it worth having a different associated for each error type or is that unnecessary complexity. (Personally, I like it for the added expressiveness)
  • &mut self vs &self in trait methods.
  • Do we need the unready() method for disarming or should we rely on the dropping of TokenService::Token for disarmament?

Related issues:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions