Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

how to implement A vec of layers, something like this: Vec<Box<dyn Layer>>, configurable layers #496

Closed
kingrongH opened this issue Dec 28, 2020 · 10 comments

Comments

@kingrongH
Copy link

kingrongH commented Dec 28, 2020

I have been running into this situation recently. I want a store a Vec<Box<dyn Layer>> stuff in a struct, and then I can chain them all together while using this struct . This would be something like this:

struct MyStruct {
     layers: Vec<Box<dyn Layer>>
}

but since Layer is a trait with generic argument and associated type, it's difficult for me to implement Vec<Box<dyn Layer>> with layers stored, I can build a service with these layers and dynamically add/remove the layer.

Help needed. tks first

Edit:
In my situation, I want to dynamically add/remove the layers in MyStruct(even in the runtime!) according to some kind config.

@olix0r
Copy link
Collaborator

olix0r commented Dec 28, 2020

Do you actually need a totally dynamic Vec of layers or might the Stack type be suitable?

E.g.

use tower::layer::{Identity, Stack, Layer};

struct MyStruct<L> {
    layers: L,
}

impl Default for MyStruct<Identity> { ... }

impl<L> MyStuct<L> {
    pub fn push<M>(self, outer: M) -> MyStruct<Stack<L, M>> {
        MyStruct {
            layers: Stack::new(self.layers, outer),
        }
    }

    pub fn layer(&self) -> &L {
        &self.layers
    }
}

If you really do need this to be dynamic, you probably want to look at implementing a BoxLayer helper akin to BoxService

@kingrongH
Copy link
Author

Thanks for the answer~

I have tried Stack, but using it as the type of layers, I must have a generic arg for MyStruct. And the push method will consume the original MyStruct, that's is not what i want.

