tower-rs / tower

async fn(Request) -> Result<Response, Error>
https://docs.rs/tower
MIT License
3.56k stars 281 forks source link

Can I ask one question? #714

Closed Kotodian closed 1 year ago

Kotodian commented 2 years ago

Hi, I'm a rust newer.When I read the steer's source code, I found the PhantomData in Steer, I'm really confused about it. Therefore, I just want to know the case that why we need the PhantomData.

LegNeato commented 1 year ago

Hello! Welcome to Rust 🚀 !

Have you checked out https://doc.rust-lang.org/std/marker/struct.PhantomData.html?

Steer is generic (that is, it doesn't care about what specific type of each is passed in) over three things--the services to steer to (S), the picker function to pick which service to steer to (F), and the request to pass through to the service (Req).

To perform the act of steering, Steer needs to store and use the the services to steer to (S) and the picker function to pick which service to steer to (F). So those are stored in the struct normally. But the logic in Steer doesn't care about what the request (Req) is...it just blindly passes it to the picked service. But in Rust, you can't specify a generic without "using" it, as that may be a sign the programmer made a mistake. But Steer needs both to allow any request and as just mentioned it doesn't need to store or use it. So we are at a stalemate...Steer needs to behave one way and the compiler expects something else. PhantomData is a way to mark the generic request (Req) as stored and used without actually storing or using it. It is essentially telling the compiler, "I know this generic Req data is unused by Steer, this is ok and not a mistake."

Kotodian commented 1 year ago

I'm really appreciate for your reply.I understand that the PhantomData's in Steer to make sure when implementing Service's Req is equal to the Steer's Service's Req and Picker's Req, is that true? But When I remove the PhantomData, it looks like nothing different.

use std::{
    collections::VecDeque,
    fmt,
    marker::PhantomData,
    task::{Context, Poll},
};

use tower_service::Service;

pub trait Picker<S, Req> {
    fn pick(&mut self, r: &Req, services: &[S]) -> usize;
}

impl<S, F, Req> Picker<S, Req> for F
where
    F: Fn(&Req, &[S]) -> usize,
{
    fn pick(&mut self, r: &Req, services: &[S]) -> usize {
        self(r, services)
    }
}

pub struct Steer<S, F> {
    router: F,
    services: Vec<S>,
    not_ready: VecDeque<usize>,
    // _phantom: PhantomData<Req>,
}

impl<S, F> Steer<S, F> {
    pub fn new(services: impl IntoIterator<Item = S>, router: F) -> Self {
        let services: Vec<_> = services.into_iter().collect();
        let not_ready: VecDeque<_> = services.iter().enumerate().map(|(i, _)| i).collect();
        Self {
            router,
            services,
            not_ready,
            // _phantom: PhantomData,
        }
    }
}

impl<S, Req, F> Service<Req> for Steer<S, F>
where
    S: Service<Req>,
    F: Picker<S, Req>,
{
    type Response = S::Response;

    type Error = S::Error;

    type Future = S::Future;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        loop {
            if self.not_ready.is_empty() {
                return Poll::Ready(Ok(()));
            } else {
                if self.services[self.not_ready[0]]
                    .poll_ready(cx)?
                    .is_pending()
                {
                    return Poll::Pending;
                }

                self.not_ready.pop_front();
            }
        }
    }

    fn call(&mut self, req: Req) -> Self::Future {
        assert!(
            self.not_ready.is_empty(),
            "Steer must wait for all services to be ready. Did you forget to call poll_ready()?"
        );

        let idx = self.router.pick(&req, &self.services[..]);
        let cl = &mut self.services[idx];
        self.not_ready.push_back(idx);
        cl.call(req)
    }
}

impl<S, F> Clone for Steer<S, F>
where
    S: Clone,
    F: Clone,
{
    fn clone(&self) -> Self {
        Self {
            router: self.router.clone(),
            services: self.services.clone(),
            not_ready: self.not_ready.clone(),
            // _phantom: PhantomData,
        }
    }
}

impl<S, F> fmt::Debug for Steer<S, F>
where
    S: fmt::Debug,
    F: fmt::Debug,
{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let Self {
            router,
            services,
            not_ready,
            // _phantom: PhantomData,
        } = self;
        f.debug_struct("Steer")
            .field("router", router)
            .field("services", services)
            .field("not_ready", not_ready)
            .finish()
    }
}

mod test {
    use std::task::Poll;

    use futures_util::future::{ready, Ready};
    use tower_service::Service;

    use crate::steer::Steer;

    type StdError = Box<dyn std::error::Error + Send + Sync + 'static>;
    #[tokio::test(flavor = "current_thread")]
    async fn test_new_steer() {
        struct MyService(u8, bool);
        impl Service<String> for MyService {
            type Response = u8;
            type Error = StdError;
            type Future = Ready<Result<u8, Self::Error>>;

            fn poll_ready(
                &mut self,
                cx: &mut std::task::Context<'_>,
            ) -> std::task::Poll<Result<(), Self::Error>> {
                if !self.1 {
                    Poll::Pending
                } else {
                    Poll::Ready(Ok(()))
                }
            }

            fn call(&mut self, req: String) -> Self::Future {
                ready(Ok(self.0))
            }
        }

        let srvs = vec![MyService(42, true), MyService(57, true)];

        let mut st = Steer::new(srvs, |_: &String, _: &[_]| 1);
        futures_util::future::poll_fn(|cx| st.poll_ready(cx))
            .await
            .unwrap();
        let r = st.call(String::from("foo")).await.unwrap();
        assert_eq!(r, 57);
    }
}

And I noticed that the Req in MakeBalance's PhantomData is the fn(Req), What's difference between the fn(Req) and Req.

pub struct MakeBalance<S, Req> {
    inner: S,
    _marker: PhantomData<fn(Req)>,
}
LegNeato commented 1 year ago

I am not super familiar with Steer, but the difference between the tower version and your implementation is a bit subtle (and perhaps I have it wrong!).

There are two generic Reqs in reality, the one passed into Steer and one passed into the service S. Let's call them Req1 and Req2 respectively. With your implementation, Steer is indeed a passthrough. Your Steer is implemented for any Req that the underlying service S can handle--aka where Req1 is the same as Req2. With the tower Steer, it has the same property plus an additional one--the generic Req passed to Steer can be different than the one passed into S--Req1 doesn't have to be the same as Req2. So tower's Steer is a bit more flexible...it can be a pure passthrough and/or it can take a Req that the service S can't handle and transform it into something the service can handle.

While for Steer it doesn't really matter in practice due to the current implementation, for a library like tower the flexibility is desirable as it puts fewer constraints on calling code.

Kotodian commented 1 year ago

I am not super familiar with Steer, but the difference between the tower version and your implementation is a bit subtle (and perhaps I have it wrong!).

There are two generic Reqs in reality, the one passed into Steer and one passed into the service S. Let's call them Req1 and Req2 respectively. With your implementation, Steer is indeed a passthrough. Your Steer is implemented for any Req that the underlying service S can handle--aka where Req1 is the same as Req2. With the tower Steer, it has the same property plus an additional one--the generic Req passed to Steer can be different than the one passed into S--Req1 doesn't have to be the same as Req2. So tower's Steer is a bit more flexible...it can be a pure passthrough and/or it can take a Req that the service S can't handle and transform it into something the service can handle.

While for Steer it doesn't really matter in practice due to the current implementation, for a library like tower the flexibility is desirable as it puts fewer constraints on calling code.

I understand. thanks for your reply.