use std::io;
use std::sync::{mpsc, Arc, Mutex};
use std::time::Duration;
use super::DrainOutcome;
pub(crate) const MAX_DRAIN_SECONDS: f64 = 86_400.0;
const MAX_ABANDONED_WORKERS: u32 = 8;
static ABANDONED_WORKERS: Mutex<u32> = Mutex::new(0);
struct DrainAccounting {
finished: bool,
counted: bool,
}
fn abandoned_worker_count() -> u32 {
*crate::_lock::lock(&ABANDONED_WORKERS)
}
fn drain_budget_saturated(count: u32) -> bool {
count >= MAX_ABANDONED_WORKERS
}
pub(crate) fn _reset_abandoned_workers_for_tests() {
*crate::_lock::lock(&ABANDONED_WORKERS) = 0;
}
#[cfg(test)]
pub(crate) fn abandoned_worker_count_for_tests() -> u32 {
abandoned_worker_count()
}
fn note_worker_finished(acct: &Mutex<DrainAccounting>) {
let mut state = crate::_lock::lock(acct);
state.finished = true;
if state.counted {
let mut count = crate::_lock::lock(&ABANDONED_WORKERS);
*count = count.saturating_sub(1);
}
}
fn note_worker_abandoned(acct: &Mutex<DrainAccounting>) {
let mut state = crate::_lock::lock(acct);
if !state.finished {
state.counted = true;
*crate::_lock::lock(&ABANDONED_WORKERS) += 1;
}
}
pub(crate) fn drain_deadline(timeout_secs: f64) -> Option<Duration> {
if timeout_secs.is_nan() || timeout_secs == f64::INFINITY {
return None;
}
if timeout_secs <= 0.0 {
return Some(Duration::ZERO);
}
Some(Duration::from_secs_f64(timeout_secs.min(MAX_DRAIN_SECONDS)))
}
fn configured_drain_seconds() -> f64 {
crate::runtime::get_runtime_config()
.map(|cfg| cfg.exporter.logs_shutdown_timeout_seconds)
.unwrap_or(5.0)
}
pub(crate) type SpawnResult = io::Result<std::thread::JoinHandle<()>>;
fn spawn_worker(name: String, job: Box<dyn FnOnce() + Send + 'static>) -> SpawnResult {
std::thread::Builder::new().name(name).spawn(job)
}
fn run_leftover_job<T>(job: &Mutex<Option<impl FnOnce() -> T>>) -> Option<T> {
crate::_lock::lock(job).take().map(|job| job())
}
pub(crate) fn join_or_inline<T>(
spawned: io::Result<std::thread::ScopedJoinHandle<'_, T>>,
inline: impl FnOnce() -> T,
) -> T {
match spawned {
Ok(handle) => handle.join().expect("drain worker must not panic"),
Err(_) => inline(),
}
}
pub(crate) fn bounded_teardown<F>(signal: &str, timeout_seconds: Option<f64>, teardown: F)
where
F: FnOnce() + Send + 'static,
{
bounded_teardown_with(signal, timeout_seconds, teardown, spawn_worker);
}
pub(crate) fn bounded_teardown_with<F, S>(
signal: &str,
timeout_seconds: Option<f64>,
teardown: F,
spawn: S,
) where
F: FnOnce() + Send + 'static,
S: FnOnce(String, Box<dyn FnOnce() + Send + 'static>) -> SpawnResult,
{
let timeout_secs = timeout_seconds.unwrap_or_else(configured_drain_seconds);
let Some(timeout) = drain_deadline(timeout_secs) else {
teardown();
return;
};
let (tx, rx) = mpsc::channel();
let acct = Arc::new(Mutex::new(DrainAccounting {
finished: false,
counted: false,
}));
let job = Arc::new(Mutex::new(Some(teardown)));
let worker_acct = Arc::clone(&acct);
let worker_job = Arc::clone(&job);
let spawned = spawn(
format!("provide-{signal}-shutdown"),
Box::new(move || {
if let Some(teardown) = crate::_lock::lock(&worker_job).take() {
teardown();
let _ = tx.send(());
}
note_worker_finished(&worker_acct);
}),
);
if spawned.is_err() {
eprintln!(
"provide_telemetry: {signal} shutdown worker could not be spawned; tearing down inline without a deadline",
);
run_leftover_job(&job).expect("teardown job is present: the failed spawn never ran it");
return;
}
if rx.recv_timeout(timeout).is_err() {
note_worker_abandoned(&acct);
eprintln!(
"provide_telemetry: {signal} shutdown exceeded {:.3}s deadline; abandoning background flush",
timeout.as_secs_f64(),
);
}
}
pub(crate) fn bounded_flush<F>(signal: &str, timeout_seconds: Option<f64>, flush: F) -> DrainOutcome
where
F: FnOnce() -> bool + Send + 'static,
{
bounded_flush_with(signal, timeout_seconds, flush, spawn_worker)
}
fn completed_drain_outcome(exported: bool) -> DrainOutcome {
if exported {
DrainOutcome::Drained
} else {
DrainOutcome::Failed
}
}
pub(crate) fn bounded_flush_with<F, S>(
signal: &str,
timeout_seconds: Option<f64>,
flush: F,
spawn: S,
) -> DrainOutcome
where
F: FnOnce() -> bool + Send + 'static,
S: FnOnce(String, Box<dyn FnOnce() + Send + 'static>) -> SpawnResult,
{
let timeout_secs = timeout_seconds.unwrap_or_else(configured_drain_seconds);
let Some(timeout) = drain_deadline(timeout_secs) else {
return completed_drain_outcome(flush());
};
if drain_budget_saturated(abandoned_worker_count()) {
eprintln!(
"provide_telemetry: {signal} flush skipped: {MAX_ABANDONED_WORKERS} earlier drain workers are still pending against an unresponsive exporter",
);
return DrainOutcome::TimedOut;
}
let (tx, rx) = mpsc::channel();
let acct = Arc::new(Mutex::new(DrainAccounting {
finished: false,
counted: false,
}));
let job = Arc::new(Mutex::new(Some(flush)));
let worker_acct = Arc::clone(&acct);
let worker_job = Arc::clone(&job);
let spawned = spawn(
format!("provide-{signal}-flush"),
Box::new(move || {
if let Some(flush) = crate::_lock::lock(&worker_job).take() {
let _ = tx.send(flush());
}
note_worker_finished(&worker_acct);
}),
);
if spawned.is_err() {
eprintln!(
"provide_telemetry: {signal} flush worker could not be spawned; draining inline without a deadline",
);
return completed_drain_outcome(
run_leftover_job(&job).expect("flush job is present: the failed spawn never ran it"),
);
}
match rx.recv_timeout(timeout) {
Ok(true) => DrainOutcome::Drained,
Ok(false) => {
eprintln!("provide_telemetry: {signal} flush failed");
DrainOutcome::Failed
}
Err(_) => {
note_worker_abandoned(&acct);
eprintln!(
"provide_telemetry: {signal} flush exceeded {:.3}s deadline; abandoning background flush",
timeout.as_secs_f64(),
);
DrainOutcome::TimedOut
}
}
}
#[cfg(test)]
mod accounting_tests {
use super::*;
use crate::testing::acquire_test_state_lock;
#[test]
fn the_budget_saturates_exactly_at_the_cap() {
assert!(!drain_budget_saturated(MAX_ABANDONED_WORKERS - 1));
assert!(drain_budget_saturated(MAX_ABANDONED_WORKERS));
assert!(drain_budget_saturated(MAX_ABANDONED_WORKERS + 1));
}
#[test]
fn a_slot_is_charged_only_when_the_worker_had_not_finished() {
let _guard = acquire_test_state_lock();
_reset_abandoned_workers_for_tests();
let acct = Mutex::new(DrainAccounting {
finished: false,
counted: false,
});
note_worker_abandoned(&acct);
assert_eq!(abandoned_worker_count(), 1, "abandon charges a slot");
note_worker_finished(&acct);
assert_eq!(abandoned_worker_count(), 0, "late finish releases it");
let acct = Mutex::new(DrainAccounting {
finished: false,
counted: false,
});
note_worker_finished(&acct);
note_worker_abandoned(&acct);
assert_eq!(
abandoned_worker_count(),
0,
"a worker that finished first is never charged"
);
_reset_abandoned_workers_for_tests();
}
#[test]
fn releasing_a_slot_saturates_at_zero() {
let _guard = acquire_test_state_lock();
_reset_abandoned_workers_for_tests();
let acct = Mutex::new(DrainAccounting {
finished: false,
counted: true,
});
note_worker_finished(&acct);
assert_eq!(abandoned_worker_count(), 0);
}
#[test]
fn a_leftover_job_runs_once_and_only_once() {
let job = Mutex::new(Some(|| 7));
assert_eq!(run_leftover_job(&job), Some(7));
assert_eq!(run_leftover_job(&job), None);
}
}