Then I follow the implementation of BoxService I have something like this

    use tower::{
        Service,
        layer::Layer,
        timeout::TimeoutLayer,
        limit::RateLimitLayer
    };

    use std::{ pin::Pin, future::Future, time::Duration };

    type BoxFuture<T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send>>;
    type BoxService<T, U, E> = Box<dyn Service<T, Response = U, Error = E, Future = BoxFuture<U, E>> + Send>;
    
    type StdError =
        std::boxed::Box<(dyn std::error::Error + std::marker::Send + std::marker::Sync + 'static)>;
    
    type BoxLayer<S, T, U, E> = Box<dyn Layer<S, Service = BoxService<T, U, E>>>;
    
    struct Layers<S, T, U, E> {
        layers: Vec<BoxLayer<S, T, U, E>>
    }

    impl<S, T, U, E> Layers<S, T, U, E> {
        
        fn new() -> Self {
            Self {
                layers: Vec::new()
            }
        }

        fn push<L: 'static>(&mut self, layer: L) 
        where L: Layer<S, Service = BoxService<T, U, E>>
        {
            self.layers.push(Box::new(layer));
        }

    }
    
    fn test() {
        
        let timeout_layer = TimeoutLayer::new(Duration::from_secs(5));
        let rate_limit_layer = RateLimitLayer::new(1000, Duration::from_secs(1));
        
        let layers = Layers::new();
        layers.push(timeout_layer);

    }

And this won't compile 🤦‍♂️

image

Maybe it's not a proper pattern/usage(Vec of Layer) of tower-layer ?

@davidpdrsn
Copy link
Member

@kingrongH What causes you to want a vec of layers? Are you able to use a ServiceBuilder?

let timeout_layer = TimeoutLayer::new(Duration::from_secs(5));
let rate_limit_layer = RateLimitLayer::new(1000, Duration::from_secs(1));

let builder = tower::ServiceBuilder::new()
    .layer(timeout_layer)
    .layer(rate_limit_layer);

let some_service =
    tower::service_fn(|req: String| async move { Ok::<_, Infallible>(req.to_uppercase()) });
let wrapped_service = builder.service(some_service);

I think the type gymnastics required to do that might not be worth the hassle if even possible.

@kingrongH
Copy link
Author

kingrongH commented Dec 29, 2020

Yes it's not easy to do so. But somehow in my situation, I still want to dynamically add/remove the layers in MyStruct(even in the runtime!) according to some kind config. And also, with Vec<dyn Layer> been implemented in MyStruct, add or remove a predifined layer would be much easier.

ServiceBuilder is very nice, I can maybe impl a configurable layers with code:

let builder = if config_timeout {
    tower::ServiceBuilder::new().layer(timeout_layer)
} else if config_rate_limit {
    tower::ServiceBuilder::new().layer(rate_limit_layer)
} else if config_timeout && config_rate_limit {
    tower::ServiceBuilder::new().layer(timeout_layer).layer(rate_limit_layer)
} else {
    tower::ServiceBuilder::new()
};

but actually this code won't compile, cuz ServiceBuilder returned by different branch has different type 😹

And I'm fascinated by the impl of configurable layers.

@kingrongH
Copy link
Author

kingrongH commented Dec 29, 2020

I think I understand a little now. I have to create a new Boxed struct which impl the Layer trait, and in the impl, constrain the associated type Service to Box<dyn Service<T, Response = U, Error = E, Future = BoxFuture<U, E>> + Send> . I'll try later . 😃

@kingrongH kingrongH changed the title how to implement A vec of layers .something like this: Vec<Box<dyn Layer>> how to implement A vec of layers, something like this: Vec<Box<dyn Layer>>, configurable layers Dec 30, 2020
@kingrongH
Copy link
Author

After some fight with the compiler, I think maybe we just can't have this kind of impl, because the request and response of Service need to be generic(for collecting them) and the type of request and response can even change while combining different services.

I think implementing configurable layers by using Vec<Box<dyn Service>> and Vec<Box<dyn Layer>> could be just bad pattern(may be not impossible pattern).

@kingrongH
Copy link
Author

Finally, I have achieve some thing that partly solve my problem.

The idea is OptionLayer, that used to map Option<X> config to a real layer. if my config is Option::None, then It will do nothing like Identity. The downside of this way would be that we'll have a real long return type Stack<OptionLayer<TimeoutLayer>, OptionLayer<ConcurrencyLimitLayer>>, once we add more layers, it'll turns even longer.

enum OptionLayer<L> {
    Layer(L),
    NoneLayer,
}

impl<L> OptionLayer<L> {
    
    pub fn from_option(option: Option<L>) -> OptionLayer<L>
    {
        option.map_or_else(|| OptionLayer::NoneLayer, |v| OptionLayer::Layer(v))
    } 
    
}

impl<S, L: Layer<S>> Layer<S> for OptionLayer<L> {

    type Service = Either<L::Service, S>;
    
    fn layer(&self, inner: S) -> Self::Service {
        match self {
            OptionLayer::Layer(l) => Either::A(l.layer(inner)),
            OptionLayer::NoneLayer => Either::B(inner)
        }
    }
    
}

struct MyConfig {
    timeout: Option<Duration>,
    concurrency_limit: Option<usize>,
}

impl MyConfig {
    
    pub fn layer(&self) -> Stack<OptionLayer<TimeoutLayer>, OptionLayer<ConcurrencyLimitLayer>> {
        let timeout_layer = OptionLayer::from_option(self.timeout.map(TimeoutLayer::new));
        let concurrency_limit_layer = OptionLayer::from_option(self.concurrency_limit.map(ConcurrencyLimitLayer::new));
        
        Stack::new(timeout_layer, concurrency_limit_layer)
    }
    
}

good:

  1. it is configurable now(for me)

bad:

  1. The return type will be even longer
  2. It is still not a that flexible way(like Vec<dyn Layer>)

@davidpdrsn
Copy link
Member

davidpdrsn commented Jan 3, 2021

You could also consider dispatching to one of two services based on some config/env var at runtime. That way the types don't change. Something like

use futures::future::Either;
use std::task::{Context, Poll};
use tower::layer::Layer;
use tower::Service;

pub struct EnvDispatchLayer<L> {
    var: String,
    toggle_layer: L,
}

impl<L> EnvDispatchLayer<L> {
    pub fn new(var: impl Into<String>, toggle_layer: L) -> Self {
        Self {
            var: var.into(),
            toggle_layer,
        }
    }
}

impl<L, S> Layer<S> for EnvDispatchLayer<L>
where
    L: Layer<S>,
    S: Clone,
{
    type Service = EnvDispatch<L::Service, S>;

    fn layer(&self, inner: S) -> Self::Service {
        EnvDispatch {
            layered: self.toggle_layer.layer(inner.clone()),
            fallback: inner,
            var: self.var.clone(),
            env_value: None,
        }
    }
}

#[derive(Clone, Debug)]
pub struct EnvDispatch<Layered, Fallback> {
    layered: Layered,
    fallback: Fallback,
    var: String,
    env_value: Option<String>,
}

impl<T, K> EnvDispatch<T, K> {
    fn update_value(&mut self) {
        self.env_value = std::env::var(&self.var).ok();
    }

    fn enabled(&self) -> bool {
        self.env_value
            .as_ref()
            .map(|value| value == "1")
            .unwrap_or(false)
    }
}

impl<R, Layered, Fallback> Service<R> for EnvDispatch<Layered, Fallback>
where
    Layered: Service<R>,
    Fallback: Service<R, Response = Layered::Response, Error = Layered::Error>,
{
    type Response = Layered::Response;
    type Error = Layered::Error;
    type Future = Either<Layered::Future, Fallback::Future>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        // we don't want the env var to change between `poll_ready` and `call` so we cache it
        // between calls
        self.update_value();

        if self.enabled() {
            self.layered.poll_ready(cx)
        } else {
            self.fallback.poll_ready(cx)
        }
    }

    fn call(&mut self, req: R) -> Self::Future {
        if self.enabled() {
            Either::Left(self.layered.call(req))
        } else {
            Either::Right(self.fallback.call(req))
        }
    }
}

