use std::{
fmt::Debug,
future::Future,
pin::Pin,
sync::{
atomic::{AtomicUsize, Ordering},
Arc, Mutex,
},
task::{Context, Poll},
time::Duration,
};
use futures::{stream::FuturesUnordered, StreamExt};
use tokio::task::JoinHandle;
use tokio_test::{assert_pending, assert_ready, assert_ready_err, assert_ready_ok, task};
use tower::{layer::Layer, Service, ServiceExt};
use tower_test::{
assert_request_eq,
mock::{self, Mock},
};
use tower_batch::{error, Batch, BatchControl, BatchLayer, BoxError};
mod support;
#[derive(Clone)]
struct Aggregator<T> {
items: Arc<Mutex<Vec<Vec<T>>>>,
current: Arc<AtomicUsize>,
}
impl<T> Aggregator<T> {
pub fn new() -> Self {
Self {
items: Arc::new(Mutex::new(Vec::new())),
current: Arc::new(AtomicUsize::new(0)),
}
}
fn batch_has_size(&self, index: usize, size: usize) -> bool {
if index == self.current.load(Ordering::Acquire) {
return false;
}
let items = &self.items.lock().unwrap();
items.get(index).is_some_and(|v| v.len() == size)
}
fn batch_items(&self, index: usize) -> Option<Vec<T>>
where
T: Clone,
{
if index == self.current.load(Ordering::Acquire) {
return None;
}
let items = self.items.lock().unwrap();
items.get(index).cloned()
}
}
impl<T> Service<BatchControl<T>> for Aggregator<T>
where
T: Debug,
{
type Response = ();
type Error = BoxError;
type Future = Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send + Sync + 'static>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: BatchControl<T>) -> Self::Future {
match req {
BatchControl::Item(item) => {
let mut items = self.items.lock().unwrap();
match items.get_mut(self.current.load(Ordering::Acquire)) {
None => {
items.push(vec![item]);
}
Some(v) => {
v.push(item);
}
}
}
BatchControl::Flush => {
self.current.fetch_add(1, Ordering::SeqCst);
return Box::pin(async {
tracing::info!("sleeping ...");
async {
tokio::time::sleep(Duration::from_nanos(5)).await;
}
.await;
tracing::info!("awaking ...");
Ok(())
});
}
}
Box::pin(futures::future::ready(Ok(())))
}
}
#[tokio::test]
async fn batch_flushes_on_max_size() -> Result<(), BoxError> {
let _guard = support::trace_init();
let aggregator: Aggregator<u32> = Aggregator::new();
let mut batch = Batch::new(aggregator.clone(), 10, Duration::from_secs(1));
let mut results = FuturesUnordered::new();
for i in 0..10 {
let span = tracing::trace_span!("msg", i);
batch.ready().await?;
results.push(span.in_scope(|| batch.call(i)));
}
while let Some(Ok(())) = results.next().await {}
assert!(aggregator.batch_has_size(0, 10));
Ok(())
}
#[tokio::test]
async fn batch_flushes_on_elapsed_time() -> Result<(), BoxError> {
let _guard = support::trace_init();
let aggregator: Aggregator<u32> = Aggregator::new();
let mut batch = Batch::new(aggregator.clone(), 100, Duration::from_millis(200));
let mut results = FuturesUnordered::new();
for i in 0..10 {
let span = tracing::trace_span!("msg", i);
batch.ready().await?;
results.push(span.in_scope(|| batch.call(i)));
}
while let Some(Ok(())) = results.next().await {}
tokio::time::sleep(Duration::from_millis(500)).await;
assert!(aggregator.batch_has_size(0, 10));
Ok(())
}
#[tokio::test]
async fn batch_flushes_multiple_times() -> Result<(), BoxError> {
let _guard = support::trace_init();
let aggregator: Aggregator<u32> = Aggregator::new();
let mut batch = Batch::new(aggregator.clone(), 10, Duration::from_secs(1));
let mut results = FuturesUnordered::new();
for i in 0..20 {
let span = tracing::trace_span!("msg", i);
batch.ready().await?;
results.push(span.in_scope(|| batch.call(i)));
}
while let Some(Ok(())) = results.next().await {}
assert!(aggregator.batch_has_size(0, 10));
assert!(aggregator.batch_has_size(1, 10));
Ok(())
}
#[tokio::test]
async fn batch_items_are_ordered() -> Result<(), BoxError> {
let _guard = support::trace_init();
let aggregator: Aggregator<u32> = Aggregator::new();
let mut batch = Batch::new(aggregator.clone(), 10, Duration::from_secs(1));
let mut results = FuturesUnordered::new();
for i in 0..10 {
batch.ready().await?;
results.push(batch.call(i));
}
while let Some(Ok(())) = results.next().await {}
let items = aggregator.batch_items(0).expect("batch 0 should exist");
assert_eq!(items, (0..10).collect::<Vec<u32>>());
Ok(())
}
#[tokio::test]
async fn concurrent_clones_send_requests() -> Result<(), BoxError> {
let _guard = support::trace_init();
let aggregator: Aggregator<u32> = Aggregator::new();
let batch = Batch::new(aggregator.clone(), 10, Duration::from_secs(1));
let mut handles = FuturesUnordered::new();
for clone_id in 0..3u32 {
let mut svc = batch.clone();
handles.push(tokio::spawn(async move {
let mut results = Vec::new();
for i in 0..3 {
svc.ready().await.unwrap();
results.push(svc.call(clone_id * 10 + i).await);
}
results
}));
}
let mut total = 0usize;
while let Some(result) = handles.next().await {
let results = result.unwrap();
for r in results {
r.unwrap();
total += 1;
}
}
assert_eq!(total, 9);
drop(batch);
tokio::time::sleep(Duration::from_millis(200)).await;
let items = aggregator.items.lock().unwrap();
let delivered: usize = items.iter().map(Vec::len).sum();
assert_eq!(delivered, 9, "all 9 items should reach the aggregator");
Ok(())
}
#[tokio::test]
async fn time_based_flush_triggers_multiple_batches() -> Result<(), BoxError> {
let _guard = support::trace_init();
let aggregator: Aggregator<u32> = Aggregator::new();
let mut batch = Batch::new(aggregator.clone(), 100, Duration::from_millis(100));
let mut results = FuturesUnordered::new();
for i in 0..3 {
batch.ready().await?;
results.push(batch.call(i));
}
while let Some(Ok(())) = results.next().await {}
tokio::time::sleep(Duration::from_millis(250)).await;
let mut results = FuturesUnordered::new();
for i in 10..13 {
batch.ready().await?;
results.push(batch.call(i));
}
while let Some(Ok(())) = results.next().await {}
tokio::time::sleep(Duration::from_millis(250)).await;
assert!(
aggregator.batch_has_size(0, 3),
"first time-based batch should have 3 items"
);
assert!(
aggregator.batch_has_size(1, 3),
"second time-based batch should have 3 items"
);
let b0 = aggregator.batch_items(0).unwrap();
assert_eq!(b0, vec![0, 1, 2]);
let b1 = aggregator.batch_items(1).unwrap();
assert_eq!(b1, vec![10, 11, 12]);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn concurrent_clones_with_backpressure() {
let _guard = support::trace_init();
let (mut service, mut handle) = mock::spawn_with(|s: Mock<BatchControl<&str>, &str>| {
let (svc, worker) = Batch::pair(s, 2, Duration::from_secs(1));
tokio::spawn(async move {
let _guard = support::trace_init();
let mut fut = tokio_test::task::spawn(worker);
while fut.poll().is_pending() {}
});
svc
});
let mut service2 = service.clone();
handle.allow(0);
assert_ready_ok!(service.poll_ready());
let mut res1 = task::spawn(service.call("from_clone1"));
assert_ready_ok!(service2.poll_ready());
let mut res2 = task::spawn(service2.call("from_clone2"));
tokio::time::sleep(Duration::from_millis(50)).await;
assert_pending!(res1.poll());
assert_pending!(res2.poll());
handle.allow(4);
assert_request_eq!(handle, BatchControl::from("from_clone1")).send_response("resp1");
assert_request_eq!(handle, BatchControl::from("from_clone2")).send_response("resp2");
assert_request_eq!(handle, BatchControl::Flush).send_response("flushed");
tokio::time::sleep(Duration::from_millis(50)).await;
assert_eq!(assert_ready_ok!(res1.poll()), "resp1");
assert_eq!(assert_ready_ok!(res2.poll()), "resp2");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn clears_canceled_requests() {
let _guard = support::trace_init();
let (mut service, mut handle) = mock::spawn_with(|s: Mock<BatchControl<&str>, &str>| {
let (svc, worker) = Batch::pair(s, 2, Duration::from_secs(1));
tokio::spawn(async move {
let _guard = support::trace_init();
let mut fut = tokio_test::task::spawn(worker);
while fut.poll().is_pending() {}
});
svc
});
handle.allow(1);
assert_ready_ok!(service.poll_ready());
let mut res1 = task::spawn(service.call("hello"));
let send_response1 = assert_request_eq!(handle, BatchControl::from("hello"));
assert_ready_ok!(service.poll_ready());
let res2 = task::spawn(service.call("hello2"));
assert_pending!(handle.poll_request());
tokio::time::sleep(Duration::from_millis(10)).await;
assert_ready_ok!(service.poll_ready());
let mut res3 = task::spawn(service.call("hello3"));
drop(res2);
send_response1.send_response("world");
tokio::time::sleep(Duration::from_millis(10)).await;
assert_pending!(res1.poll());
assert_pending!(res3.poll());
handle.allow(1);
assert_request_eq!(handle, BatchControl::from("hello3")).send_response("world3");
handle.allow(2);
assert_request_eq!(handle, BatchControl::Flush).send_response("flush");
tokio::time::sleep(Duration::from_millis(10)).await;
assert_eq!(assert_ready_ok!(res1.poll()), "world");
assert_eq!(assert_ready_ok!(res3.poll()), "world3");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn when_inner_is_not_ready() {
let _guard = support::trace_init();
let (mut service, mut handle) = mock::spawn_with(|s: Mock<BatchControl<&str>, &str>| {
let (svc, worker) = Batch::pair(s, 10, Duration::from_secs(1));
tokio::spawn(async move {
let _guard = support::trace_init();
let mut fut = tokio_test::task::spawn(worker);
while fut.poll().is_pending() {}
});
svc
});
handle.allow(0);
assert_ready_ok!(service.poll_ready());
let mut res1 = task::spawn(service.call("hello"));
tokio::time::sleep(Duration::from_millis(100)).await;
assert_pending!(res1.poll());
assert_pending!(handle.poll_request());
handle.allow(3);
assert_request_eq!(handle, BatchControl::from("hello")).send_response("world");
assert_request_eq!(handle, BatchControl::Flush).send_response("flushed");
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(assert_ready_ok!(res1.poll()), "world");
}
#[tokio::test(flavor = "current_thread")]
async fn when_inner_fails() {
use std::error::Error as StdError;
let _guard = support::trace_init();
let (mut service, mut handle) = mock::spawn_with(|s: Mock<BatchControl<&str>, &str>| {
let (svc, worker) = Batch::pair(s, 10, Duration::from_secs(1));
tokio::spawn(async move {
let mut fut = tokio_test::task::spawn(worker);
while fut.poll().is_pending() {}
});
svc
});
handle.allow(0);
handle.send_error("foobar");
assert_ready_ok!(service.poll_ready());
let mut res1 = task::spawn(service.call("hello"));
tokio::time::sleep(Duration::from_millis(100)).await;
let e = assert_ready_err!(res1.poll());
if let Some(e) = e.downcast_ref::<error::ServiceError>() {
let e = e.source().unwrap();
assert_eq!(e.to_string(), "foobar");
} else {
panic!("unexpected error type: {e:?}");
}
}
#[tokio::test(flavor = "current_thread")]
async fn poll_ready_when_worker_is_dropped_early() {
let _guard = support::trace_init();
let (service, _handle) = mock::pair::<BatchControl<()>, ()>();
let (service, worker) = Batch::pair(service, 1, Duration::from_secs(1));
let mut service = mock::Spawn::new(service);
drop(worker);
let err = assert_ready_err!(service.poll_ready());
assert!(err.is::<error::Closed>(), "should be a Closed: {err:?}");
}
#[tokio::test(flavor = "current_thread")]
async fn response_future_when_worker_is_dropped_early() {
let _guard = support::trace_init();
let (service, mut handle) = mock::pair::<BatchControl<&'static str>, &'static str>();
let (service, worker) = Batch::pair(service, 1, Duration::from_secs(1));
let mut service = mock::Spawn::new(service);
handle.allow(0);
assert_ready_ok!(service.poll_ready());
let mut response = task::spawn(service.call("hello"));
drop(worker);
tokio::time::sleep(Duration::from_millis(100)).await;
let err = assert_ready_err!(response.poll());
assert!(err.is::<error::Closed>(), "should be a Closed: {err:?}");
}
#[tokio::test(flavor = "current_thread")]
async fn waits_for_channel_capacity() -> Result<(), BoxError> {
let _guard = support::trace_init();
let (service, mut handle) = mock::pair::<BatchControl<&'static str>, &'static str>();
let (service, worker) = Batch::pair(service, 3, Duration::from_secs(1));
let mut service = mock::Spawn::new(service);
let mut worker = task::spawn(worker);
handle.allow(0);
assert_ready_ok!(service.poll_ready());
let mut response1 = task::spawn(service.call("hello"));
assert_pending!(worker.poll());
assert_ready_ok!(service.poll_ready());
let mut response2 = task::spawn(service.call("hello"));
assert_pending!(worker.poll());
assert_ready_ok!(service.poll_ready());
let mut response3 = task::spawn(service.call("hello"));
assert_pending!(worker.poll());
assert_pending!(service.poll_ready());
handle.allow(1);
assert_pending!(worker.poll());
handle
.next_request()
.await
.unwrap()
.1
.send_response("world");
assert_pending!(worker.poll());
assert_pending!(response1.poll());
assert_ready_ok!(service.poll_ready());
let mut response4 = task::spawn(service.call("hello"));
assert_pending!(worker.poll());
handle.allow(4);
assert_pending!(worker.poll());
handle
.next_request()
.await
.unwrap()
.1
.send_response("world");
assert_pending!(worker.poll());
assert_pending!(response2.poll());
handle
.next_request()
.await
.unwrap()
.1
.send_response("world");
assert_pending!(worker.poll());
assert_pending!(response3.poll());
assert_request_eq!(handle, BatchControl::Flush).send_response("world");
assert_pending!(worker.poll());
assert_ready_ok!(response1.poll());
assert_ready_ok!(response2.poll());
assert_ready_ok!(response3.poll());
handle
.next_request()
.await
.unwrap()
.1
.send_response("world");
assert_pending!(worker.poll());
assert_pending!(response4.poll());
Ok(())
}
#[tokio::test]
async fn request_futures_fail_if_flush_fails() {
let _guard = support::trace_init();
let (service, mut handle) = mock::pair::<BatchControl<&str>, ()>();
let (service, worker) = Batch::pair(service, 2, Duration::from_secs(1));
let mut service = mock::Spawn::new(service);
let mut worker = task::spawn(worker);
handle.allow(4);
assert_ready_ok!(service.poll_ready());
let mut res1 = task::spawn(service.call("hello1"));
assert_ready_ok!(service.poll_ready());
let mut res2 = task::spawn(service.call("hello2"));
assert_pending!(worker.poll());
assert_request_eq!(handle, BatchControl::from("hello1")).send_response(());
assert_pending!(res1.poll());
assert_request_eq!(handle, BatchControl::from("hello2")).send_response(());
assert_pending!(res2.poll());
handle.allow(2);
assert_request_eq!(handle, BatchControl::Flush).send_error("flush failed");
assert_ready!(worker.poll());
assert_ready_err!(res1.poll());
assert_ready_err!(res2.poll());
}
#[tokio::test(flavor = "current_thread")]
async fn wakes_pending_waiters_on_close() -> Result<(), BoxError> {
let _guard = support::trace_init();
let (service, mut handle) = mock::pair::<BatchControl<&str>, ()>();
let (mut service, worker) = Batch::pair(service, 1, Duration::from_secs(1));
let mut worker = task::spawn(worker);
handle.allow(0);
let service1 = service.ready().await?;
assert_pending!(worker.poll());
let mut response = task::spawn(service1.call("hello"));
let mut service1 = service.clone();
let mut ready1 = task::spawn(service1.ready());
assert_pending!(worker.poll());
assert_pending!(ready1.poll(), "no capacity");
let mut service1 = service.clone();
let mut ready2 = task::spawn(service1.ready());
assert_pending!(worker.poll());
assert_pending!(ready2.poll(), "no capacity");
drop(worker);
let err = assert_ready_err!(response.poll());
assert!(
err.is::<error::Closed>(),
"response should fail with a Closed, got: {err:?}"
);
assert!(
ready1.is_woken(),
"dropping worker should wake ready task 1"
);
let err = assert_ready_err!(ready1.poll());
assert!(
err.is::<error::Closed>(),
"ready 1 should fail with a Closed, got: {err:?}"
);
assert!(
ready2.is_woken(),
"dropping worker should wake ready task 2"
);
let err = assert_ready_err!(ready2.poll());
assert!(
err.is::<error::Closed>(),
"ready 2 should fail with a Closed, got: {err:?}"
);
Ok(())
}
#[tokio::test(flavor = "current_thread")]
async fn wakes_pending_waiters_on_failure() -> Result<(), BoxError> {
let _guard = support::trace_init();
let (service, mut handle) = mock::pair::<BatchControl<&str>, ()>();
let (mut service, worker) = Batch::pair(service, 1, Duration::from_secs(1));
let mut worker = task::spawn(worker);
handle.allow(0);
let service1 = service.ready().await?;
assert_pending!(worker.poll());
let mut response = task::spawn(service1.call("hello"));
let mut service1 = service.clone();
let mut ready1 = task::spawn(service1.ready());
assert_pending!(worker.poll());
assert_pending!(ready1.poll(), "no capacity");
let mut service1 = service.clone();
let mut ready2 = task::spawn(service1.ready());
assert_pending!(worker.poll());
assert_pending!(ready2.poll(), "no capacity");
handle.send_error("foobar");
assert_ready!(worker.poll());
let err = assert_ready_err!(response.poll());
assert!(
err.is::<error::ServiceError>(),
"response should fail with a ServiceError, got: {err:?}"
);
assert!(
ready1.is_woken(),
"dropping worker should wake ready task 1"
);
let err = assert_ready_err!(ready1.poll());
assert!(
err.is::<error::ServiceError>(),
"ready 1 should fail with a ServiceError, got: {err:?}"
);
assert!(
ready2.is_woken(),
"dropping worker should wake ready task 2"
);
let err = assert_ready_err!(ready2.poll());
assert!(
err.is::<error::ServiceError>(),
"ready 2 should fail with a ServiceError, got: {err:?}"
);
Ok(())
}
#[tokio::test]
async fn propagates_trace_spans() -> Result<(), BoxError> {
use tower::util::ServiceExt;
use tracing::Instrument;
let _guard = support::trace_init();
let span = tracing::info_span!("my_span");
let service = support::AssertSpanSvc::new(span.clone());
let (mut service, worker) = Batch::pair(service, 5, Duration::from_millis(250));
let worker = tokio::spawn(worker);
let result: JoinHandle<Result<(), tower_batch::BoxError>> = tokio::spawn(async move {
service.ready().await?;
service.call(()).await?;
Ok(())
});
let _ = result.instrument(span).await?;
worker.await?;
Ok(())
}
#[tokio::test(flavor = "current_thread")]
async fn doesnt_leak_permits() {
let _guard = support::trace_init();
let (service, mut handle) = mock::pair::<BatchControl<()>, ()>();
let (mut service1, worker) = Batch::pair(service, 2, Duration::from_secs(1));
let mut worker = task::spawn(worker);
let mut service2 = service1.clone();
let mut service3 = service1.clone();
assert_ready_ok!(task::spawn(service1.ready()).poll());
assert_ready_ok!(task::spawn(service1.ready()).poll());
assert_ready_ok!(task::spawn(service1.ready()).poll());
assert_ready_ok!(task::spawn(service2.ready()).poll());
assert_ready_ok!(task::spawn(service2.ready()).poll());
assert_ready_ok!(task::spawn(service2.ready()).poll());
let mut ready3 = task::spawn(service3.ready());
assert_pending!(ready3.poll());
let mut response1 = task::spawn(service1.call(()));
let mut response2 = task::spawn(service2.call(()));
handle.allow(3);
assert_pending!(worker.poll());
handle.next_request().await.unwrap().1.send_response(());
assert_pending!(worker.poll());
handle.next_request().await.unwrap().1.send_response(());
assert_pending!(worker.poll());
assert_request_eq!(handle, BatchControl::Flush).send_response(());
assert_pending!(worker.poll());
assert_ready_ok!(response1.poll());
assert_ready_ok!(response2.poll());
assert!(ready3.is_woken());
assert_ready_ok!(ready3.poll());
}
#[tokio::test]
async fn batch_layer_wraps_service() {
let _guard = support::trace_init();
let aggregator: Aggregator<u32> = Aggregator::new();
let layer = BatchLayer::<u32>::new(10, Duration::from_secs(1));
let debug_str = format!("{layer:?}");
assert!(
debug_str.contains("BatchLayer"),
"Debug should contain 'BatchLayer', got: {debug_str}",
);
let mut service = layer.layer(aggregator.clone());
service.ready().await.unwrap();
service.call(42).await.unwrap();
tokio::time::sleep(Duration::from_millis(200)).await;
assert!(
aggregator.batch_has_size(0, 1),
"layer-created service should deliver the item"
);
}
#[tokio::test(flavor = "current_thread")]
async fn error_display_and_debug_formatting() {
use std::error::Error as StdError;
let _guard = support::trace_init();
{
let (service, _handle) = mock::pair::<BatchControl<()>, ()>();
let (service, worker) = Batch::pair(service, 1, Duration::from_secs(1));
let mut service = mock::Spawn::new(service);
drop(worker);
let err = assert_ready_err!(service.poll_ready());
let closed = err
.downcast_ref::<error::Closed>()
.expect("should be Closed");
let debug_str = format!("{closed:?}");
assert!(debug_str.contains("Closed"), "Debug: {debug_str}");
let display_str = format!("{closed}");
assert!(
display_str.contains("batch's worker closed unexpectedly"),
"Display: {display_str}",
);
}
{
let (service, mut handle) = mock::pair::<BatchControl<&str>, &str>();
let (service, worker) = Batch::pair(service, 10, Duration::from_secs(1));
let mut service = mock::Spawn::new(service);
let mut worker = task::spawn(worker);
handle.allow(0);
handle.send_error("boom");
assert_ready_ok!(service.poll_ready());
let mut response = task::spawn(service.call("hello"));
tokio::time::sleep(Duration::from_millis(100)).await;
assert_ready!(worker.poll());
let err = assert_ready_err!(response.poll());
let svc_err = err
.downcast_ref::<error::ServiceError>()
.expect("should be ServiceError");
let display_str = format!("{svc_err}");
assert!(
display_str.contains("batch service failed:"),
"Display: {display_str}",
);
let source = svc_err.source().unwrap();
assert_eq!(source.to_string(), "boom");
}
}
#[tokio::test(flavor = "current_thread")]
async fn call_after_worker_death() {
let _guard = support::trace_init();
let (service, _handle) = mock::pair::<BatchControl<&str>, &str>();
let (service, worker) = Batch::pair(service, 1, Duration::from_secs(1));
let mut service = mock::Spawn::new(service);
assert_ready_ok!(service.poll_ready());
drop(worker);
let mut response = task::spawn(service.call("hello"));
let err = assert_ready_err!(response.poll());
assert!(err.is::<error::Closed>(), "should be Closed, got: {err:?}");
}
#[tokio::test(flavor = "current_thread")]
async fn flush_phase_poll_ready_failure() {
let _guard = support::trace_init();
let (service, mut handle) = mock::pair::<BatchControl<&str>, &str>();
let (service, worker) = Batch::pair(service, 2, Duration::from_secs(1));
let mut service = mock::Spawn::new(service);
let mut worker = task::spawn(worker);
handle.allow(2);
assert_ready_ok!(service.poll_ready());
let mut res1 = task::spawn(service.call("a"));
assert_ready_ok!(service.poll_ready());
let mut res2 = task::spawn(service.call("b"));
assert_pending!(worker.poll());
assert_request_eq!(handle, BatchControl::from("a")).send_response("ra");
assert_request_eq!(handle, BatchControl::from("b")).send_response("rb");
handle.send_error("flush ready failed");
assert_ready!(worker.poll());
let err1 = assert_ready_err!(res1.poll());
assert!(
err1.is::<error::ServiceError>(),
"res1 should be ServiceError, got: {err1:?}"
);
let err2 = assert_ready_err!(res2.poll());
assert!(
err2.is::<error::ServiceError>(),
"res2 should be ServiceError, got: {err2:?}"
);
}
#[tokio::test(flavor = "current_thread")]
async fn cancelled_request_in_channel() {
let _guard = support::trace_init();
let (service, mut handle) = mock::pair::<BatchControl<&str>, &str>();
let (service, worker) = Batch::pair(service, 2, Duration::from_millis(1));
let mut service = mock::Spawn::new(service);
let mut worker = task::spawn(worker);
assert_ready_ok!(service.poll_ready());
let res_a = task::spawn(service.call("a"));
drop(res_a);
assert_ready_ok!(service.poll_ready());
let mut res_b = task::spawn(service.call("b"));
handle.allow(3);
assert_pending!(worker.poll());
assert_request_eq!(handle, BatchControl::from("b")).send_response("rb");
tokio::time::sleep(Duration::from_millis(10)).await;
assert_pending!(worker.poll());
assert_request_eq!(handle, BatchControl::Flush).send_response("flushed");
assert_pending!(worker.poll());
assert_eq!(assert_ready_ok!(res_b.poll()), "rb");
}
#[tokio::test]
#[allow(clippy::items_after_statements)]
async fn timer_does_not_overwrite_in_progress_flush() {
let _guard = support::trace_init();
let flush_count = Arc::new(AtomicUsize::new(0));
struct SlowFlushService {
flush_count: Arc<AtomicUsize>,
}
impl Service<BatchControl<String>> for SlowFlushService {
type Response = ();
type Error = BoxError;
type Future = Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: BatchControl<String>) -> Self::Future {
match req {
BatchControl::Item(_) => Box::pin(futures::future::ready(Ok(()))),
BatchControl::Flush => {
let count = self.flush_count.clone();
Box::pin(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
count.fetch_add(1, Ordering::SeqCst);
Ok(())
})
}
}
}
}
let service = SlowFlushService {
flush_count: flush_count.clone(),
};
let mut batch = Batch::new(service, 1, Duration::from_millis(1));
batch.ready().await.unwrap();
let response = batch.call("hello".to_string());
response.await.unwrap();
assert_eq!(
flush_count.load(Ordering::SeqCst),
1,
"flush future should have run to completion, not been dropped by the timer"
);
}
#[tokio::test(flavor = "current_thread")]
async fn worker_terminates_on_poll_ready_error_with_queued_messages() {
let _guard = support::trace_init();
let (service, mut handle) = mock::pair::<BatchControl<&str>, &str>();
let (service, worker) = Batch::pair(service, 3, Duration::from_secs(1));
let mut service = mock::Spawn::new(service);
let mut worker = task::spawn(worker);
handle.allow(0);
assert_ready_ok!(service.poll_ready());
let mut res1 = task::spawn(service.call("msg1"));
assert_ready_ok!(service.poll_ready());
let mut res2 = task::spawn(service.call("msg2"));
assert_pending!(worker.poll());
handle.send_error("boom");
assert_ready!(worker.poll());
let err1 = assert_ready_err!(res1.poll());
assert!(
err1.is::<error::ServiceError>(),
"res1 should be ServiceError, got: {err1:?}"
);
drop(worker);
let err2 = assert_ready_err!(res2.poll());
assert!(
err2.is::<error::Closed>(),
"res2 should be Closed, got: {err2:?}"
);
}