use super::bounded::{abandoned_worker_count_for_tests, bounded_flush_with, bounded_teardown_with};
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
fn blocked_until(released: &Arc<AtomicBool>) -> impl FnOnce() -> bool + Send + 'static {
blocked_until_for(released, Duration::from_millis(400))
}
fn blocked_until_for(
released: &Arc<AtomicBool>,
cap: Duration,
) -> impl FnOnce() -> bool + Send + 'static {
let released = Arc::clone(released);
move || {
let started = Instant::now();
while !released.load(Ordering::Acquire) && started.elapsed() < cap {
std::thread::sleep(Duration::from_millis(5));
}
true
}
}
#[test]
fn the_blocked_until_helper_returns_promptly_once_released() {
let released = Arc::new(AtomicBool::new(true));
let job = blocked_until_for(&released, Duration::from_secs(5));
let started = Instant::now();
assert!(job());
assert!(
started.elapsed() < Duration::from_secs(1),
"a released worker must not wait out its cap"
);
}
#[test]
fn resetting_the_abandoned_worker_budget_clears_it() {
let _guard = crate::testing::acquire_test_state_lock();
_reset_abandoned_workers_for_tests();
let released = Arc::new(AtomicBool::new(false));
assert_eq!(
bounded_flush(
"logs",
Some(0.01),
blocked_until_for(&released, Duration::from_secs(3))
),
DrainOutcome::TimedOut
);
assert_eq!(abandoned_worker_count_for_tests(), 1);
_reset_abandoned_workers_for_tests();
assert_eq!(
abandoned_worker_count_for_tests(),
0,
"reset must clear the stranded-worker budget"
);
released.store(true, Ordering::Release);
}
#[test]
fn a_failed_drain_is_reported_as_failure() {
let _guard = crate::testing::acquire_test_state_lock();
assert_eq!(
bounded_flush("traces", None, || false),
DrainOutcome::Failed
);
}
#[test]
fn a_successful_drain_is_reported_as_success() {
let _guard = crate::testing::acquire_test_state_lock();
assert_eq!(
bounded_flush("traces", None, || true),
DrainOutcome::Drained
);
}
#[test]
fn a_drain_abandoned_at_the_deadline_is_reported_as_failure() {
use crate::config::TelemetryConfig;
use crate::testing::acquire_test_state_lock;
let _guard = acquire_test_state_lock();
_reset_abandoned_workers_for_tests();
let mut cfg = TelemetryConfig::default();
cfg.exporter.logs_shutdown_timeout_seconds = 0.05;
crate::runtime::set_active_config(Some(cfg));
let released = Arc::new(AtomicBool::new(false));
assert_eq!(
bounded_flush("metrics", None, blocked_until(&released)),
DrainOutcome::TimedOut
);
assert_eq!(
abandoned_worker_count_for_tests(),
1,
"an abandoned drain worker must charge a budget slot"
);
released.store(true, Ordering::Release);
let started = Instant::now();
while abandoned_worker_count_for_tests() > 0 && started.elapsed() < Duration::from_secs(2) {
std::thread::sleep(Duration::from_millis(5));
}
assert_eq!(
abandoned_worker_count_for_tests(),
0,
"a stranded worker must release its slot when it finally exits"
);
crate::runtime::set_active_config(None);
}
#[test]
fn a_zero_configured_deadline_abandons_the_drain_immediately() {
use crate::config::TelemetryConfig;
use crate::testing::acquire_test_state_lock;
let _guard = acquire_test_state_lock();
_reset_abandoned_workers_for_tests();
let mut cfg = TelemetryConfig::default();
cfg.exporter.logs_shutdown_timeout_seconds = 0.0;
crate::runtime::set_active_config(Some(cfg));
let released = Arc::new(AtomicBool::new(false));
let started = Instant::now();
assert_eq!(
bounded_flush("logs", None, blocked_until(&released)),
DrainOutcome::TimedOut
);
assert!(
started.elapsed() < Duration::from_millis(200),
"a zero budget must not wait for the drain"
);
released.store(true, Ordering::Release);
crate::runtime::set_active_config(None);
_reset_abandoned_workers_for_tests();
}
#[test]
fn a_non_positive_caller_deadline_abandons_the_drain_immediately() {
use crate::testing::acquire_test_state_lock;
let _guard = acquire_test_state_lock();
_reset_abandoned_workers_for_tests();
for deadline in [0.0, -1.0] {
let released = Arc::new(AtomicBool::new(false));
let started = Instant::now();
assert_eq!(
bounded_flush("traces", Some(deadline), blocked_until(&released)),
DrainOutcome::TimedOut,
"flush({deadline}) must report the drain abandoned"
);
assert!(
started.elapsed() < Duration::from_millis(200),
"flush({deadline}) must not block on the drain"
);
released.store(true, Ordering::Release);
}
_reset_abandoned_workers_for_tests();
}
#[test]
fn a_small_positive_deadline_still_drains() {
let _guard = crate::testing::acquire_test_state_lock();
assert_eq!(
bounded_flush("logs", Some(0.5), || true),
DrainOutcome::Drained
);
}
#[test]
fn bounded_flush_survives_a_non_finite_caller_timeout() {
let _guard = crate::testing::acquire_test_state_lock();
assert_eq!(
bounded_flush("logs", Some(f64::NAN), || true),
DrainOutcome::Drained
);
assert_eq!(
bounded_flush("traces", Some(f64::INFINITY), || true),
DrainOutcome::Drained
);
assert_eq!(
bounded_flush("metrics", Some(f64::MAX), || false),
DrainOutcome::Failed
);
}
#[test]
fn a_failed_spawn_falls_back_to_an_inline_drain() {
use crate::testing::acquire_test_state_lock;
let _guard = acquire_test_state_lock();
let failing_spawn = |_name: String, _job: Box<dyn FnOnce() + Send + 'static>| {
Err(std::io::Error::other("thread limit reached"))
};
assert_eq!(
bounded_flush_with("logs", Some(5.0), || true, failing_spawn),
DrainOutcome::Drained
);
assert_eq!(
bounded_flush_with("logs", Some(5.0), || false, failing_spawn),
DrainOutcome::Failed
);
}
#[test]
fn a_failed_teardown_spawn_tears_down_inline() {
use crate::testing::acquire_test_state_lock;
let _guard = acquire_test_state_lock();
let caller = std::thread::current().id();
let (tx, rx) = std::sync::mpsc::channel();
bounded_teardown_with(
"logs",
Some(5.0),
move || {
let _ = tx.send(std::thread::current().id());
},
|_name, _job| Err(std::io::Error::other("thread limit reached")),
);
assert_eq!(rx.try_recv(), Ok(caller));
}
#[test]
fn a_saturated_budget_declines_flush_but_never_teardown() {
use crate::testing::acquire_test_state_lock;
let _guard = acquire_test_state_lock();
_reset_abandoned_workers_for_tests();
let released = Arc::new(AtomicBool::new(false));
for n in 0..8 {
assert_eq!(
bounded_flush(
"logs",
Some(0.01),
blocked_until_for(&released, Duration::from_secs(3))
),
DrainOutcome::TimedOut,
"stranding flush {n} must report an abandoned drain"
);
}
assert_eq!(abandoned_worker_count_for_tests(), 8);
let ran = Arc::new(AtomicBool::new(false));
let ran_flag = Arc::clone(&ran);
assert_eq!(
bounded_flush("logs", Some(5.0), move || {
ran_flag.store(true, Ordering::Release);
true
}),
DrainOutcome::TimedOut
);
assert!(
!ran.load(Ordering::Acquire),
"a declined flush must not strand another worker"
);
let done = Arc::new(AtomicBool::new(false));
let done_flag = Arc::clone(&done);
bounded_teardown("logs", Some(5.0), move || {
done_flag.store(true, Ordering::Release);
});
assert!(
done.load(Ordering::Acquire),
"teardown must proceed even with the budget saturated"
);
released.store(true, Ordering::Release);
_reset_abandoned_workers_for_tests();
}
#[test]
fn a_teardown_abandoned_at_the_deadline_returns_to_the_caller() {
use crate::testing::acquire_test_state_lock;
let _guard = acquire_test_state_lock();
_reset_abandoned_workers_for_tests();
let (release_tx, release_rx) = std::sync::mpsc::channel();
let started = Instant::now();
bounded_teardown("traces", Some(0.05), move || {
let _ = release_rx.recv_timeout(Duration::from_secs(2));
});
let elapsed = started.elapsed();
let _ = release_tx.send(());
assert!(
elapsed < Duration::from_secs(1),
"teardown ran {elapsed:?} past a 0.05s deadline"
);
assert_eq!(
abandoned_worker_count_for_tests(),
1,
"an abandoned teardown worker still charges the shared budget"
);
_reset_abandoned_workers_for_tests();
}
#[test]
fn a_teardown_that_finishes_in_time_is_waited_for() {
use crate::testing::acquire_test_state_lock;
let _guard = acquire_test_state_lock();
let done = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&done);
bounded_teardown("logs", Some(5.0), move || {
flag.store(true, Ordering::SeqCst);
});
assert!(done.load(Ordering::SeqCst));
}
#[test]
fn an_unbounded_teardown_runs_inline_and_a_zero_budget_does_not_wait() {
use crate::testing::acquire_test_state_lock;
let _guard = acquire_test_state_lock();
_reset_abandoned_workers_for_tests();
let caller = std::thread::current().id();
let (tx, rx) = std::sync::mpsc::channel();
for timeout in [f64::NAN, f64::INFINITY] {
let tx = tx.clone();
bounded_teardown("metrics", Some(timeout), move || {
let _ = tx.send(std::thread::current().id());
});
}
drop(tx);
let ran_on: Vec<_> = rx.iter().collect();
assert_eq!(ran_on, vec![caller; 2]);
let started = Instant::now();
let released = Arc::new(AtomicBool::new(false));
let blocked = blocked_until(&released);
bounded_teardown("metrics", Some(0.0), move || {
let _ = blocked();
});
assert!(
started.elapsed() < Duration::from_millis(200),
"a zero-budget teardown must not wait for the worker"
);
released.store(true, Ordering::Release);
_reset_abandoned_workers_for_tests();
}
#[test]
fn stalled_drains_run_together_share_one_deadline() {
use crate::config::TelemetryConfig;
use crate::testing::acquire_test_state_lock;
let _guard = acquire_test_state_lock();
_reset_abandoned_workers_for_tests();
let mut cfg = TelemetryConfig::default();
cfg.exporter.logs_shutdown_timeout_seconds = 0.1;
crate::runtime::set_active_config(Some(cfg));
let released = Arc::new(AtomicBool::new(false));
let started = Instant::now();
std::thread::scope(|scope| {
let logs = scope.spawn(|| bounded_flush("logs", None, blocked_until(&released)));
let traces = scope.spawn(|| bounded_flush("traces", None, blocked_until(&released)));
let metrics = bounded_flush("metrics", None, blocked_until(&released));
assert_eq!(logs.join().expect("logs worker"), DrainOutcome::TimedOut);
assert_eq!(
traces.join().expect("traces worker"),
DrainOutcome::TimedOut
);
assert_eq!(metrics, DrainOutcome::TimedOut);
});
let elapsed = started.elapsed();
released.store(true, Ordering::Release);
assert!(
elapsed < Duration::from_millis(300),
"three stalled drains took {elapsed:?}, close to the sequential 0.3s"
);
crate::runtime::set_active_config(None);
_reset_abandoned_workers_for_tests();
}
#[test]
fn join_or_inline_joins_a_worker_and_falls_back_inline_on_spawn_failure() {
std::thread::scope(|scope| {
let spawned = std::thread::Builder::new()
.name("provide-test-drain".to_string())
.spawn_scoped(scope, || 7);
assert_eq!(super::bounded::join_or_inline(spawned, || 0), 7);
let failed: std::io::Result<std::thread::ScopedJoinHandle<'_, i32>> =
Err(std::io::Error::other("thread limit reached"));
assert_eq!(super::bounded::join_or_inline(failed, || 42), 42);
});
}