#[allow(warnings)]
fn foo() {
    use std::convert::Infallible;
    use std::time::Duration;
    use tower::builder::ServiceBuilder;
    use tower::limit::concurrency::ConcurrencyLimitLayer;
    use tower::timeout::TimeoutLayer;

    let base_svc = tower::service_fn(|req: String| async move { Ok::<_, Infallible>(req) });

    let final_svc = ServiceBuilder::new()
        // call service wrapped in concurrency limit layer if ENABLE_RATE_LIMIT == 1
        // otherwise call the base service
        .layer(EnvDispatchLayer::new(
            "ENABLE_RATE_LIMIT",
            ConcurrencyLimitLayer::new(10),
        ))
        // call service wrapped in timeout layer if ENABLE_TIMEOUT == 1
        // otherwise call the base service
        .layer(EnvDispatchLayer::new(
            "ENABLE_TIMEOUT",
            TimeoutLayer::new(Duration::from_secs(30)),
        ))
        .service(base_svc);
}

@kingrongH
Copy link
Author

This Dispatch really meets my need. Thanks very much~ I think I can close this issue now.

And I'd like to discuss more.

From your code:

impl<R, Layered, Fallback> Service<R> for EnvDispatch<Layered, Fallback>
where
    Layered: Service<R>,
    Fallback: Service<R, Response = Layered::Response, Error = Layered::Error>

The Fallback is constrained by the Service<R, Response = Layered::Response, Error = Layered::Error> that has the same type of Response and Error as Layerd.

I am just thinking about that would MapResponseLayer work in this situation(or can be made to work).

I'd like to have a try later! Thanks again.

@kingrongH
Copy link
Author

kingrongH commented Jan 6, 2021

YES, tried, MapResponseLayer doesn't work in this case. That means the following code will result in compile error.

let dispatched_map_response = EnvDispatchLayer::new("MAP_RES", MapResponseLayer::new(base_svc, |_: String| 42));
let result_svc = dispatched_map_response.layer(base_svc);
result_svc.call("string".into());

And also, checked, Service impl for tower::util::Either has the same limitation. So would it be possible to make a Either that actually can be either MapResponse or inner service? (Maybe file another issue?)

My problem here is sovled. Gonna close this one. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants