Skip to main content

audio_graph_bsd/
hot_reload.rs

1//! Hot-reload handle: atomic snapshot swap of a compiled [`Graph`] (Phase C,
2//! ROADMAP ยง5 strategy B).
3//!
4//! Gated behind the `distributed` feature (which brings in `arc-swap`).
5//!
6//! The control thread builds and compiles a NEW [`Graph`], then calls
7//! [`RtHandle::install`] to atomically publish it. The RT thread calls
8//! [`RtHandle::process_cycle`], which performs a wait-free load of the current
9//! graph and runs one cycle. The old graph stays alive (via its `Arc`) until no
10//! RT cycle is using it, then is dropped โ€” so hot-reload is **lossless and
11//! pause-free**.
12//!
13//! # Real-time safety
14//!
15//! `process_cycle` performs only a wait-free [`arc_swap::ArcSwap::load`] (no
16//! allocation, no lock, no syscall) followed by the already-alloc-free
17//! [`Graph::process_cycle`]. The single-RT-thread invariant on
18//! [`crate::GraphScratch`] still holds: the RT thread is the sole mutator of the
19//! loaded graph's scratch.
20
21#![cfg(feature = "distributed")]
22
23use std::sync::Arc;
24
25use arc_swap::ArcSwap;
26
27use crate::{Graph, GraphError};
28use audio_core_bsd::ProcessContext;
29
30/// A real-time handle to a [`Graph`] that can be atomically swapped without
31/// stopping the RT thread.
32///
33/// Wrap a compiled [`Graph`] in an `RtHandle`; the RT thread drives cycles via
34/// [`RtHandle::process_cycle`], and the control thread publishes replacements
35/// via [`RtHandle::install`].
36///
37/// # Example
38///
39/// ```no_run
40/// # use audio_graph_bsd::{Graph, GraphConfig, RtHandle};
41/// # use audio_core_bsd::ProcessContext;
42/// # let compiled_graph: Graph = { /* ...build + compile... */ Graph::new() };
43/// let handle = RtHandle::new(compiled_graph);
44///
45/// // RT thread:
46/// let mut ctx = ProcessContext::new(256, 0, 48_000);
47/// handle.process_cycle(&mut ctx).unwrap();
48///
49/// // Control thread โ€” publish a freshly-built graph without stopping RT:
50/// let new_graph: Graph = { /* ...rebuild + recompile... */ Graph::new() };
51/// handle.install(new_graph);
52/// ```
53pub struct RtHandle {
54    inner: ArcSwap<Graph>,
55}
56
57impl RtHandle {
58    /// Creates a handle owning `graph` (typically already compiled).
59    #[must_use]
60    pub fn new(graph: Graph) -> Self {
61        Self {
62            inner: ArcSwap::from_pointee(graph),
63        }
64    }
65
66    /// Atomically publishes a new `graph`. **Control thread only.**
67    ///
68    /// The previously-installed graph is dropped once no RT cycle holds a guard
69    /// to it (epoch-based reclamation via `arc-swap`).
70    pub fn install(&self, graph: Graph) {
71        self.inner.store(Arc::new(graph));
72    }
73
74    /// Loads the current graph and runs one RT cycle.
75    ///
76    /// Wait-free load (`arc-swap`); the graph's [`Graph::process_cycle`] is
77    /// allocation-free. Returns [`GraphError::NotCompiled`] if the loaded graph
78    /// was never compiled.
79    ///
80    /// # Errors
81    ///
82    /// Propagates [`GraphError`] from [`Graph::process_cycle`].
83    pub fn process_cycle(&self, ctx: &mut ProcessContext) -> Result<(), GraphError> {
84        // Wait-free load returns a Guard<Arc<Graph>>; derefs to &Graph.
85        // process_cycle is &self, so no &mut-from-shared hazard.
86        let guard = self.inner.load();
87        guard.process_cycle(ctx)
88    }
89
90    /// Loads the current graph (wait-free) and returns a guard to it.
91    ///
92    /// Use for diagnostics or to call read-only `&self` accessors on the
93    /// installed graph (e.g. [`Graph::read_output`]) after a cycle. The guard
94    /// keeps the graph alive for the duration of the borrow.
95    #[must_use]
96    pub fn graph(&self) -> arc_swap::Guard<Arc<Graph>> {
97        self.inner.load()
98    }
99}