agent_tui_common/common/
thread_join.rs1use 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;