use crossbeam_queue::SegQueue;
use std::fmt::{Debug, Formatter};
use std::ops::{Deref, DerefMut};
use std::{fmt, mem};
use std::sync::atomic::{AtomicUsize, Ordering};
#[cfg(feature = "sync")]
use std::sync::{Condvar, Mutex};
#[cfg(feature = "async")]
use tokio::sync::Notify;
pub struct LendPool<T> {
queue: SegQueue<T>,
available: AtomicUsize,
on_loan: AtomicUsize,
#[cfg(feature = "sync")]
_mutex: Mutex<()>,
#[cfg(feature = "sync")]
_condvar: Condvar,
#[cfg(feature = "async")]
_notify: Notify,
}
impl<T> Default for LendPool<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> LendPool<T> {
pub fn new() -> Self {
Self {
queue: SegQueue::new(),
available: AtomicUsize::new(0),
on_loan: AtomicUsize::new(0),
#[cfg(feature = "sync")]
_mutex: Mutex::new(()),
#[cfg(feature = "sync")]
_condvar: Condvar::new(),
#[cfg(feature = "async")]
_notify: Notify::new(),
}
}
pub fn add(&self, item: T) {
self.available.fetch_add(1, Ordering::SeqCst);
self.queue.push(item);
#[cfg(feature = "async")]
{
self._notify.notify_one();
}
#[cfg(feature = "sync")]
{
let _lock = self._mutex.lock().unwrap();
self._condvar.notify_one();
}
}
pub fn loan(&self) -> Option<Loan<T>> {
self.queue
.pop()
.map(|item| Loan {
item: Some(item),
lp: self,
})
.inspect(|_| {
self.on_loan.fetch_add(1, Ordering::SeqCst);
})
.inspect(|_| {
self.available.fetch_sub(1, Ordering::SeqCst);
})
}
pub fn available(&self) -> usize {
self.available.load(Ordering::SeqCst)
}
pub fn is_available(&self) -> bool {
self.available() >= 1
}
pub fn on_loan(&self) -> usize {
self.on_loan.load(Ordering::SeqCst)
}
pub fn is_loaned(&self) -> bool {
self.on_loan() != 0
}
pub fn total(&self) -> usize {
self.on_loan() + self.available()
}
}
#[cfg(feature = "sync")]
impl<T> LendPool<T> {
pub fn loan_sync(&self) -> Loan<T> {
loop {
if let Some(loaned) = self.loan() {
return loaned;
}
let _lock = self._mutex.lock().expect("mutex has been poisoned");
let _wait = self
._condvar
.wait(_lock)
.expect("condvar has been poisoned");
}
}
}
#[cfg(feature = "async")]
impl<T> LendPool<T> {
pub async fn loan_async(&self) -> Loan<T> {
loop {
if let Some(loan) = self.loan() {
return loan;
}
self._notify.notified().await;
}
}
}
#[derive(Debug)]
pub struct Loan<'lp, T> {
item: Option<T>,
lp: &'lp LendPool<T>,
}
impl<T> Loan<'_, T> {
pub fn map(&mut self, f: impl FnOnce(T) -> T) {
let item = self.item.take().expect("loan already consumed");
self.item = Some(f(item));
}
pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
f(self.item.as_ref().expect("loan already consumed"))
}
pub fn with_mut<R>(&mut self, f: impl FnOnce(&mut T) -> R) -> R {
f(self.item.as_mut().expect("loan already consumed"))
}
pub fn take(mut self) -> T {
let item = self.item.take().expect("loan already consumed");
self.lp.on_loan.fetch_sub(1, Ordering::SeqCst);
item
}
pub fn swap_pool(&mut self, other: &mut Loan<'_, T>) {
mem::swap(&mut self.item, &mut other.item)
}
}
impl<'lp, T> Loan<'lp, T> {
pub fn move_pool(&mut self, pool: &'lp (impl PoolRef<'lp, T> + 'lp)) {
self.lp = pool.pool_ref();
}
}
impl<T> AsRef<T> for Loan<'_, T> {
fn as_ref(&self) -> &T {
self.item.as_ref().expect("loan already consumed")
}
}
impl<T> AsMut<T> for Loan<'_, T> {
fn as_mut(&mut self) -> &mut T {
self.item.as_mut().expect("loan already consumed")
}
}
impl<T> Deref for Loan<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.as_ref()
}
}
impl<T> DerefMut for Loan<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.as_mut()
}
}
impl<T> Drop for Loan<'_, T> {
fn drop(&mut self) {
if let Some(item) = self.item.take() {
self.lp.queue.push(item);
self.lp.on_loan.fetch_sub(1, Ordering::SeqCst);
self.lp.available.fetch_add(1, Ordering::SeqCst);
}
}
}
pub trait PoolRef<'lp, T> {
fn pool_ref(&'lp self) -> &'lp LendPool<T>;
}
impl<'lp, T> PoolRef<'lp, T> for LendPool<T> {
fn pool_ref(&'lp self) -> &'lp LendPool<T> {
self
}
}
impl<'lp, T> PoolRef<'lp, T> for Loan<'lp, T> {
fn pool_ref(&'lp self) -> &'lp LendPool<T> {
self.lp
}
}
impl<T> Debug for LendPool<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("LendPool")
.field("available", &self.available())
.field("on_loan", &self.on_loan())
.field("total", &self.total())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lendpool_new_and_counts() {
let pool = LendPool::<i32>::new();
assert_eq!(
pool.available(),
0,
"Pool should start with zero available items"
);
assert_eq!(
pool.on_loan(),
0,
"Pool should start with zero on-loan items"
);
assert_eq!(
pool.total(),
0,
"Pool should have zero total items initially"
);
}
#[test]
fn test_lendpool_add() {
let pool = LendPool::new();
pool.add("test");
assert_eq!(
pool.available(),
1,
"One item should be available after adding"
);
assert_eq!(pool.on_loan(), 0, "No items on loan yet");
assert_eq!(pool.total(), 1, "Total should be 1");
}
#[test]
fn test_lendpool_loan_non_blocking() {
let pool = LendPool::new();
pool.add(10);
pool.add(20);
let loan_opt = pool.loan();
assert!(loan_opt.is_some(), "Should be able to loan an item");
let loan = loan_opt.unwrap();
assert!(
*loan == 10 || *loan == 20,
"Loaned item should be one of the added items"
);
assert_eq!(
pool.available(),
1,
"One item left available after the first loan"
);
assert_eq!(pool.on_loan(), 1, "One item is on loan");
assert_eq!(pool.total(), 2, "Total should remain 2");
let loan2 = pool.loan().expect("Another item should be available");
assert!(
*loan2 == 10 || *loan2 == 20,
"Second loaned item should be the remaining one"
);
assert_ne!(
*loan, *loan2,
"Items should be different when two are added"
);
assert_eq!(pool.available(), 0, "No items left available");
assert_eq!(pool.on_loan(), 2, "Two items on loan now");
drop(loan);
drop(loan2);
assert_eq!(
pool.available(),
2,
"Items should return after dropping loans"
);
assert_eq!(
pool.on_loan(),
0,
"No items on loan after dropping all loans"
);
}
#[test]
fn test_lendpool_is_available_and_is_loaned() {
let pool = LendPool::new();
assert!(!pool.is_available(), "No items are in the pool yet");
assert!(!pool.is_loaned(), "No items are on loan yet");
pool.add(1);
assert!(pool.is_available(), "Pool has at least one item");
assert!(!pool.is_loaned(), "No items loaned out yet");
let loan = pool.loan().unwrap();
assert!(!pool.is_available(), "No items left in the pool after loan");
assert!(pool.is_loaned(), "Now at least one item is on loan");
drop(loan);
assert!(pool.is_available(), "Item should be back in pool");
assert!(!pool.is_loaned(), "No items on loan after dropping");
}
#[test]
fn test_loan_transformations() {
let pool = LendPool::new();
pool.add(5);
let mut loan = pool.loan().expect("Should have an item to loan");
let doubled = loan.with(|val| *val * 2);
assert_eq!(doubled, 10);
loan.with_mut(|val| *val += 10);
assert_eq!(*loan, 15);
loan.map(|val| val * 2);
assert_eq!(*loan, 30);
drop(loan);
assert_eq!(
pool.available(),
1,
"Item should return after dropping loan"
);
assert_eq!(pool.on_loan(), 0);
}
#[test]
fn test_loan_take() {
let pool = LendPool::new();
pool.add("Hello");
assert_eq!(pool.total(), 1);
{
let loan = pool.loan().expect("Item should be available");
let item = loan.take();
assert_eq!(item, "Hello");
assert_eq!(pool.on_loan(), 0, "No items should be on loan after take");
assert_eq!(pool.available(), 0, "Item is not returned to the pool");
assert_eq!(pool.total(), 0, "Pool total is effectively zero now");
}
assert_eq!(pool.available(), 0, "Still no items in the pool");
assert_eq!(pool.on_loan(), 0);
assert_eq!(pool.total(), 0);
}
#[test]
fn test_loan_swap_pool() {
let pool = LendPool::new();
pool.add('x');
pool.add('y');
let mut loan1 = pool.loan().unwrap();
let mut loan2 = pool.loan().unwrap();
let val1 = *loan1;
let val2 = *loan2;
loan1.swap_pool(&mut loan2);
assert_eq!(*loan1, val2);
assert_eq!(*loan2, val1);
drop(loan1);
drop(loan2);
assert_eq!(pool.available(), 2, "Both items returned to the pool");
assert_eq!(pool.on_loan(), 0);
}
#[test]
fn test_loan_as_ref_and_deref() {
let pool = LendPool::new();
pool.add(String::from("Hello World"));
let mut loan = pool.loan().expect("Should have a string to loan");
assert_eq!(loan.as_ref(), "Hello World");
loan.as_mut().push_str("!!!");
assert_eq!(loan.as_ref(), "Hello World!!!");
assert_eq!(&*loan, "Hello World!!!");
loan.push_str("???");
assert_eq!(&*loan, "Hello World!!!???");
}
#[test]
fn test_loan_drop() {
let pool = LendPool::new();
pool.add(99);
assert_eq!(pool.available(), 1);
assert_eq!(pool.on_loan(), 0);
{
let _loan = pool.loan().expect("Should be able to loan item");
assert_eq!(pool.available(), 0);
assert_eq!(pool.on_loan(), 1);
}
assert_eq!(pool.available(), 1, "Item is returned to the pool on drop");
assert_eq!(pool.on_loan(), 0);
}
#[test]
fn test_lendpool_debug() {
let pool = LendPool::new();
pool.add(1);
let debug_str = format!("{:?}", pool);
assert!(
debug_str.contains("LendPool"),
"Debug output should contain 'LendPool'"
);
assert!(
debug_str.contains("available"),
"Debug output should mention 'available'"
);
assert!(
debug_str.contains("on_loan"),
"Debug output should mention 'on_loan'"
);
assert!(
debug_str.contains("total"),
"Debug output should mention 'total'"
);
}
#[cfg(feature = "sync")]
#[test]
fn test_lendpool_loan_sync() {
let pool = LendPool::new();
pool.add(123);
let loan = pool.loan_sync();
assert_eq!(*loan, 123);
assert_eq!(pool.available(), 0);
assert_eq!(pool.on_loan(), 1);
drop(loan);
assert_eq!(pool.available(), 1);
assert_eq!(pool.on_loan(), 0);
}
#[cfg(feature = "async")]
#[tokio::test]
async fn test_lendpool_loan_async() {
let pool = LendPool::new();
pool.add("hello");
let loan = pool.loan_async().await;
assert_eq!(*loan, "hello");
assert_eq!(pool.available(), 0);
assert_eq!(pool.on_loan(), 1);
drop(loan);
assert_eq!(pool.available(), 1);
assert_eq!(pool.on_loan(), 0);
}
#[test]
fn test_loan_with_method() {
let pool = LendPool::new();
pool.add(42);
let loan = pool.loan().expect("Should be able to loan an item");
assert_eq!(pool.available(), 0, "Item is now on loan");
assert_eq!(pool.on_loan(), 1);
let plus_one = loan.with(|val| *val + 1);
assert_eq!(
plus_one, 43,
"`with` should apply the function to the borrowed item"
);
assert_eq!(pool.available(), 0);
assert_eq!(pool.on_loan(), 1);
drop(loan);
assert_eq!(
pool.available(),
1,
"Item should be returned after dropping the loan"
);
assert_eq!(pool.on_loan(), 0);
}
}