1
//! Keeps a job's log stream alive across long silent phases.
2
//!
3
//! Forgejo cancels a task whose runner reports nothing for `ZOMBIE_TASK_TIMEOUT`
4
//! (about 10 minutes). Phases here are legitimately quiet for longer: an LTO
5
//! link printed nothing for ~11 minutes and had its task cancelled *after* the
6
//! build had already succeeded, and an instrumented test run is ~24 minutes of
7
//! near-silence.
8
//!
9
//! In-process, unlike the shell wrapper it replaces: the thing that knows which
10
//! phase is running is the thing running it, so the pulse can name the phase
11
//! without a file passed between two scripts.
12

            
13
use std::sync::atomic::{AtomicBool, Ordering};
14
use std::sync::{Arc, Mutex};
15
use std::time::Duration;
16

            
17
const PULSE: Duration = Duration::from_secs(60);
18

            
19
/// Announces phases and pulses between them. Stops on drop.
20
pub struct Heartbeat {
21
    phase: Arc<Mutex<String>>,
22
    stop: Arc<AtomicBool>,
23
    handle: Option<std::thread::JoinHandle<()>>,
24
}
25

            
26
impl Heartbeat {
27
    pub fn start() -> Self {
28
        let phase = Arc::new(Mutex::new(String::from("starting")));
29
        let stop = Arc::new(AtomicBool::new(false));
30

            
31
        let ticker_phase = Arc::clone(&phase);
32
        let ticker_stop = Arc::clone(&stop);
33
        let handle = std::thread::spawn(move || {
34
            // Polled in short slices rather than one long sleep so teardown does
35
            // not wait out a whole minute.
36
            let mut waited = Duration::ZERO;
37
            while !ticker_stop.load(Ordering::Relaxed) {
38
                std::thread::sleep(Duration::from_millis(200));
39
                waited += Duration::from_millis(200);
40
                if waited >= PULSE {
41
                    waited = Duration::ZERO;
42
                    let current = ticker_phase
43
                        .lock()
44
                        .map(|p| p.clone())
45
                        .unwrap_or_else(|_| "unknown".into());
46
                    println!(">> …still in: {current}");
47
                }
48
            }
49
        });
50

            
51
        Self {
52
            phase,
53
            stop,
54
            handle: Some(handle),
55
        }
56
    }
57

            
58
    /// Names the phase now running, and says so in the log.
59
    pub fn phase(&self, name: &str) {
60
        println!(">> {name}");
61
        if let Ok(mut phase) = self.phase.lock() {
62
            *phase = name.to_owned();
63
        }
64
    }
65
}
66

            
67
impl Drop for Heartbeat {
68
    fn drop(&mut self) {
69
        self.stop.store(true, Ordering::Relaxed);
70
        if let Some(handle) = self.handle.take() {
71
            let _ = handle.join();
72
        }
73
    }
74
}