Skip to main content

dial9_core/
recording.rs

1use crate::buffer::{BufferMode, SegmentWriter};
2use crate::flush_loop::run_flush_loop;
3use crate::handle::{ControlCommand, Dial9Handle, InstallGlobalHandleError};
4use crate::primitives::sync::{Arc, Mutex};
5use crate::primitives::{sync::mpsc, thread::JoinHandle};
6use crate::recorder::SoleRecorderGuard;
7use crate::shared_state::SharedState;
8use std::time::Duration;
9
10/// The background worker thread and its stop signal.
11///
12/// Present only when a segment-processing pipeline is configured.
13#[cfg(feature = "pipeline")]
14pub(crate) struct WorkerHandle {
15    shutdown: Option<tokio::sync::oneshot::Sender<Duration>>,
16    thread: Option<JoinHandle<()>>,
17}
18
19#[cfg(feature = "pipeline")]
20impl WorkerHandle {
21    /// Wrap the worker's shutdown sender and join handle.
22    pub(crate) fn new(
23        shutdown: tokio::sync::oneshot::Sender<Duration>,
24        thread: JoinHandle<()>,
25    ) -> Self {
26        Self {
27            shutdown: Some(shutdown),
28            thread: Some(thread),
29        }
30    }
31}
32
33/// Owns the recording state: the [`Dial9Handle`], the flush thread, and (with
34/// the `pipeline` feature) the background worker.
35///
36/// This is an RAII guard: dropping it flushes remaining events, seals the final
37/// segment, and stops the worker. For a bounded drain of the background worker
38/// (symbolize, compress, upload) call [`graceful_shutdown`](Self::graceful_shutdown)
39/// instead.
40pub struct Recorder {
41    handle: Dial9Handle,
42    flush_thread: Option<JoinHandle<()>>,
43    /// Hooks run once, with the handle, on the first `enable()`.
44    recording_start_hooks: Mutex<Vec<RecordingStartHook>>,
45    /// Held while this is the process's recorder. Dropping it frees the slot.
46    sole_recorder: Option<SoleRecorderGuard>,
47    #[cfg(feature = "pipeline")]
48    worker: Option<WorkerHandle>,
49}
50
51impl std::fmt::Debug for Recorder {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        f.debug_struct("Recorder")
54            .field("enabled", &self.handle.is_enabled())
55            .field("recording", &self.flush_thread.is_some())
56            .finish_non_exhaustive()
57    }
58}
59
60/// A hook run once, with the live [`Dial9Handle`], when the recorder first
61/// enables recording.
62pub type RecordingStartHook = Box<dyn FnOnce(&Dial9Handle) + Send>;
63
64impl Recorder {
65    /// Create a recorder from an existing handle and flush thread.
66    pub(crate) fn new(handle: Dial9Handle, flush_thread: Option<JoinHandle<()>>) -> Self {
67        Self {
68            handle,
69            flush_thread,
70            recording_start_hooks: Mutex::new(Vec::new()),
71            sole_recorder: None,
72            #[cfg(feature = "pipeline")]
73            worker: None,
74        }
75    }
76
77    /// Hold the process's recorder slot for this recorder's lifetime.
78    pub(crate) fn hold_process(&mut self, guard: crate::recorder::SoleRecorderGuard) {
79        self.sole_recorder = Some(guard);
80    }
81
82    /// Install the one-shot hooks to run on the first `enable()`.
83    pub(crate) fn set_recording_start_hooks(&self, hooks: Vec<RecordingStartHook>) {
84        *self.recording_start_hooks.lock().unwrap() = hooks;
85    }
86
87    /// Start recording over `shared`: build the recording [`Dial9Handle`], spawn
88    /// the flush thread that drains the bus into `writer`, and own its lifecycle.
89    ///
90    /// The flush-thread control channel is created and owned internally; reach
91    /// the handle via [`handle`](Self::handle).
92    ///
93    /// `thread_init` runs once on the flush thread before the loop and returns
94    /// a teardown closure run after it — use it to register/unregister the
95    /// thread with a runtime's profiler.
96    ///
97    /// Pass `None` for `flush_metrics_sink` to discard flush metrics.
98    pub(crate) fn start<M, Init, Teardown>(
99        shared: Arc<SharedState>,
100        writer: SegmentWriter<M>,
101        flush_metrics_sink: Option<metrique::writer::BoxEntrySink>,
102        thread_init: Init,
103    ) -> Self
104    where
105        M: BufferMode + Send + 'static,
106        Init: FnOnce() -> Teardown + Send + 'static,
107        Teardown: FnOnce(),
108    {
109        let (control_tx, control_rx) = mpsc::sync_channel(1);
110        let handle = Dial9Handle::enabled(shared.clone(), control_tx);
111        let flush_metrics_sink =
112            flush_metrics_sink.unwrap_or_else(metrique::writer::sink::DevNullSink::boxed);
113        let flush_thread = crate::primitives::thread::spawn_named("dial9-flush", move || {
114            // The flush thread is latency-tolerant; lower its priority.
115            #[cfg(target_os = "linux")]
116            // SAFETY: nice() is a simple syscall with no memory-safety
117            // implications; lowering priority is always permitted unprivileged.
118            unsafe {
119                let _ = libc::nice(10);
120            }
121            let teardown = thread_init();
122            run_flush_loop(control_rx, &shared, &flush_metrics_sink, writer);
123            teardown();
124        });
125        Self::new(handle, Some(flush_thread))
126    }
127
128    /// The recording handle for this recorder.
129    pub fn handle(&self) -> &Dial9Handle {
130        &self.handle
131    }
132
133    /// Publish this recorder's handle as the process-global one.
134    ///
135    /// When set, [`Dial9Handle::current`] resolves on every thread in the
136    /// process. When not set, it resolves only on threads a runtime integration
137    /// has installed a handle on.
138    ///
139    /// Returns [`InstallGlobalHandleError`] and changes nothing if another handle
140    /// is already installed: two live globals would split one process's events
141    /// across two traces. A recorder clears its own when it stops, so a later
142    /// install succeeds.
143    ///
144    /// ```no_run
145    /// use dial9_core::buffer::MemoryBuffer;
146    /// use dial9_core::handle::Dial9Handle;
147    /// use dial9_core::recorder::recorder;
148    /// use dial9_trace_format::TraceEvent;
149    ///
150    /// #[derive(TraceEvent)]
151    /// struct Tick {
152    ///     #[traceevent(timestamp)]
153    ///     timestamp_ns: u64,
154    /// }
155    ///
156    /// let rec = recorder(MemoryBuffer::new(1 << 20)?).build();
157    /// rec.install_global_handle()?;
158    ///
159    /// std::thread::spawn(|| {
160    ///     // reachable here, with no handle plumbed in
161    ///     Dial9Handle::current().record_event(Tick { timestamp_ns: 0 });
162    /// });
163    /// # Ok::<_, Box<dyn std::error::Error>>(())
164    /// ```
165    pub fn install_global_handle(&self) -> Result<(), InstallGlobalHandleError> {
166        crate::handle::set_global_handle(self.handle.clone())
167    }
168
169    /// Attach the background worker to this recorder, so its lifecycle is tied
170    /// to the recorder's (drained on `graceful_shutdown`, stopped on drop).
171    #[cfg(feature = "pipeline")]
172    pub(crate) fn attach_worker(&mut self, worker: WorkerHandle) {
173        self.worker = Some(worker);
174    }
175
176    crate::test_util_pub! {
177        /// The shared recording state.
178        fn shared(&self) -> Option<&Arc<SharedState>> {
179            self.handle.shared()
180        }
181    }
182
183    /// Monotonic start time of the recorder in nanoseconds.
184    pub fn start_time(&self) -> Option<u64> {
185        self.shared().map(|s| s.start_time_ns())
186    }
187
188    /// Enable recording.
189    pub fn enable(&self) {
190        self.handle.enable();
191        // Run the one-shot start hooks now that the handle is live and
192        // recording. Draining leaves them run-once across repeated enables.
193        let hooks = std::mem::take(&mut *self.recording_start_hooks.lock().unwrap());
194        for hook in hooks {
195            hook(&self.handle);
196        }
197    }
198
199    /// Disable recording.
200    pub fn disable(&self) {
201        self.handle.disable();
202    }
203
204    /// Flush remaining events, seal the final segment, and join the flush thread.
205    ///
206    /// Call this before dropping any runtime state that owns worker threads, so
207    /// that their thread-local buffers have already been flushed to the central
208    /// collector.
209    pub(crate) fn stop_flush_thread(&mut self) {
210        // Clear the global before the blocking flush below, otherwise other threads
211        // keep resolving it and recording into buffers that nothing will drain.
212        if let Some(shared) = self.handle.shared() {
213            crate::handle::clear_global_handle_for(shared);
214        }
215
216        // Drain the calling thread's local buffer — it won't get a thread-stop
217        // hook, so any unflushed events would be lost otherwise.
218        if let Some(shared) = self.handle.shared() {
219            crate::encoder::drain_to_collector(&shared.collector);
220        }
221
222        // Tell the flush thread to do a final flush + finalize, then exit.
223        let (ack_tx, ack_rx) = mpsc::sync_channel(0);
224        if let Some(tx) = self.handle.control_tx()
225            && tx.send(ControlCommand::FinalizeAndStop(ack_tx)).is_ok()
226        {
227            let _ = ack_rx.recv();
228        }
229        if let Some(t) = self.flush_thread.take() {
230            let _ = t.join();
231        }
232
233        // Stop is permanent from here: recording off, enable() and new
234        // attaches refused. Nothing drains sources once the flush thread is
235        // gone, so release them and whatever they own.
236        if let Some(shared) = self.handle.shared() {
237            shared.mark_stopped();
238            shared.clear_sources();
239        }
240
241        // Runtime threads drop their handle in a thread-stop hook, but the
242        // thread that attached the runtime gets no such hook and would hold a
243        // handle to a stopped recorder for the rest of its life.
244        crate::handle::clear_tl_handle();
245    }
246
247    /// Flush remaining events, seal the final segment, and (with `pipeline`)
248    /// wait for the background worker to drain within `timeout`.
249    ///
250    /// Call this after any runtime that owns worker threads has been dropped, so
251    /// their thread-local buffers have already been flushed. Consumes the
252    /// recorder so `Drop` becomes a no-op.
253    ///
254    /// Failures during draining are logged.
255    pub fn graceful_shutdown(mut self, timeout: Duration) {
256        // `timeout` only bounds the worker drain, which exists under `pipeline`.
257        #[cfg(not(feature = "pipeline"))]
258        let _ = timeout;
259
260        // 1. Flush + finalize the last segment.
261        self.stop_flush_thread();
262
263        // 2. Signal the worker to drain, then join it.
264        #[cfg(feature = "pipeline")]
265        if let Some(w) = &mut self.worker {
266            if let Some(tx) = w.shutdown.take() {
267                let _ = tx.send(timeout);
268            }
269            if let Some(t) = w.thread.take()
270                && let Err(e) = t.join()
271            {
272                tracing::error!(target: "dial9", panic = ?e, "worker thread panicked during shutdown");
273            }
274        }
275    }
276}
277
278impl Drop for Recorder {
279    fn drop(&mut self) {
280        // 1. Flush + finalize. Idempotent, so a prior graceful_shutdown/stop is fine.
281        self.stop_flush_thread();
282
283        // 2. Hard shutdown: drop the sender without sending — the worker sees a
284        // closed channel and exits without draining. For a graceful drain, call
285        // graceful_shutdown() instead.
286        #[cfg(feature = "pipeline")]
287        if let Some(w) = &mut self.worker {
288            w.shutdown.take();
289        }
290    }
291}