Skip to main content

agent_tui_common/common/
thread_join.rs

1//! Bounded thread join helpers.
2
3use std::sync::Arc;
4use std::sync::Mutex;
5use std::thread;
6use std::time::Duration;
7use std::time::Instant;
8
9use tracing::warn;
10
11const DEFAULT_JOIN_POLL_INTERVAL: Duration = Duration::from_millis(20);
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ThreadJoinOutcome {
15    Joined,
16    ReapingInBackground,
17}
18
19pub fn join_thread_with_timeout_or_reap(
20    handle: thread::JoinHandle<()>,
21    timeout: Duration,
22    thread_label: &'static str,
23    reaper_name: &'static str,
24) -> ThreadJoinOutcome {
25    join_thread_with_timeout_or_reap_with_poll_interval(
26        handle,
27        timeout,
28        DEFAULT_JOIN_POLL_INTERVAL,
29        thread_label,
30        reaper_name,
31    )
32}
33
34pub fn join_thread_with_timeout_or_reap_with_poll_interval(
35    handle: thread::JoinHandle<()>,
36    timeout: Duration,
37    poll_interval: Duration,
38    thread_label: &'static str,
39    reaper_name: &'static str,
40) -> ThreadJoinOutcome {
41    let deadline = Instant::now() + timeout;
42    while !handle.is_finished() {
43        if Instant::now() >= deadline {
44            warn!(
45                thread = thread_label,
46                timeout_ms = timeout.as_millis(),
47                "Timed out joining thread; handing ownership to background reaper"
48            );
49            return spawn_join_reaper(handle, thread_label, reaper_name);
50        }
51        thread::park_timeout(poll_interval);
52    }
53    let _ = handle.join();
54    ThreadJoinOutcome::Joined
55}
56
57fn spawn_join_reaper(
58    handle: thread::JoinHandle<()>,
59    thread_label: &'static str,
60    reaper_name: &'static str,
61) -> ThreadJoinOutcome {
62    let handle_cell = Arc::new(Mutex::new(Some(handle)));
63    let handle_for_thread = Arc::clone(&handle_cell);
64    match thread::Builder::new()
65        .name(reaper_name.to_string())
66        .spawn(move || {
67            let Some(handle) = handle_for_thread
68                .lock()
69                .unwrap_or_else(std::sync::PoisonError::into_inner)
70                .take()
71            else {
72                return;
73            };
74
75            if handle.join().is_err() {
76                warn!(
77                    thread = thread_label,
78                    "Background reaper observed thread panic"
79                );
80            }
81        }) {
82        Ok(_) => ThreadJoinOutcome::ReapingInBackground,
83        Err(err) => {
84            warn!(
85                thread = thread_label,
86                reaper = reaper_name,
87                error = %err,
88                "Failed to spawn background join reaper; joining synchronously"
89            );
90            if let Some(handle) = handle_cell
91                .lock()
92                .unwrap_or_else(std::sync::PoisonError::into_inner)
93                .take()
94            {
95                let _ = handle.join();
96            }
97            ThreadJoinOutcome::Joined
98        }
99    }
100}
101
102#[cfg(test)]
103#[path = "thread_join_tests.rs"]
104mod tests;