Closed Kotodian closed 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."
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)>,
}
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 am not super familiar with
Steer
, but the difference between thetower
version and your implementation is a bit subtle (and perhaps I have it wrong!).There are two generic
Reqs
in reality, the one passed intoSteer
and one passed into the serviceS
. Let's call themReq1
andReq2
respectively. With your implementation,Steer
is indeed a passthrough. YourSteer
is implemented for anyReq
that the underlying serviceS
can handle--aka whereReq1
is the same asReq2
. With thetower
Steer
, it has the same property plus an additional one--the genericReq
passed toSteer
can be different than the one passed intoS
--Req1
doesn't have to be the same asReq2
. Sotower
'sSteer
is a bit more flexible...it can be a pure passthrough and/or it can take aReq
that the serviceS
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 liketower
the flexibility is desirable as it puts fewer constraints on calling code.
I understand. thanks for your reply.
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.