use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI64;
use std::sync::atomic::AtomicU64;
use parking_lot::Mutex;
use crate::log_file::mapped_file::reference_resource::ReferenceResource;
#[repr(align(64))] pub struct ReferenceResourceBase {
pub ref_count: AtomicI64,
pub available: AtomicBool,
pub cleanup_over: AtomicBool,
pub first_shutdown_timestamp: AtomicU64,
pub hold_lock: Mutex<()>,
pub release_lock: Mutex<()>,
}
impl ReferenceResourceBase {
#[inline]
pub fn new() -> Self {
Self {
ref_count: AtomicI64::new(1),
available: AtomicBool::new(true),
cleanup_over: AtomicBool::new(false),
first_shutdown_timestamp: AtomicU64::new(0),
hold_lock: Mutex::new(()),
release_lock: Mutex::new(()),
}
}
}
impl Default for ReferenceResourceBase {
fn default() -> Self {
Self::new()
}
}
pub struct ReferenceResourceCounter {
base: ReferenceResourceBase,
}
impl ReferenceResourceCounter {
#[inline]
pub fn new() -> Self {
Self {
base: ReferenceResourceBase::new(),
}
}
#[inline]
pub fn base(&self) -> &ReferenceResourceBase {
&self.base
}
}
impl Default for ReferenceResourceCounter {
fn default() -> Self {
Self::new()
}
}
impl ReferenceResource for ReferenceResourceCounter {
fn base(&self) -> &ReferenceResourceBase {
&self.base
}
fn cleanup(&self, _current_ref: i64) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::Barrier;
use std::thread;
use std::time::Duration;
use super::*;
#[test]
fn test_initial_state() {
let resource = ReferenceResourceCounter::new();
assert_eq!(resource.get_ref_count(), 1, "Initial refCount should be 1");
assert!(resource.is_available(), "Should be available initially");
assert!(!resource.is_cleanup_over(), "Cleanup should not be over");
}
#[test]
fn test_default_trait() {
let resource = ReferenceResourceCounter::default();
assert_eq!(resource.get_ref_count(), 1);
assert!(resource.is_available());
}
#[test]
fn test_hold_success_when_available() {
let resource = ReferenceResourceCounter::new();
assert!(resource.hold(), "First hold should succeed");
assert_eq!(resource.get_ref_count(), 2);
assert!(resource.hold(), "Second hold should succeed");
assert_eq!(resource.get_ref_count(), 3);
}
#[test]
fn test_hold_fails_after_shutdown() {
let resource = ReferenceResourceCounter::new();
resource.shutdown(0);
assert!(
!resource.is_available(),
"Should not be available after shutdown"
);
assert!(!resource.hold(), "Hold should fail after shutdown");
}
#[test]
fn test_hold_rollback_when_refcount_negative() {
let resource = ReferenceResourceCounter::new();
resource.base.ref_count.store(-5, Ordering::Release);
resource.base.available.store(true, Ordering::Release);
assert!(
!resource.hold(),
"Hold should fail when refCount is negative"
);
assert_eq!(resource.get_ref_count(), -5, "Should rollback increment");
}
#[test]
fn test_release_decrements_refcount() {
let resource = ReferenceResourceCounter::new();
resource.hold(); resource.release(); assert_eq!(resource.get_ref_count(), 1);
assert!(!resource.is_cleanup_over());
}
#[test]
fn test_release_triggers_cleanup_at_zero() {
let resource = ReferenceResourceCounter::new();
resource.release(); assert_eq!(resource.get_ref_count(), 0);
assert!(resource.is_cleanup_over(), "Cleanup should be triggered");
}
#[test]
fn test_multiple_releases_go_negative() {
let resource = ReferenceResourceCounter::new();
resource.release(); resource.release(); assert_eq!(resource.get_ref_count(), -1);
resource.release(); assert_eq!(resource.get_ref_count(), -2);
}
#[test]
fn test_shutdown_immediate_cleanup_when_no_holders() {
let resource = ReferenceResourceCounter::new();
resource.shutdown(0); assert!(!resource.is_available());
assert_eq!(resource.get_ref_count(), 0);
assert!(resource.is_cleanup_over());
}
#[test]
fn test_shutdown_waits_for_holders() {
let resource = ReferenceResourceCounter::new();
resource.hold(); resource.hold();
resource.shutdown(0); assert!(!resource.is_available());
assert_eq!(resource.get_ref_count(), 2);
assert!(!resource.is_cleanup_over(), "Should wait for holders");
resource.release(); assert!(!resource.is_cleanup_over());
resource.release(); assert!(resource.is_cleanup_over());
}
#[test]
fn test_forced_shutdown_after_timeout() {
let resource = ReferenceResourceCounter::new();
resource.hold(); resource.shutdown(50);
assert_eq!(resource.get_ref_count(), 1);
assert!(!resource.is_cleanup_over());
thread::sleep(Duration::from_millis(60));
resource.shutdown(50);
let count = resource.get_ref_count();
assert!(
count <= -1000,
"Should force negative after timeout, got {}",
count
);
}
#[test]
fn test_no_forced_shutdown_before_timeout() {
let resource = ReferenceResourceCounter::new();
resource.hold(); resource.shutdown(1000);
thread::sleep(Duration::from_millis(10));
resource.shutdown(1000);
assert_eq!(
resource.get_ref_count(),
1,
"Should not force before timeout"
);
}
#[test]
fn test_shutdown_idempotent_after_first_call() {
let resource = ReferenceResourceCounter::new();
let timestamp_after_first = {
resource.shutdown(1000);
resource
.base
.first_shutdown_timestamp
.load(Ordering::Acquire)
};
thread::sleep(Duration::from_millis(10));
resource.shutdown(1000);
let timestamp_after_second = resource
.base
.first_shutdown_timestamp
.load(Ordering::Acquire);
assert_eq!(
timestamp_after_first, timestamp_after_second,
"Timestamp should not change on subsequent shutdowns"
);
}
#[test]
fn test_cleanup_called_exactly_once() {
struct TestResource {
base: ReferenceResourceBase,
cleanup_count: Arc<AtomicUsize>,
}
impl ReferenceResource for TestResource {
fn base(&self) -> &ReferenceResourceBase {
&self.base
}
fn cleanup(&self, _current_ref: i64) -> bool {
self.cleanup_count.fetch_add(1, Ordering::SeqCst);
true
}
}
let cleanup_count = Arc::new(AtomicUsize::new(0));
let resource = Arc::new(TestResource {
base: ReferenceResourceBase::new(),
cleanup_count: Arc::clone(&cleanup_count),
});
resource.hold(); resource.release(); resource.release();
assert_eq!(
cleanup_count.load(Ordering::SeqCst),
1,
"Cleanup called once"
);
resource.release(); assert_eq!(cleanup_count.load(Ordering::SeqCst), 1, "Still only once");
}
#[test]
fn test_concurrent_hold_release() {
let resource = Arc::new(ReferenceResourceCounter::new());
let num_threads = 10;
let iterations = 1000;
let mut handles = vec![];
for _ in 0..num_threads {
let res = Arc::clone(&resource);
handles.push(thread::spawn(move || {
for _ in 0..iterations {
if res.hold() {
thread::yield_now();
res.release();
}
}
}));
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(
resource.get_ref_count(),
1,
"Should return to initial state"
);
}
#[test]
fn test_concurrent_shutdown() {
let resource = Arc::new(ReferenceResourceCounter::new());
let num_threads = 5;
let barrier = Arc::new(Barrier::new(num_threads));
let mut handles = vec![];
for _ in 0..num_threads {
let res = Arc::clone(&resource);
let bar = Arc::clone(&barrier);
handles.push(thread::spawn(move || {
bar.wait();
res.shutdown(0);
}));
}
for handle in handles {
handle.join().unwrap();
}
assert!(!resource.is_available());
assert!(resource.get_ref_count() <= 0, "refCount should be <= 0");
assert!(resource.is_cleanup_over());
}
#[test]
fn test_hold_release_race_with_shutdown() {
let resource = Arc::new(ReferenceResourceCounter::new());
let res1 = Arc::clone(&resource);
let handle1 = thread::spawn(move || {
for _ in 0..500 {
if res1.hold() {
thread::yield_now();
res1.release();
}
}
});
let res2 = Arc::clone(&resource);
let handle2 = thread::spawn(move || {
thread::sleep(Duration::from_millis(10));
res2.shutdown(0);
});
handle1.join().unwrap();
handle2.join().unwrap();
assert!(!resource.is_available());
assert!(resource.get_ref_count() <= 0);
}
#[test]
fn test_is_cleanup_over_checks_both_conditions() {
let resource = ReferenceResourceCounter::new();
resource.hold();
assert!(!resource.is_cleanup_over());
resource.base.cleanup_over.store(true, Ordering::Release);
assert!(!resource.is_cleanup_over(), "refCount still > 0");
}
#[test]
fn test_zero_timeout_forces_immediately() {
let resource = ReferenceResourceCounter::new();
resource.hold(); resource.shutdown(0);
resource.shutdown(0);
let count = resource.get_ref_count();
assert!(count <= -1000, "Should force with 0 timeout, got {}", count);
}
}