Skip to main content

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    pub fn namespace(&self) -> &str {
74        &self.namespace
75    }
76
77    /// Returns the process runtime ID.
78    pub fn runtime_id(&self) -> &str {
79        &self.runtime_id
80    }
81
82    /// Returns the configured notifier, if cross-process sync is enabled.
83    pub fn notifier(&self) -> Option<&SharedConfigChangeNotifier> {
84        self.notifier.as_ref()
85    }
86
87    /// Returns whether config sync is enabled.
88    pub fn enabled(&self) -> bool {
89        self.notifier.is_some()
90    }
91
92    /// Converts this runtime into the reload-worker filter configuration.
93    pub fn worker_config(&self) -> ConfigReloadWorkerConfig {
94        ConfigReloadWorkerConfig::new(self.namespace(), self.runtime_id())
95    }
96
97    /// Publishes a reload hint after a local config mutation.
98    pub async fn publish_reload(
99        &self,
100        keys: impl IntoIterator<Item = impl Into<String>>,
101        source: ConfigNotificationSource,
102    ) -> Result<()> {
103        let Some(notifier) = self.notifier() else {
104            return Ok(());
105        };
106
107        notifier
108            .publish_reload(ConfigReloadMessage::new(
109                self.namespace(),
110                self.runtime_id(),
111                keys,
112                source,
113            ))
114            .await
115    }
116
117    /// Runs this runtime's reload subscription worker until shutdown.
118    ///
119    /// Disabled runtimes simply wait for shutdown, which lets callers spawn the
120    /// same task unconditionally if that is more convenient.
121    pub async fn run_reload_subscription<F, Fut>(
122        &self,
123        shutdown: CancellationToken,
124        reload: F,
125    ) -> Result<()>
126    where
127        F: FnMut(ConfigReloadMessage) -> Fut,
128        Fut: Future<Output = Result<()>>,
129    {
130        let Some(notifier) = self.notifier().cloned() else {
131            shutdown.cancelled().await;
132            return Ok(());
133        };
134        run_config_reload_worker(notifier, self.worker_config(), shutdown, reload).await
135    }
136
137    /// Runs this runtime's reload subscription worker and reports observations.
138    pub async fn run_reload_subscription_with_observer<F, Fut>(
139        &self,
140        shutdown: CancellationToken,
141        reload: F,
142        observer: Option<&dyn ConfigReloadObserver>,
143    ) -> Result<()>
144    where
145        F: FnMut(ConfigReloadMessage) -> Fut,
146        Fut: Future<Output = Result<()>>,
147    {
148        let Some(notifier) = self.notifier().cloned() else {
149            shutdown.cancelled().await;
150            return Ok(());
151        };
152        run_config_reload_worker_with_observer(
153            notifier,
154            self.worker_config(),
155            shutdown,
156            reload,
157            observer,
158        )
159        .await
160    }
161
162    /// Runs a reconnecting subscription with an authoritative reconcile callback.
163    ///
164    /// `reconcile` runs after each successful subscription. Product code should
165    /// reload its full snapshot and invalidate all derived configuration caches.
166    pub async fn run_reload_subscription_with_reconcile<R, RFut, F, Fut>(
167        &self,
168        shutdown: CancellationToken,
169        reconcile: R,
170        reload: F,
171    ) -> Result<()>
172    where
173        R: FnMut() -> RFut,
174        RFut: Future<Output = Result<()>>,
175        F: FnMut(ConfigReloadMessage) -> Fut,
176        Fut: Future<Output = Result<()>>,
177    {
178        let Some(notifier) = self.notifier().cloned() else {
179            shutdown.cancelled().await;
180            return Ok(());
181        };
182        run_config_reload_supervisor(notifier, self.worker_config(), shutdown, reconcile, reload)
183            .await
184    }
185
186    /// Runs a reconnecting subscription and reports reload and connection observations.
187    pub async fn run_reload_subscription_with_reconcile_and_observers<R, RFut, F, Fut>(
188        &self,
189        shutdown: CancellationToken,
190        reconcile: R,
191        reload: F,
192        reload_observer: Option<&dyn ConfigReloadObserver>,
193        connection_observer: Option<&dyn ConfigSyncConnectionObserver>,
194    ) -> Result<()>
195    where
196        R: FnMut() -> RFut,
197        RFut: Future<Output = Result<()>>,
198        F: FnMut(ConfigReloadMessage) -> Fut,
199        Fut: Future<Output = Result<()>>,
200    {
201        let Some(notifier) = self.notifier().cloned() else {
202            shutdown.cancelled().await;
203            return Ok(());
204        };
205        run_config_reload_supervisor_with_observers(
206            notifier,
207            self.worker_config(),
208            shutdown,
209            reconcile,
210            reload,
211            reload_observer,
212            connection_observer,
213        )
214        .await
215    }
216}