mirror of https://github.com/telemt/telemt.git
Add stress testing for quota-lock and refactor test guard usage
This commit is contained in:
parent
c8632de5b6
commit
c1ee43fbac
|
|
@ -45,6 +45,18 @@ jobs:
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test --verbose
|
run: cargo test --verbose
|
||||||
|
|
||||||
|
- name: Stress quota-lock suites (PR only)
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
env:
|
||||||
|
RUST_TEST_THREADS: 16
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
for i in $(seq 1 12); do
|
||||||
|
echo "[quota-lock-stress] iteration ${i}/12"
|
||||||
|
cargo test quota_lock_ --bin telemt -- --nocapture --test-threads 16
|
||||||
|
cargo test relay_quota_wake --bin telemt -- --nocapture --test-threads 16
|
||||||
|
done
|
||||||
|
|
||||||
# clippy dont fail on warnings because of active development of telemt
|
# clippy dont fail on warnings because of active development of telemt
|
||||||
# and many warnings
|
# and many warnings
|
||||||
- name: Run clippy
|
- name: Run clippy
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,13 @@ fn quota_user_lock_test_guard() -> &'static Mutex<()> {
|
||||||
TEST_LOCK.get_or_init(|| Mutex::new(()))
|
TEST_LOCK.get_or_init(|| Mutex::new(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn quota_user_lock_test_scope() -> std::sync::MutexGuard<'static, ()> {
|
||||||
|
quota_user_lock_test_guard()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
fn quota_overflow_user_lock(user: &str) -> Arc<Mutex<()>> {
|
fn quota_overflow_user_lock(user: &str) -> Arc<Mutex<()>> {
|
||||||
let stripes = QUOTA_USER_OVERFLOW_LOCKS.get_or_init(|| {
|
let stripes = QUOTA_USER_OVERFLOW_LOCKS.get_or_init(|| {
|
||||||
(0..QUOTA_OVERFLOW_LOCK_STRIPES)
|
(0..QUOTA_OVERFLOW_LOCK_STRIPES)
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@ use tokio::time::Instant;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quota_lock_same_user_returns_same_arc_instance() {
|
fn quota_lock_same_user_returns_same_arc_instance() {
|
||||||
let _guard = super::quota_user_lock_test_guard()
|
let _guard = super::quota_user_lock_test_scope();
|
||||||
.lock()
|
|
||||||
.expect("quota lock test guard must be available");
|
|
||||||
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
||||||
map.clear();
|
map.clear();
|
||||||
|
|
||||||
|
|
@ -25,9 +23,7 @@ fn quota_lock_same_user_returns_same_arc_instance() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quota_lock_parallel_same_user_reuses_single_lock() {
|
fn quota_lock_parallel_same_user_reuses_single_lock() {
|
||||||
let _guard = super::quota_user_lock_test_guard()
|
let _guard = super::quota_user_lock_test_scope();
|
||||||
.lock()
|
|
||||||
.expect("quota lock test guard must be available");
|
|
||||||
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
||||||
map.clear();
|
map.clear();
|
||||||
|
|
||||||
|
|
@ -51,9 +47,7 @@ fn quota_lock_parallel_same_user_reuses_single_lock() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quota_lock_unique_users_materialize_distinct_entries() {
|
fn quota_lock_unique_users_materialize_distinct_entries() {
|
||||||
let _guard = super::quota_user_lock_test_guard()
|
let _guard = super::quota_user_lock_test_scope();
|
||||||
.lock()
|
|
||||||
.expect("quota lock test guard must be available");
|
|
||||||
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
||||||
|
|
||||||
map.clear();
|
map.clear();
|
||||||
|
|
@ -74,9 +68,7 @@ fn quota_lock_unique_users_materialize_distinct_entries() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quota_lock_unique_churn_stress_keeps_all_inserted_keys_addressable() {
|
fn quota_lock_unique_churn_stress_keeps_all_inserted_keys_addressable() {
|
||||||
let _guard = super::quota_user_lock_test_guard()
|
let _guard = super::quota_user_lock_test_scope();
|
||||||
.lock()
|
|
||||||
.expect("quota lock test guard must be available");
|
|
||||||
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
||||||
|
|
||||||
map.clear();
|
map.clear();
|
||||||
|
|
@ -94,9 +86,7 @@ fn quota_lock_unique_churn_stress_keeps_all_inserted_keys_addressable() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quota_lock_saturation_returns_stable_overflow_lock_without_cache_growth() {
|
fn quota_lock_saturation_returns_stable_overflow_lock_without_cache_growth() {
|
||||||
let _guard = super::quota_user_lock_test_guard()
|
let _guard = super::quota_user_lock_test_scope();
|
||||||
.lock()
|
|
||||||
.expect("quota lock test guard must be available");
|
|
||||||
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
||||||
map.clear();
|
map.clear();
|
||||||
|
|
||||||
|
|
@ -135,17 +125,19 @@ fn quota_lock_saturation_returns_stable_overflow_lock_without_cache_growth() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quota_lock_reclaims_unreferenced_entries_before_ephemeral_fallback() {
|
fn quota_lock_reclaims_unreferenced_entries_before_ephemeral_fallback() {
|
||||||
let _guard = super::quota_user_lock_test_guard()
|
let _guard = super::quota_user_lock_test_scope();
|
||||||
.lock()
|
|
||||||
.expect("quota lock test guard must be available");
|
|
||||||
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
||||||
map.clear();
|
map.clear();
|
||||||
|
|
||||||
// Fill and immediately drop strong references, leaving only map-owned Arcs.
|
// Saturate with retained strong references first so parallel tests cannot
|
||||||
|
// reclaim our fixture entries before we validate the reclaim path.
|
||||||
|
let prefix = format!("quota-reclaim-drop-{}", std::process::id());
|
||||||
|
let mut retained = Vec::with_capacity(QUOTA_USER_LOCKS_MAX);
|
||||||
for idx in 0..QUOTA_USER_LOCKS_MAX {
|
for idx in 0..QUOTA_USER_LOCKS_MAX {
|
||||||
let _ = quota_user_lock(&format!("quota-reclaim-drop-{}-{idx}", std::process::id()));
|
retained.push(quota_user_lock(&format!("{prefix}-{idx}")));
|
||||||
}
|
}
|
||||||
assert_eq!(map.len(), QUOTA_USER_LOCKS_MAX);
|
|
||||||
|
drop(retained);
|
||||||
|
|
||||||
let overflow_user = format!("quota-reclaim-overflow-{}", std::process::id());
|
let overflow_user = format!("quota-reclaim-overflow-{}", std::process::id());
|
||||||
let overflow = quota_user_lock(&overflow_user);
|
let overflow = quota_user_lock(&overflow_user);
|
||||||
|
|
@ -162,9 +154,7 @@ fn quota_lock_reclaims_unreferenced_entries_before_ephemeral_fallback() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quota_lock_saturated_same_user_must_not_return_distinct_locks() {
|
fn quota_lock_saturated_same_user_must_not_return_distinct_locks() {
|
||||||
let _guard = super::quota_user_lock_test_guard()
|
let _guard = super::quota_user_lock_test_scope();
|
||||||
.lock()
|
|
||||||
.expect("quota lock test guard must be available");
|
|
||||||
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
||||||
map.clear();
|
map.clear();
|
||||||
|
|
||||||
|
|
@ -187,9 +177,7 @@ fn quota_lock_saturated_same_user_must_not_return_distinct_locks() {
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||||
async fn quota_lock_saturation_concurrent_same_user_never_overshoots_quota() {
|
async fn quota_lock_saturation_concurrent_same_user_never_overshoots_quota() {
|
||||||
let _guard = super::quota_user_lock_test_guard()
|
let _guard = super::quota_user_lock_test_scope();
|
||||||
.lock()
|
|
||||||
.expect("quota lock test guard must be available");
|
|
||||||
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
||||||
map.clear();
|
map.clear();
|
||||||
|
|
||||||
|
|
@ -240,9 +228,7 @@ async fn quota_lock_saturation_concurrent_same_user_never_overshoots_quota() {
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||||
async fn quota_lock_saturation_stress_same_user_never_overshoots_quota() {
|
async fn quota_lock_saturation_stress_same_user_never_overshoots_quota() {
|
||||||
let _guard = super::quota_user_lock_test_guard()
|
let _guard = super::quota_user_lock_test_scope();
|
||||||
.lock()
|
|
||||||
.expect("quota lock test guard must be available");
|
|
||||||
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
||||||
map.clear();
|
map.clear();
|
||||||
|
|
||||||
|
|
@ -322,6 +308,24 @@ fn quota_error_classifier_rejects_plain_permission_denied() {
|
||||||
assert!(!is_quota_io_error(&err));
|
assert!(!is_quota_io_error(&err));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quota_lock_test_scope_recovers_after_guard_poison() {
|
||||||
|
let poison_result = std::thread::spawn(|| {
|
||||||
|
let _guard = super::quota_user_lock_test_scope();
|
||||||
|
panic!("intentional test-only guard poison");
|
||||||
|
})
|
||||||
|
.join();
|
||||||
|
assert!(poison_result.is_err(), "poison setup thread must panic");
|
||||||
|
|
||||||
|
let _guard = super::quota_user_lock_test_scope();
|
||||||
|
let map = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
||||||
|
map.clear();
|
||||||
|
|
||||||
|
let a = quota_user_lock("quota-lock-poison-recovery-user");
|
||||||
|
let b = quota_user_lock("quota-lock-poison-recovery-user");
|
||||||
|
assert!(Arc::ptr_eq(&a, &b));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn quota_lock_integration_zero_quota_cuts_off_without_forwarding() {
|
async fn quota_lock_integration_zero_quota_cuts_off_without_forwarding() {
|
||||||
let stats = Arc::new(Stats::new());
|
let stats = Arc::new(Stats::new());
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,8 @@ fn saturate_lock_cache() -> Vec<Arc<std::sync::Mutex<()>>> {
|
||||||
retained
|
retained
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quota_test_guard() -> std::sync::MutexGuard<'static, ()> {
|
fn quota_test_guard() -> impl Drop {
|
||||||
super::quota_user_lock_test_guard()
|
super::quota_user_lock_test_scope()
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,8 @@ impl std::task::Wake for WakeCounter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quota_test_guard() -> std::sync::MutexGuard<'static, ()> {
|
fn quota_test_guard() -> impl Drop {
|
||||||
super::quota_user_lock_test_guard()
|
super::quota_user_lock_test_scope()
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn saturate_quota_user_locks() -> Vec<Arc<std::sync::Mutex<()>>> {
|
fn saturate_quota_user_locks() -> Vec<Arc<std::sync::Mutex<()>>> {
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,7 @@ impl std::task::Wake for WakeCounter {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn quota_lock_contention_does_not_self_wake_pending_writer() {
|
async fn quota_lock_contention_does_not_self_wake_pending_writer() {
|
||||||
let _guard = super::quota_user_lock_test_guard()
|
let _guard = super::quota_user_lock_test_scope();
|
||||||
.lock()
|
|
||||||
.expect("quota lock test guard must be available");
|
|
||||||
let map = super::QUOTA_USER_LOCKS.get_or_init(dashmap::DashMap::new);
|
let map = super::QUOTA_USER_LOCKS.get_or_init(dashmap::DashMap::new);
|
||||||
map.clear();
|
map.clear();
|
||||||
|
|
||||||
|
|
@ -72,9 +70,7 @@ async fn quota_lock_contention_does_not_self_wake_pending_writer() {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn quota_lock_contention_writer_schedules_single_deferred_wake_until_lock_acquired() {
|
async fn quota_lock_contention_writer_schedules_single_deferred_wake_until_lock_acquired() {
|
||||||
let _guard = super::quota_user_lock_test_guard()
|
let _guard = super::quota_user_lock_test_scope();
|
||||||
.lock()
|
|
||||||
.expect("quota lock test guard must be available");
|
|
||||||
let map = super::QUOTA_USER_LOCKS.get_or_init(dashmap::DashMap::new);
|
let map = super::QUOTA_USER_LOCKS.get_or_init(dashmap::DashMap::new);
|
||||||
map.clear();
|
map.clear();
|
||||||
|
|
||||||
|
|
@ -145,9 +141,7 @@ async fn quota_lock_contention_writer_schedules_single_deferred_wake_until_lock_
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn quota_lock_contention_read_path_schedules_deferred_wake_for_liveness() {
|
async fn quota_lock_contention_read_path_schedules_deferred_wake_for_liveness() {
|
||||||
let _guard = super::quota_user_lock_test_guard()
|
let _guard = super::quota_user_lock_test_scope();
|
||||||
.lock()
|
|
||||||
.expect("quota lock test guard must be available");
|
|
||||||
let map = super::QUOTA_USER_LOCKS.get_or_init(dashmap::DashMap::new);
|
let map = super::QUOTA_USER_LOCKS.get_or_init(dashmap::DashMap::new);
|
||||||
map.clear();
|
map.clear();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue