#![allow(dead_code)]
use std::{
marker::PhantomData,
rc::Rc,
sync::{
Arc, Mutex, MutexGuard,
atomic::{AtomicBool, AtomicU64, Ordering::SeqCst},
},
};
use compio_pool::{Detach, Manage, SlotMeta};
#[derive(Debug)]
pub struct LocalConn {
pub id: u64,
_not_send: Rc<()>,
}
#[derive(Debug, Default)]
pub struct Counts {
pub connected: AtomicU64,
pub disconnected: AtomicU64,
pub recycled: AtomicU64,
}
impl LocalConn {
pub fn new(id: u64) -> Self {
Self {
id,
_not_send: Rc::new(()),
}
}
}
pub struct LocalManager {
pub counts: Arc<Counts>,
next_id: AtomicU64,
fail_connect: AtomicBool,
fail_recycle: AtomicBool,
hang_connect: AtomicBool,
hang_recycle: AtomicBool,
}
impl LocalManager {
pub fn new() -> Self {
Self {
counts: Arc::new(Counts::default()),
next_id: AtomicU64::new(0),
fail_connect: AtomicBool::new(false),
fail_recycle: AtomicBool::new(false),
hang_connect: AtomicBool::new(false),
hang_recycle: AtomicBool::new(false),
}
}
pub fn counts(&self) -> Arc<Counts> {
self.counts.clone()
}
pub fn set_fail_recycle(&self, v: bool) {
self.fail_recycle.store(v, SeqCst);
}
pub fn set_fail_connect(&self, v: bool) {
self.fail_connect.store(v, SeqCst);
}
pub fn set_hang_connect(&self, v: bool) {
self.hang_connect.store(v, SeqCst);
}
pub fn set_hang_recycle(&self, v: bool) {
self.hang_recycle.store(v, SeqCst);
}
}
impl Manage for LocalManager {
type Connection = LocalConn;
type Error = &'static str;
async fn connect(&self) -> Result<LocalConn, &'static str> {
if self.hang_connect.load(SeqCst) {
std::future::pending::<()>().await;
}
if self.fail_connect.load(SeqCst) {
return Err("connect refused");
}
self.counts.connected.fetch_add(1, SeqCst);
Ok(LocalConn {
id: self.next_id.fetch_add(1, SeqCst),
_not_send: Rc::new(()),
})
}
async fn recycle(&self, _conn: &mut LocalConn, _meta: &SlotMeta) -> Result<(), &'static str> {
if self.hang_recycle.load(SeqCst) {
std::future::pending::<()>().await;
}
self.counts.recycled.fetch_add(1, SeqCst);
if self.fail_recycle.load(SeqCst) {
return Err("stale");
}
Ok(())
}
fn disconnect(&self, _conn: LocalConn) {
self.counts.disconnected.fetch_add(1, SeqCst);
}
}
#[derive(Debug)]
pub struct MovableConn {
pub id: u64,
_not_send: PhantomData<*const ()>,
}
impl MovableConn {
pub fn new(id: u64) -> Self {
Self {
id,
_not_send: PhantomData,
}
}
}
pub struct MovableManager {
pub counts: Arc<Counts>,
next_id: AtomicU64,
}
impl MovableManager {
pub fn new() -> Self {
Self {
counts: Arc::new(Counts::default()),
next_id: AtomicU64::new(0),
}
}
pub fn counts(&self) -> Arc<Counts> {
self.counts.clone()
}
}
impl Manage for MovableManager {
type Connection = MovableConn;
type Error = &'static str;
async fn connect(&self) -> Result<MovableConn, &'static str> {
self.counts.connected.fetch_add(1, SeqCst);
Ok(MovableConn {
id: self.next_id.fetch_add(1, SeqCst),
_not_send: PhantomData,
})
}
async fn recycle(&self, _conn: &mut MovableConn, _meta: &SlotMeta) -> Result<(), &'static str> {
self.counts.recycled.fetch_add(1, SeqCst);
Ok(())
}
fn disconnect(&self, _conn: MovableConn) {
self.counts.disconnected.fetch_add(1, SeqCst);
}
}
pub static CAN_DETACH: AtomicBool = AtomicBool::new(true);
pub static CAN_ATTACH: AtomicBool = AtomicBool::new(true);
pub static HANG_ATTACH: AtomicBool = AtomicBool::new(false);
static DETACH_LOCK: Mutex<()> = Mutex::new(());
pub fn detach_lock() -> DetachGuard {
let guard = DETACH_LOCK.lock().unwrap_or_else(|p| p.into_inner());
CAN_DETACH.store(true, SeqCst);
CAN_ATTACH.store(true, SeqCst);
HANG_ATTACH.store(false, SeqCst);
DetachGuard(Some(guard))
}
pub struct DetachGuard(Option<MutexGuard<'static, ()>>);
impl Drop for DetachGuard {
fn drop(&mut self) {
CAN_DETACH.store(true, SeqCst);
CAN_ATTACH.store(true, SeqCst);
HANG_ATTACH.store(false, SeqCst);
drop(self.0.take());
}
}
impl Detach for MovableManager {
type Parked = u64;
fn detach(conn: MovableConn) -> Option<u64> {
CAN_DETACH.load(SeqCst).then_some(conn.id)
}
async fn attach(id: u64) -> Result<MovableConn, &'static str> {
if HANG_ATTACH.load(SeqCst) {
std::future::pending::<()>().await;
}
if !CAN_ATTACH.load(SeqCst) {
return Err("cannot attach");
}
Ok(MovableConn {
id,
_not_send: PhantomData,
})
}
}
impl Counts {
pub fn connected(&self) -> u64 {
self.connected.load(SeqCst)
}
pub fn disconnected(&self) -> u64 {
self.disconnected.load(SeqCst)
}
pub fn recycled(&self) -> u64 {
self.recycled.load(SeqCst)
}
}
pub struct DefaultsManager;
impl Manage for DefaultsManager {
type Connection = LocalConn;
type Error = &'static str;
async fn connect(&self) -> Result<LocalConn, &'static str> {
Ok(LocalConn {
id: 0,
_not_send: Rc::new(()),
})
}
async fn recycle(&self, _conn: &mut LocalConn, _meta: &SlotMeta) -> Result<(), &'static str> {
Ok(())
}
}
pub fn cfg() -> compio_pool::Config {
compio_pool::Config::new()
.max_lifetime(None)
.idle_timeout(None)
.acquire_timeout(std::time::Duration::from_millis(200))
}
pub fn field(config: &compio_pool::Config, name: &str) -> String {
let s = format!("{config:?}");
let at = s
.find(&format!("{name}: "))
.unwrap_or_else(|| panic!("no field `{name}` in {s}"));
let rest = &s[at + name.len() + 2..];
let end = rest
.find(", ")
.or_else(|| rest.find(" }"))
.expect("Debug output is malformed");
rest[..end].to_string()
}
pub struct Rng(u64);
impl Rng {
pub fn new(seed: u64) -> Self {
Self((seed ^ 0x9E37_79B9_7F4A_7C15) | 1)
}
pub fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
pub fn byte(&mut self) -> u8 {
self.next_u64() as u8
}
pub fn below(&mut self, n: usize) -> usize {
if n == 0 {
0
} else {
(self.next_u64() % n as u64) as usize
}
}
pub fn in_range(&mut self, lo: usize, hi: usize) -> usize {
lo + self.below(hi - lo + 1)
}
pub fn chance(&mut self, percent: u32) -> bool {
(self.next_u64() % 100) < percent as u64
}
}
pub fn fuzz_seeds() -> u64 {
env_or("COMPIO_POOL_FUZZ_SEEDS", 24)
}
pub fn fuzz_steps() -> usize {
env_or("COMPIO_POOL_FUZZ_STEPS", 200) as usize
}
fn env_or(name: &str, default: u64) -> u64 {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
pub trait ConnId {
fn conn_id(&self) -> u64;
}
impl ConnId for LocalConn {
fn conn_id(&self) -> u64 {
self.id
}
}
impl ConnId for MovableConn {
fn conn_id(&self) -> u64 {
self.id
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Op {
Acquire,
Release(usize),
Poison(usize),
Abandon(usize),
Take(usize),
Invalidate,
Warm,
Close,
}
impl Op {
pub fn decode(selector: u8, index: u8) -> Self {
let i = index as usize;
match selector % 64 {
0..=24 => Op::Acquire,
25..=46 => Op::Release(i),
47..=52 => Op::Poison(i),
53..=56 => Op::Abandon(i),
57..=58 => Op::Take(i),
59..=61 => Op::Invalidate,
_ => Op::Warm,
}
}
pub fn decode_all(bytes: &[u8]) -> Vec<Op> {
bytes
.chunks(2)
.map(|c| Op::decode(c[0], *c.get(1).unwrap_or(&0)))
.collect()
}
pub fn sample(rng: &mut Rng) -> Self {
Op::decode(rng.byte(), rng.byte())
}
}
#[derive(Debug, Default)]
pub struct CountingWaker(AtomicU64);
impl CountingWaker {
pub fn count(&self) -> u64 {
self.0.load(SeqCst)
}
}
impl std::task::Wake for CountingWaker {
fn wake(self: Arc<Self>) {
self.0.fetch_add(1, SeqCst);
}
fn wake_by_ref(self: &Arc<Self>) {
self.0.fetch_add(1, SeqCst);
}
}
pub fn counting_waker() -> (Arc<CountingWaker>, std::task::Waker) {
let arc = Arc::new(CountingWaker::default());
let waker = std::task::Waker::from(arc.clone());
(arc, waker)
}
#[derive(Debug, Default, Clone)]
pub struct Tally {
pub acquires: u64,
pub timeouts: u64,
pub backend_errors: u64,
pub closed_errors: u64,
pub taken: u64,
pub cleared: u64,
pub retired: std::collections::HashSet<u64>,
}
pub async fn run_ops<M, X>(
pool: &compio_pool::Pool<M, X>,
max_size: usize,
ops: &[Op],
label: &str,
tally: &mut Tally,
) where
M: Manage,
M::Connection: ConnId,
X: compio_pool::Exchange<M>,
{
let mut held: Vec<compio_pool::Pooled<M, X>> = Vec::new();
let mut prev = pool.metrics();
for (step, op) in ops.iter().enumerate() {
match *op {
Op::Acquire => match pool.acquire().await {
Ok(conn) => {
let id = conn.conn_id();
assert!(
!tally.retired.contains(&id),
"{label} step {step}: connection {id} was handed out again after \
being poisoned or taken"
);
tally.acquires += 1;
held.push(conn);
}
Err(compio_pool::Error::Timeout) => tally.timeouts += 1,
Err(compio_pool::Error::Closed) => tally.closed_errors += 1,
Err(compio_pool::Error::Backend(_)) => tally.backend_errors += 1,
},
Op::Release(i) if !held.is_empty() => {
let i = i % held.len();
drop(held.remove(i));
}
Op::Poison(i) if !held.is_empty() => {
let i = i % held.len();
let conn = held.remove(i);
tally.retired.insert(conn.conn_id());
conn.poison();
drop(conn);
}
Op::Abandon(i) if !held.is_empty() => {
let i = i % held.len();
tally.retired.insert(held[i].conn_id());
drop(held[i].begin_op());
}
Op::Take(i) if !held.is_empty() => {
let i = i % held.len();
let conn = held.remove(i);
tally.retired.insert(conn.conn_id());
let _escaped = conn.take();
tally.taken += 1;
}
Op::Invalidate => {
tally.cleared += pool.metrics().parked;
pool.invalidate();
}
Op::Warm => {
let _ = pool.warm().await;
}
Op::Close => {
tally.cleared += pool.metrics().parked;
pool.close();
}
_ => {}
}
check_pool_invariants(pool, max_size, &held, tally, &prev, label, step, *op);
prev = pool.metrics();
}
}
#[allow(clippy::too_many_arguments)]
pub fn check_pool_invariants<M, X>(
pool: &compio_pool::Pool<M, X>,
max_size: usize,
held: &[compio_pool::Pooled<M, X>],
tally: &Tally,
prev: &compio_pool::Metrics,
label: &str,
step: usize,
op: Op,
) where
M: Manage,
M::Connection: ConnId,
X: compio_pool::Exchange<M>,
{
let m = pool.metrics();
let (size, idle) = (pool.local_size(), pool.local_idle());
assert!(
size <= max_size,
"{label} step {step} {op:?}: shard holds {size} > max_size {max_size}"
);
assert!(
idle <= size,
"{label} step {step} {op:?}: {idle} idle but only {size} owned"
);
assert!(
held.len() <= max_size,
"{label} step {step} {op:?}: {} guards out, max_size {max_size}",
held.len()
);
assert!(
m.idle <= m.live,
"{label} step {step} {op:?}: idle exceeds live: {m:?}"
);
assert_eq!(
m.live as usize, size,
"{label} step {step} {op:?}: live gauge and shard size disagree: {m:?}"
);
assert_eq!(
m.idle as usize, idle,
"{label} step {step} {op:?}: idle gauge and free list disagree: {m:?}"
);
for (name, was, now) in [
("created", prev.created, m.created),
("closed", prev.closed, m.closed),
("acquires", prev.acquires, m.acquires),
("waits", prev.waits, m.waits),
("timeouts", prev.timeouts, m.timeouts),
("poisoned", prev.poisoned, m.poisoned),
(
"recycle_failures",
prev.recycle_failures,
m.recycle_failures,
),
("unparked", prev.unparked, m.unparked),
] {
assert!(
now >= was,
"{label} step {step} {op:?}: counter `{name}` went backwards, {was} -> {now}"
);
}
assert_eq!(
m.created,
m.closed + m.live + m.parked + tally.taken + tally.cleared,
"{label} step {step} {op:?}: connections unaccounted for \
(taken {}, cleared {}): {m:?}",
tally.taken,
tally.cleared
);
assert_eq!(
m.acquires, tally.acquires,
"{label} step {step} {op:?}: acquire count disagrees with the driver: {m:?}"
);
let mut ids: Vec<u64> = held.iter().map(|c| c.conn_id()).collect();
let out = ids.len();
ids.sort_unstable();
ids.dedup();
assert_eq!(
ids.len(),
out,
"{label} step {step} {op:?}: the same connection is checked out twice"
);
}
pub async fn sample_meta() -> SlotMeta {
let pool = compio_pool::Pool::new(LocalManager::new(), cfg().max_size(1));
let conn = pool
.acquire()
.await
.expect("a fresh single-slot pool always serves");
conn.meta().clone()
}