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}