mod common;
use std::{sync::mpsc, time::Duration};
use common::LocalManager;
use compio_pool::{Config, Pool};
const TICK: Duration = Duration::from_millis(20);
async fn ticks(n: u32) {
compio::time::sleep(TICK * n).await;
}
fn reaping(idle_timeout: Option<Duration>, max_lifetime: Option<Duration>) -> Config {
Config::new()
.max_size(4)
.idle_timeout(idle_timeout)
.max_lifetime(max_lifetime)
.reap_interval(TICK)
.acquire_timeout(Duration::from_secs(5))
}
#[compio::test]
async fn an_idle_connection_that_ages_out_is_reaped() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(manager, reaping(Some(Duration::from_millis(1)), None));
drop(pool.acquire().await.unwrap());
assert_eq!(pool.local_idle(), 1);
ticks(5).await;
assert_eq!(
pool.local_idle(),
0,
"nobody asked for it, so nobody else would have caught it"
);
assert_eq!(pool.local_size(), 0, "and its budget came back");
assert_eq!(counts.disconnected(), 1, "closed through the manager");
let m = pool.metrics();
assert_eq!((m.live, m.idle, m.closed), (0, 0, 1));
}
#[compio::test]
async fn a_connection_past_its_max_lifetime_is_reaped() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(manager, reaping(None, Some(Duration::from_millis(1))));
drop(pool.acquire().await.unwrap());
ticks(5).await;
assert_eq!(pool.local_idle(), 0);
assert_eq!(counts.disconnected(), 1);
}
#[compio::test]
async fn a_fresh_idle_connection_is_left_alone() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(manager, reaping(Some(Duration::from_secs(3600)), None));
drop(pool.acquire().await.unwrap());
ticks(5).await;
assert_eq!(
pool.local_idle(),
1,
"the reaper must not close live capacity"
);
assert_eq!(counts.disconnected(), 0);
}
#[compio::test]
async fn a_checked_out_connection_is_not_reaped_under_the_caller() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(manager, reaping(Some(Duration::from_millis(1)), None));
let held = pool.acquire().await.unwrap();
ticks(5).await;
assert_eq!(counts.disconnected(), 0, "it is still in use");
assert_eq!(pool.local_size(), 1);
drop(held);
ticks(5).await;
assert_eq!(counts.disconnected(), 1, "and reaped once it is idle");
}
#[compio::test]
async fn the_reaper_reaps_only_what_has_aged_out() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(manager, reaping(Some(TICK * 4), None));
let a = pool.acquire().await.unwrap();
let b = pool.acquire().await.unwrap();
drop(a);
ticks(3).await;
drop(b);
ticks(3).await;
assert_eq!(counts.disconnected(), 1, "one aged out, one did not");
assert_eq!(pool.local_idle(), 1);
}
#[compio::test]
async fn the_reaper_refills_min_idle() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(
manager,
Config::new()
.max_size(4)
.min_idle(2)
.idle_timeout(None)
.max_lifetime(None)
.reap_interval(TICK),
);
assert_eq!(pool.local_idle(), 0);
ticks(4).await;
assert_eq!(pool.local_idle(), 2, "min_idle should have been dialled up");
assert_eq!(counts.connected(), 2);
assert_eq!(pool.local_size(), 2);
ticks(4).await;
assert_eq!(counts.connected(), 2);
}
#[compio::test]
async fn the_reaper_tops_up_after_a_connection_is_reaped() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(
manager,
Config::new()
.max_size(4)
.min_idle(1)
.idle_timeout(Duration::from_millis(1))
.max_lifetime(None)
.reap_interval(TICK),
);
assert_eq!(pool.local_idle(), 0);
ticks(6).await;
assert!(
counts.connected() >= 2,
"the reaper should keep replacing what it reaps, got {}",
counts.connected()
);
assert_eq!(
counts.connected(),
counts.disconnected() + pool.local_idle() as u64
);
}
#[compio::test]
async fn the_refill_cannot_push_a_shard_past_max_size() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(
manager,
Config::new()
.max_size(2)
.min_idle(2)
.idle_timeout(None)
.max_lifetime(None)
.reap_interval(TICK),
);
let _held = pool.acquire().await.unwrap();
ticks(4).await;
assert_eq!(pool.local_size(), 2, "one held, one warmed, and no more");
assert_eq!(pool.local_idle(), 1);
assert_eq!(counts.connected(), 2);
}
#[compio::test]
async fn a_refill_that_cannot_dial_gives_up_without_leaking() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(
manager,
Config::new()
.max_size(2)
.min_idle(2)
.idle_timeout(None)
.max_lifetime(None)
.reap_interval(TICK),
);
pool.manager().set_fail_connect(true);
assert_eq!(pool.local_idle(), 0);
ticks(4).await;
assert_eq!(counts.connected(), 0);
assert_eq!(pool.local_size(), 0, "each failed attempt freed its slot");
pool.manager().set_fail_connect(false);
ticks(4).await;
assert_eq!(pool.local_idle(), 2);
}
#[compio::test]
async fn the_refill_stands_aside_for_a_local_waiter() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(
manager,
Config::new()
.max_size(1)
.min_idle(1)
.idle_timeout(None)
.max_lifetime(None)
.reap_interval(TICK)
.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) }
});
ticks(4).await;
assert_eq!(counts.connected(), 1, "the waiter gets the one coming back");
drop(held);
assert!(waiting.await.unwrap().is_ok());
}
#[compio::test]
async fn the_reaper_stops_once_the_pool_is_closed() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(
manager,
Config::new()
.max_size(4)
.min_idle(2)
.idle_timeout(None)
.max_lifetime(None)
.reap_interval(TICK),
);
assert_eq!(pool.local_idle(), 0);
ticks(4).await;
assert_eq!(pool.local_idle(), 2);
pool.close();
assert_eq!(pool.local_idle(), 0, "close drains this thread itself");
ticks(6).await;
assert_eq!(pool.local_idle(), 0, "a closed pool is never refilled");
assert_eq!(counts.connected(), 2, "and the reaper dialled nothing more");
assert_eq!(counts.disconnected(), 2);
}
#[test]
fn a_reaper_closes_idle_connections_after_another_thread_closed_the_pool() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(
manager,
Config::new()
.max_size(4)
.min_idle(0)
.max_lifetime(Duration::from_secs(3600))
.idle_timeout(None)
.reap_interval(TICK)
.acquire_timeout(Duration::from_secs(5)),
);
let (ready_tx, ready_rx) = mpsc::channel();
let (closed_tx, closed_rx) = mpsc::channel();
let worker = std::thread::spawn({
let pool = pool.clone();
move || {
compio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
drop(pool.acquire().await.unwrap());
assert_eq!(pool.local_idle(), 1);
ready_tx.send(()).unwrap();
closed_rx.recv().unwrap();
ticks(5).await;
pool.local_idle()
})
}
});
ready_rx.recv().unwrap();
assert_eq!(pool.metrics().idle, 1);
pool.close();
closed_tx.send(()).unwrap();
assert_eq!(
worker.join().unwrap(),
0,
"the worker's reaper must close what close() could not reach"
);
assert_eq!(counts.disconnected(), 1);
assert_eq!(pool.metrics().live, 0);
}
#[test]
fn a_pool_without_a_runtime_enforces_expiry_on_acquire_instead() {
let manager = LocalManager::new();
let counts = manager.counts();
let pool = Pool::new(manager, reaping(Some(Duration::from_millis(1)), None));
assert_eq!(pool.local_size(), 0);
compio::runtime::Runtime::new().unwrap().block_on(async {
drop(pool.acquire().await.unwrap());
assert_eq!(pool.local_idle(), 1);
compio::time::sleep(TICK * 5).await;
assert_eq!(pool.local_idle(), 1);
let conn = pool.acquire().await.unwrap();
assert_eq!(conn.id, 1, "a replacement, not the stale one");
});
assert_eq!(counts.disconnected(), 1);
}