mod common;
use std::time::Duration;
use common::{LocalManager, cfg};
use compio_pool::{Error, Pool};
#[compio::test]
async fn the_first_checkout_dials_and_does_not_recycle() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(manager, cfg().max_size(2));
let conn = pool.acquire().await.unwrap();
assert_eq!(conn.id, 0);
assert_eq!(counts.connected(), 1);
assert_eq!(counts.recycled(), 0, "there was nothing to revalidate");
assert_eq!(pool.local_size(), 1);
assert_eq!(pool.local_idle(), 0, "it is checked out, not idle");
assert_eq!(pool.metrics().acquires, 1);
}
#[compio::test]
async fn returning_a_connection_makes_it_idle_rather_than_closing_it() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(manager, cfg().max_size(2));
drop(pool.acquire().await.unwrap());
assert_eq!(pool.local_idle(), 1);
assert_eq!(pool.local_size(), 1, "it is still this shard's connection");
assert_eq!(counts.disconnected(), 0);
let m = pool.metrics();
assert_eq!((m.live, m.idle, m.closed), (1, 1, 0));
}
#[compio::test]
async fn idle_connections_are_reused_newest_first() {
let pool = Pool::new(LocalManager::new(), cfg().max_size(4));
let a = pool.acquire().await.unwrap();
let b = pool.acquire().await.unwrap();
let (a_id, b_id) = (a.id, b.id);
drop(a);
drop(b);
let first = pool.acquire().await.unwrap();
let second = pool.acquire().await.unwrap();
assert_eq!((first.id, second.id), (b_id, a_id));
}
#[compio::test]
async fn distinct_checkouts_get_distinct_connections() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(manager, cfg().max_size(3));
let a = pool.acquire().await.unwrap();
let b = pool.acquire().await.unwrap();
let c = pool.acquire().await.unwrap();
let mut ids = [a.id, b.id, c.id];
ids.sort();
assert_eq!(ids, [0, 1, 2]);
assert_eq!(counts.connected(), 3);
assert_eq!(pool.local_size(), 3);
}
#[compio::test]
async fn a_refused_dial_surfaces_the_manager_error() {
let pool = Pool::new(LocalManager::new(), cfg().max_size(1));
pool.manager().set_fail_connect(true);
let err = pool.acquire().await.expect_err("connect was refused");
assert_eq!(err.into_backend(), Some("connect refused"));
assert_eq!(pool.local_size(), 0);
let m = pool.metrics();
assert_eq!((m.created, m.live, m.acquires), (0, 0, 0));
}
#[compio::test]
async fn a_shard_recovers_from_a_run_of_refused_dials() {
let pool = Pool::new(LocalManager::new(), cfg().max_size(1));
pool.manager().set_fail_connect(true);
for _ in 0..5 {
assert!(pool.acquire().await.is_err());
}
pool.manager().set_fail_connect(false);
assert!(pool.acquire().await.is_ok(), "the shard wedged at max_size");
}
#[compio::test]
async fn a_whole_stale_free_list_is_discarded_and_replaced() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(manager, cfg().max_size(4));
let held: Vec<_> = {
let mut v = Vec::new();
for _ in 0..3 {
v.push(pool.acquire().await.unwrap());
}
v
};
drop(held);
assert_eq!(pool.local_idle(), 3);
pool.manager().set_fail_recycle(true);
pool.manager().set_fail_connect(true);
let err = pool.acquire().await.expect_err("nothing usable is left");
assert_eq!(err.into_backend(), Some("connect refused"));
assert_eq!(counts.disconnected(), 3, "all three stale ones were closed");
assert_eq!(pool.metrics().recycle_failures, 3);
assert_eq!(pool.local_size(), 0, "and their budget came back");
}
#[compio::test]
async fn an_error_reports_which_failure_mode_it_was() {
let pool = Pool::new(LocalManager::new(), cfg().max_size(1));
pool.manager().set_fail_connect(true);
let err = pool.acquire().await.unwrap_err();
assert!(err.to_string().contains("failed to establish connection"));
pool.close();
let err = pool.acquire().await.unwrap_err();
assert!(matches!(err, Error::Closed));
assert_eq!(err.into_backend(), None);
}
#[compio::test]
async fn a_waiter_is_counted_and_then_served() {
let pool = Pool::new(LocalManager::new(), cfg().max_size(1));
let held = pool.acquire().await.unwrap();
let id = held.id;
let waiting = compio::runtime::spawn({
let pool = pool.clone();
async move { pool.acquire().await.map(|c| c.id) }
});
compio::time::sleep(Duration::from_millis(20)).await;
assert_eq!(pool.metrics().waits, 1);
drop(held);
assert_eq!(waiting.await.unwrap().unwrap(), id, "the same warm socket");
assert_eq!(
pool.manager().counts().connected(),
1,
"waiting beats dialling past max_size"
);
}
#[compio::test]
async fn a_waiter_is_woken_when_a_connection_is_destroyed_instead_of_returned() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(manager, cfg().max_size(1));
let held = pool.acquire().await.unwrap();
held.poison();
let waiting = compio::runtime::spawn({
let pool = pool.clone();
async move { pool.acquire().await.map(|c| c.id) }
});
compio::time::sleep(Duration::from_millis(20)).await;
drop(held);
let id = waiting
.await
.unwrap()
.expect("the freed slot must be usable");
assert_ne!(id, 0, "a poisoned connection is never handed on");
assert_eq!(counts.connected(), 2);
}
#[compio::test]
async fn a_waiter_woken_after_close_is_told_the_pool_is_closed() {
let pool = Pool::new(LocalManager::new(), cfg().max_size(1).acquire_timeout(None));
let held = pool.acquire().await.unwrap();
let waiting = compio::runtime::spawn({
let pool = pool.clone();
async move { pool.acquire().await.map(|c| c.id) }
});
compio::time::sleep(Duration::from_millis(20)).await;
pool.close();
drop(held);
assert!(matches!(waiting.await.unwrap(), Err(Error::Closed)));
}
#[compio::test]
async fn without_a_timeout_a_checkout_waits_indefinitely() {
let pool = Pool::new(LocalManager::new(), cfg().max_size(1).acquire_timeout(None));
let held = pool.acquire().await.unwrap();
let waiting = compio::runtime::spawn({
let pool = pool.clone();
async move { pool.acquire().await.map(|c| c.id) }
});
compio::time::sleep(Duration::from_millis(300)).await;
drop(held);
assert!(waiting.await.unwrap().is_ok());
assert_eq!(pool.metrics().timeouts, 0);
}
#[compio::test]
async fn a_timeout_is_counted_and_leaves_the_shard_intact() {
let pool = Pool::new(
LocalManager::new(),
cfg().max_size(1).acquire_timeout(Duration::from_millis(20)),
);
let held = pool.acquire().await.unwrap();
assert!(matches!(pool.acquire().await, Err(Error::Timeout)));
assert!(matches!(pool.acquire().await, Err(Error::Timeout)));
assert_eq!(pool.metrics().timeouts, 2);
drop(held);
assert_eq!(pool.local_idle(), 1);
assert!(pool.acquire().await.is_ok());
}
#[compio::test]
async fn warm_is_a_no_op_without_min_idle() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(manager, cfg().max_size(4).min_idle(0));
pool.warm().await.unwrap();
assert_eq!(counts.connected(), 0);
assert_eq!(pool.local_idle(), 0);
}
#[compio::test]
async fn warm_is_idempotent() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(manager, cfg().max_size(4).min_idle(2));
pool.warm().await.unwrap();
pool.warm().await.unwrap();
assert_eq!(counts.connected(), 2, "the target was already met");
assert_eq!(pool.local_idle(), 2);
}
#[compio::test]
async fn warm_clamps_min_idle_to_max_size() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(manager, cfg().max_size(2).min_idle(10));
pool.warm().await.unwrap();
assert_eq!(counts.connected(), 2);
assert_eq!(pool.local_idle(), 2);
}
#[compio::test]
async fn warm_stops_when_the_shard_has_no_budget_left() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(manager, cfg().max_size(2).min_idle(2));
let _held = pool.acquire().await.unwrap();
pool.warm().await.unwrap();
assert_eq!(counts.connected(), 2, "one checked out, one warmed");
assert_eq!(pool.local_idle(), 1);
assert_eq!(pool.local_size(), 2);
}
#[compio::test]
async fn warm_reports_a_refused_dial_and_returns_the_budget() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(manager, cfg().max_size(4).min_idle(3));
pool.manager().set_fail_connect(true);
let err = pool.warm().await.expect_err("connect was refused");
assert_eq!(err.into_backend(), Some("connect refused"));
assert_eq!(counts.connected(), 0);
assert_eq!(pool.local_size(), 0, "the failed attempt freed its slot");
assert_eq!(pool.local_idle(), 0);
}
#[compio::test]
async fn warm_keeps_the_connections_it_opened_before_failing() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(manager, cfg().max_size(4).min_idle(1));
pool.warm().await.unwrap();
pool.manager().set_fail_connect(true);
assert!(pool.warm().await.is_ok(), "min_idle is already satisfied");
assert_eq!(counts.connected(), 1);
assert_eq!(pool.local_idle(), 1);
}
#[compio::test]
async fn a_warmed_connection_is_handed_out_without_a_new_dial() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(manager, cfg().max_size(4).min_idle(2));
pool.warm().await.unwrap();
let conn = pool.acquire().await.unwrap();
assert_eq!(counts.connected(), 2, "no handshake on the first request");
assert_eq!(counts.recycled(), 1, "but it is revalidated first");
assert_eq!(conn.meta().uses, 0, "warming is not a checkout");
}