loadpace-tower 0.1.0-rc.4

Tower adapter for adaptive client-side load balancing and backpressure
Documentation
use futures_util::StreamExt;
use futures_util::stream;
use loadpace::EndpointConfig;
use loadpace_tower::AdaptiveDiscovery;
use std::future::Future;
use std::marker::PhantomPinned;
use std::pin::Pin;
use std::task::{Context, Poll};
use tower::discover::Change;
use tower::{BoxError, Service, ServiceExt};

pin_project_lite::pin_project! {
    struct NotUnpinStream<S> {
        #[pin]
        inner: S,
        _pin: PhantomPinned,
    }
}

impl<S> NotUnpinStream<S> {
    fn new(inner: S) -> Self {
        Self {
            inner,
            _pin: PhantomPinned,
        }
    }
}

impl<S: futures_core::Stream> futures_core::Stream for NotUnpinStream<S> {
    type Item = S::Item;

    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
        self.project().inner.poll_next(cx)
    }
}

#[derive(Clone, Default)]
struct BoxedEcho;

struct NotAService;

impl Service<u64> for BoxedEcho {
    type Response = u64;
    type Error = BoxError;
    type Future = Pin<Box<dyn Future<Output = Result<u64, BoxError>> + Send>>;

    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        Poll::Ready(Ok(()))
    }

    fn call(&mut self, request: u64) -> Self::Future {
        Box::pin(async move { Ok(request) })
    }
}

#[tokio::test(start_paused = true)]
async fn discovery_wraps_inserts_and_preserves_removes() {
    let changes = stream::iter([
        Ok::<_, BoxError>(Change::Insert(7_u64, BoxedEcho)),
        Ok::<_, BoxError>(Change::Remove(7_u64)),
    ]);
    let mut discovery = AdaptiveDiscovery::new(changes, EndpointConfig::default());

    let insert = discovery.next().await.unwrap().unwrap();
    let endpoint = match insert {
        Change::Insert(key, endpoint) => {
            assert_eq!(key, 7);
            endpoint
        }
        Change::Remove(_) => panic!("expected an insert"),
    };
    assert_eq!(endpoint.oneshot(11).await.unwrap(), 11);

    assert!(matches!(
        discovery.next().await.unwrap().unwrap(),
        Change::Remove(7)
    ));
}

#[tokio::test(start_paused = true)]
async fn discovery_can_feed_towers_p2c_balance() {
    let changes = stream::iter([
        Ok::<_, BoxError>(Change::Insert(1_u64, BoxedEcho)),
        Ok::<_, BoxError>(Change::Insert(2_u64, BoxedEcho)),
    ]);
    let discovery = AdaptiveDiscovery::new(changes, EndpointConfig::default());
    let balance = tower::balance::p2c::Balance::new(discovery);

    assert_eq!(balance.oneshot(99).await.unwrap(), 99);
}

#[tokio::test(start_paused = true)]
async fn discovery_accepts_a_non_unpin_stream() {
    let changes = NotUnpinStream::new(stream::iter([
        Ok::<_, BoxError>(Change::Insert(7_u64, BoxedEcho)),
        Ok::<_, BoxError>(Change::Remove(7_u64)),
    ]));
    let discovery = AdaptiveDiscovery::new(changes, EndpointConfig::default());
    futures_util::pin_mut!(discovery);

    let insert = discovery.next().await.unwrap().unwrap();
    assert!(matches!(insert, Change::Insert(7, _)));
    assert!(matches!(
        discovery.next().await.unwrap().unwrap(),
        Change::Remove(7)
    ));
}

#[tokio::test]
async fn discovery_mapping_does_not_require_service_or_key_bounds() {
    let changes = stream::iter([Ok::<_, BoxError>(Change::Insert(1.5_f64, NotAService))]);
    let mut discovery = AdaptiveDiscovery::new(changes, EndpointConfig::default());

    assert!(matches!(
        discovery.next().await.unwrap().unwrap(),
        Change::Insert(1.5, _)
    ));
}