Skip to main content

aurum_core/
engine.rs

1//! Owned engine boundary for library hosts (JOE-1782 / JOE-1654).
2//!
3//! # Ownership model
4//!
5//! [`AurumEngine`] owns:
6//! * a [`ValidatedConfig`]
7//! * an engine-local [`ResourceGovernor`]
8//! * an engine-local [`Metrics`] sink
9//! * lifecycle bookkeeping for explicit shutdown
10//!
11//! # Residual process-global state (honest)
12//!
13//! Local whisper `WhisperContext` residency and TTS session pools remain
14//! **process-global** in this release line (see `providers::local` and TTS
15//! session code). Multiple engines therefore share model caches. Engine
16//! shutdown does **not** clear the process whisper cache — call
17//! [`crate::providers::local::clear_context_cache`] at process exit when using
18//! Metal, as before.
19//!
20//! Full per-engine model isolation is a follow-up under JOE-1654.
21
22use crate::config::{Config, ValidatedConfig};
23use crate::doctor::{run_doctor, DoctorReport};
24use crate::error::Result;
25use crate::observability::{Metrics, MetricsSnapshot};
26use crate::runtime::{GovernorConfig, ResourceGovernor};
27use crate::support::{build_support_bundle, SupportBundle};
28use std::sync::atomic::{AtomicBool, Ordering};
29use std::sync::Arc;
30
31/// Library-facing engine: validated config + owned governor/metrics.
32pub struct AurumEngine {
33    config: ValidatedConfig,
34    governor: Arc<ResourceGovernor>,
35    metrics: Arc<Metrics>,
36    closed: AtomicBool,
37}
38
39impl std::fmt::Debug for AurumEngine {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        f.debug_struct("AurumEngine")
42            .field("config", &self.config)
43            .field("closed", &self.closed.load(Ordering::SeqCst))
44            .field("metrics", &self.metrics.snapshot())
45            .finish_non_exhaustive()
46    }
47}
48
49impl AurumEngine {
50    /// Build from an already-validated config with default governor settings.
51    pub fn new(config: ValidatedConfig) -> Self {
52        Self::with_governor(config, GovernorConfig::default())
53    }
54
55    /// Build with an explicit governor profile (mobile/server/custom).
56    pub fn with_governor(config: ValidatedConfig, gov: GovernorConfig) -> Self {
57        Self {
58            config,
59            governor: Arc::new(ResourceGovernor::new(gov)),
60            metrics: Arc::new(Metrics::new()),
61            closed: AtomicBool::new(false),
62        }
63    }
64
65    /// Load config from the default file/env path and validate.
66    pub fn load() -> Result<Self> {
67        Ok(Self::new(ValidatedConfig::load()?))
68    }
69
70    /// Load from an explicit config file path (must exist).
71    pub fn load_from_required(path: &std::path::Path) -> Result<Self> {
72        Ok(Self::new(ValidatedConfig::load_from_required(path)?))
73    }
74
75    /// Validate a raw [`Config`] and wrap it.
76    pub fn from_config(cfg: Config) -> Result<Self> {
77        Ok(Self::new(ValidatedConfig::try_from_config(cfg)?))
78    }
79
80    pub fn config(&self) -> &Config {
81        self.config.as_ref()
82    }
83
84    pub fn validated_config(&self) -> &ValidatedConfig {
85        &self.config
86    }
87
88    pub fn governor(&self) -> &Arc<ResourceGovernor> {
89        &self.governor
90    }
91
92    pub fn metrics(&self) -> &Arc<Metrics> {
93        &self.metrics
94    }
95
96    pub fn is_closed(&self) -> bool {
97        self.closed.load(Ordering::SeqCst)
98    }
99
100    /// Mark the engine closed for new high-level work.
101    ///
102    /// Does not clear process-global model caches (see module docs).
103    pub fn shutdown(&self) {
104        self.closed.store(true, Ordering::SeqCst);
105    }
106
107    /// Read-only doctor report using this engine's config.
108    pub fn doctor(&self) -> DoctorReport {
109        run_doctor(self.config.as_ref())
110    }
111
112    /// Privacy-safe support bundle using this engine's config and **engine** metrics.
113    pub fn support_bundle(&self, user_notes: Option<String>) -> SupportBundle {
114        let mut bundle = build_support_bundle(self.config.as_ref(), user_notes);
115        // Prefer engine-local metrics over process-global for multi-engine hosts.
116        bundle.metrics = self.metrics.snapshot();
117        bundle
118            .redaction_notes
119            .push("metrics are engine-local (not process-global)".into());
120        bundle
121    }
122
123    pub fn metrics_snapshot(&self) -> MetricsSnapshot {
124        self.metrics.snapshot()
125    }
126
127    /// Cache directory from validated config.
128    pub fn cache_dir(&self) -> &std::path::Path {
129        &self.config.as_ref().cache_dir
130    }
131}
132
133impl Drop for AurumEngine {
134    fn drop(&mut self) {
135        self.closed.store(true, Ordering::SeqCst);
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn independent_engines_have_independent_metrics() {
145        let a = AurumEngine::load().unwrap();
146        let b = AurumEngine::load().unwrap();
147        a.metrics().record_start();
148        a.metrics()
149            .record_complete(std::time::Duration::from_millis(1));
150        assert_eq!(a.metrics_snapshot().ops_started, 1);
151        assert_eq!(b.metrics_snapshot().ops_started, 0);
152        assert!(!std::ptr::eq(
153            Arc::as_ptr(a.governor()),
154            Arc::as_ptr(b.governor())
155        ));
156    }
157
158    #[test]
159    fn shutdown_flags_closed() {
160        let e = AurumEngine::load().unwrap();
161        assert!(!e.is_closed());
162        e.shutdown();
163        assert!(e.is_closed());
164    }
165
166    #[test]
167    fn doctor_and_support_bundle_work() {
168        let e = AurumEngine::load().unwrap();
169        let d = e.doctor();
170        assert!(!d.checks.is_empty());
171        let b = e.support_bundle(None);
172        assert_eq!(b.schema_version, crate::support::SUPPORT_BUNDLE_VERSION);
173        let json = b.to_json_pretty().unwrap();
174        assert!(json.contains("engine-local"));
175    }
176}