aster_forge_config/notification/
runtime.rs

1use std::future::Future;
2use tokio_util::sync::CancellationToken;
3
4use crate::Result;
5
6use super::message::{ConfigNotificationSource, ConfigReloadMessage, ConfigReloadWorkerConfig};
7use super::notifier::SharedConfigChangeNotifier;
8use super::supervisor::{
9    ConfigReloadObserver, ConfigSyncConnectionObserver, run_config_reload_supervisor,
10    run_config_reload_supervisor_with_observers, run_config_reload_worker,
11    run_config_reload_worker_with_observer,
12};
13
14/// Namespaced runtime handle for cross-process config synchronization.
15///
16/// This type is the product-facing boundary for config sync. It keeps the
17/// namespace, runtime identity, backend notifier, publish helper, and subscription
18/// worker wiring together so product crates only provide their authoritative
19/// reload callback.
20#[derive(Clone)]
21pub struct ConfigSyncRuntime {
22    namespace: String,
23    runtime_id: String,
24    notifier: Option<SharedConfigChangeNotifier>,
25}
26
27impl ConfigSyncRuntime {
28    /// Creates an enabled runtime from a namespace, runtime ID, and notifier.
29    pub fn new(
30        namespace: impl Into<String>,
31        runtime_id: impl Into<String>,
32        notifier: impl Into<SharedConfigChangeNotifier>,
33    ) -> Self {
34        Self {
35            namespace: namespace.into(),
36            runtime_id: runtime_id.into(),
37            notifier: Some(notifier.into()),
38        }
39    }
40
41    /// Creates a disabled runtime with a generated runtime ID.
42    pub fn disabled(namespace: impl Into<String>) -> Self {
43        Self::disabled_with_runtime_id(namespace, aster_forge_utils::id::new_runtime_id())
44    }
45
46    /// Creates a disabled runtime with an explicit runtime ID.
47    pub fn disabled_with_runtime_id(
48        namespace: impl Into<String>,
49        runtime_id: impl Into<String>,
50    ) -> Self {
51        Self {
52            namespace: namespace.into(),
53            runtime_id: runtime_id.into(),
54            notifier: None,
55        }
56    }
57
58    /// Creates a disabled runtime for tests and single-process defaults.
59    pub fn disabled_for_test(namespace: impl Into<String>) -> Self {
60        Self::disabled_with_runtime_id(namespace, "test-runtime")
61    }
62
63    /// Creates an enabled runtime from an explicit notifier for tests.
64    pub fn with_notifier_for_test(
65        namespace: impl Into<String>,
66        runtime_id: impl Into<String>,
67        notifier: impl Into<SharedConfigChangeNotifier>,
68    ) -> Self {
69        Self::new(namespace, runtime_id, notifier)
70    }
71
72    /// Returns the product namespace this runtime accepts and publishes.
73    #[must_use]
74    pub fn namespace(&self) -> &str {
75        &self.namespace
76    }
77
78    /// Returns the process runtime ID.
79    #[must_use]
80    pub fn runtime_id(&self) -> &str {
81        &self.runtime_id
82    }
83
84    /// Returns the configured notifier, if cross-process sync is enabled.
85    #[must_use]
86    pub fn notifier(&self) -> Option<&SharedConfigChangeNotifier> {
87        self.notifier.as_ref()
88    }
89
90    /// Returns whether config sync is enabled.
91    #[must_use]
92    pub fn enabled(&self) -> bool {
93        self.notifier.is_some()
94    }
95
96    /// Converts this runtime into the reload-worker filter configuration.
97    #[must_use]
98    pub fn worker_config(&self) -> ConfigReloadWorkerConfig {
99        ConfigReloadWorkerConfig::new(self.namespace(), self.runtime_id())
100    }
101
102    /// Publishes a reload hint after a local config mutation.
103    ///
104    /// # Errors
105    ///
106    /// Returns [`ConfigError`] when publishing, subscription, reconciliation, or reload handling fails.
107    pub async fn publish_reload(
108        &self,
109        keys: impl IntoIterator<Item = impl Into<String>>,
110        source: ConfigNotificationSource,
111    ) -> Result<()> {
112        let Some(notifier) = self.notifier() else {
113            return Ok(());
114        };
115
116        notifier
117            .publish_reload(ConfigReloadMessage::new(
118                self.namespace(),
119                self.runtime_id(),
120                keys,
121                source,
122            ))
123            .await
124    }
125
126    /// Runs this runtime's reload subscription worker until shutdown.
127    ///
128    /// Disabled runtimes simply wait for shutdown, which lets callers spawn the
129    /// same task unconditionally if that is more convenient.
130    ///
131    /// # Errors
132    ///
133    /// Returns [`ConfigError`] when publishing, subscription, reconciliation, or reload handling fails.
134    pub async fn run_reload_subscription<F, Fut>(
135        &self,
136        shutdown: CancellationToken,
137        reload: F,
138    ) -> Result<()>
139    where
140        F: FnMut(ConfigReloadMessage) -> Fut,
141        Fut: Future<Output = Result<()>>,
142    {
143        let Some(notifier) = self.notifier().cloned() else {
144            shutdown.cancelled().await;
145            return Ok(());
146        };
147        run_config_reload_worker(notifier, self.worker_config(), shutdown, reload).await
148    }
149
150    /// Runs this runtime's reload subscription worker and reports observations.
151    ///
152    /// # Errors
153    ///
154    /// Returns [`ConfigError`] when publishing, subscription, reconciliation, or reload handling fails.
155    pub async fn run_reload_subscription_with_observer<F, Fut>(
156        &self,
157        shutdown: CancellationToken,
158        reload: F,
159        observer: Option<&dyn ConfigReloadObserver>,
160    ) -> Result<()>
161    where
162        F: FnMut(ConfigReloadMessage) -> Fut,
163        Fut: Future<Output = Result<()>>,
164    {
165        let Some(notifier) = self.notifier().cloned() else {
166            shutdown.cancelled().await;
167            return Ok(());
168        };
169        run_config_reload_worker_with_observer(
170            notifier,
171            self.worker_config(),
172            shutdown,
173            reload,
174            observer,
175        )
176        .await
177    }
178
179    /// Runs a reconnecting subscription with an authoritative reconcile callback.
180    ///
181    /// `reconcile` runs after each successful subscription. Product code should
182    /// reload its full snapshot and invalidate all derived configuration caches.
183    ///
184    /// # Errors
185    ///
186    /// Returns [`ConfigError`] when publishing, subscription, reconciliation, or reload handling fails.
187    pub async fn run_reload_subscription_with_reconcile<R, RFut, F, Fut>(
188        &self,
189        shutdown: CancellationToken,
190        reconcile: R,
191        reload: F,
192    ) -> Result<()>
193    where
194        R: FnMut() -> RFut,
195        RFut: Future<Output = Result<()>>,
196        F: FnMut(ConfigReloadMessage) -> Fut,
197        Fut: Future<Output = Result<()>>,
198    {
199        let Some(notifier) = self.notifier().cloned() else {
200            shutdown.cancelled().await;
201            return Ok(());
202        };
203        run_config_reload_supervisor(notifier, self.worker_config(), shutdown, reconcile, reload)
204            .await
205    }
206
207    /// Runs a reconnecting subscription and reports reload and connection observations.
208    ///
209    /// # Errors
210    ///
211    /// Returns [`ConfigError`] when publishing, subscription, reconciliation, or reload handling fails.
212    pub async fn run_reload_subscription_with_reconcile_and_observers<R, RFut, F, Fut>(
213        &self,
214        shutdown: CancellationToken,
215        reconcile: R,
216        reload: F,
217        reload_observer: Option<&dyn ConfigReloadObserver>,
218        connection_observer: Option<&dyn ConfigSyncConnectionObserver>,
219    ) -> Result<()>
220    where
221        R: FnMut() -> RFut,
222        RFut: Future<Output = Result<()>>,
223        F: FnMut(ConfigReloadMessage) -> Fut,
224        Fut: Future<Output = Result<()>>,
225    {
226        let Some(notifier) = self.notifier().cloned() else {
227            shutdown.cancelled().await;
228            return Ok(());
229        };
230        run_config_reload_supervisor_with_observers(
231            notifier,
232            self.worker_config(),
233            shutdown,
234            reconcile,
235            reload,
236            reload_observer,
237            connection_observer,
238        )
239        .await
240    }
241}