1use async_trait::async_trait;
2use std::future::Future;
3use std::sync::Arc;
4use std::time::{Duration, Instant};
5use tokio::sync::mpsc;
6use tokio_util::sync::CancellationToken;
7
8use crate::{ConfigCoreError, Result};
9
10use super::message::{
11 ConfigChangeEvent, ConfigReloadDecision, ConfigReloadMessage, ConfigReloadWorkerConfig,
12 handle_config_reload_notification,
13};
14use super::notifier::{ConfigChangeNotifier, ConfigNotification};
15
16#[derive(Debug, Clone, PartialEq)]
18pub struct ConfigReloadObservation {
19 pub source: &'static str,
21 pub decision: ConfigReloadDecision,
23 pub status: &'static str,
25 pub changed_keys: u64,
27 pub duration_seconds: f64,
29}
30
31impl ConfigReloadObservation {
32 fn new(
33 source: &'static str,
34 decision: ConfigReloadDecision,
35 status: &'static str,
36 changed_keys: usize,
37 duration_seconds: f64,
38 ) -> Self {
39 Self {
40 source,
41 decision,
42 status,
43 changed_keys: u64::try_from(changed_keys).unwrap_or(u64::MAX),
44 duration_seconds,
45 }
46 }
47}
48
49pub type ConfigSyncConnectionState = aster_forge_events::EventConnectionState;
51
52#[derive(Debug, Clone, PartialEq)]
54pub struct ConfigSyncConnectionObservation {
55 pub state: ConfigSyncConnectionState,
57 pub reconnect_attempt: u32,
59 pub backoff_seconds: f64,
61}
62
63impl ConfigSyncConnectionObservation {
64 fn new(state: ConfigSyncConnectionState, reconnect_attempt: u32, backoff: Duration) -> Self {
65 Self {
66 state,
67 reconnect_attempt,
68 backoff_seconds: backoff.as_secs_f64(),
69 }
70 }
71}
72
73pub trait ConfigReloadObserver: Send + Sync {
75 fn observe_config_reload(&self, observation: ConfigReloadObservation);
77}
78
79impl<F> ConfigReloadObserver for F
80where
81 F: Fn(ConfigReloadObservation) + Send + Sync,
82{
83 fn observe_config_reload(&self, observation: ConfigReloadObservation) {
84 self(observation);
85 }
86}
87
88pub trait ConfigSyncConnectionObserver: Send + Sync {
90 fn observe_config_sync_connection(&self, observation: ConfigSyncConnectionObservation);
92}
93
94impl<F> ConfigSyncConnectionObserver for F
95where
96 F: Fn(ConfigSyncConnectionObservation) + Send + Sync,
97{
98 fn observe_config_sync_connection(&self, observation: ConfigSyncConnectionObservation) {
99 self(observation);
100 }
101}
102
103pub(super) type ConfigReloadReconnectPolicy = aster_forge_events::EventReconnectPolicy;
104
105fn default_config_reload_reconnect_policy() -> ConfigReloadReconnectPolicy {
106 ConfigReloadReconnectPolicy {
107 initial_delay: Duration::from_millis(250),
108 max_delay: Duration::from_secs(30),
109 stable_reset_after: Duration::from_secs(30),
110 jitter_min_percent: 50,
111 jitter_max_percent: 100,
112 }
113}
114
115pub async fn run_config_reload_worker<N, F, Fut>(
130 notifier: Arc<N>,
131 config: ConfigReloadWorkerConfig,
132 shutdown: CancellationToken,
133 reload: F,
134) -> Result<()>
135where
136 N: ConfigChangeNotifier + ?Sized,
137 F: FnMut(ConfigReloadMessage) -> Fut,
138 Fut: Future<Output = Result<()>>,
139{
140 run_config_reload_worker_with_observer(
141 notifier,
142 config,
143 shutdown,
144 reload,
145 None::<&dyn ConfigReloadObserver>,
146 )
147 .await
148}
149
150pub async fn run_config_reload_worker_with_observer<N, F, Fut>(
156 notifier: Arc<N>,
157 config: ConfigReloadWorkerConfig,
158 shutdown: CancellationToken,
159 mut reload: F,
160 observer: Option<&dyn ConfigReloadObserver>,
161) -> Result<()>
162where
163 N: ConfigChangeNotifier + ?Sized,
164 F: FnMut(ConfigReloadMessage) -> Fut,
165 Fut: Future<Output = Result<()>>,
166{
167 let mut reconcile = || async { Ok(()) };
168 run_config_reload_supervisor_inner(
169 notifier,
170 config,
171 default_config_reload_reconnect_policy(),
172 shutdown,
173 &mut reconcile,
174 &mut reload,
175 observer,
176 None,
177 )
178 .await
179}
180
181pub async fn run_config_reload_supervisor<N, R, RFut, F, Fut>(
199 notifier: Arc<N>,
200 config: ConfigReloadWorkerConfig,
201 shutdown: CancellationToken,
202 reconcile: R,
203 reload: F,
204) -> Result<()>
205where
206 N: ConfigChangeNotifier + ?Sized,
207 R: FnMut() -> RFut,
208 RFut: Future<Output = Result<()>>,
209 F: FnMut(ConfigReloadMessage) -> Fut,
210 Fut: Future<Output = Result<()>>,
211{
212 run_config_reload_supervisor_with_observers(
213 notifier, config, shutdown, reconcile, reload, None, None,
214 )
215 .await
216}
217
218pub async fn run_config_reload_supervisor_with_observers<N, R, RFut, F, Fut>(
224 notifier: Arc<N>,
225 config: ConfigReloadWorkerConfig,
226 shutdown: CancellationToken,
227 mut reconcile: R,
228 mut reload: F,
229 reload_observer: Option<&dyn ConfigReloadObserver>,
230 connection_observer: Option<&dyn ConfigSyncConnectionObserver>,
231) -> Result<()>
232where
233 N: ConfigChangeNotifier + ?Sized,
234 R: FnMut() -> RFut,
235 RFut: Future<Output = Result<()>>,
236 F: FnMut(ConfigReloadMessage) -> Fut,
237 Fut: Future<Output = Result<()>>,
238{
239 run_config_reload_supervisor_inner(
240 notifier,
241 config,
242 default_config_reload_reconnect_policy(),
243 shutdown,
244 &mut reconcile,
245 &mut reload,
246 reload_observer,
247 connection_observer,
248 )
249 .await
250}
251
252#[expect(
253 clippy::too_many_arguments,
254 reason = "The supervisor accepts explicit notifier, retry, reload, cancellation, and observability dependencies."
255)]
256pub(super) async fn run_config_reload_supervisor_inner<N, R, RFut, F, Fut>(
257 notifier: Arc<N>,
258 config: ConfigReloadWorkerConfig,
259 reconnect_policy: ConfigReloadReconnectPolicy,
260 shutdown: CancellationToken,
261 reconcile: &mut R,
262 reload: &mut F,
263 reload_observer: Option<&dyn ConfigReloadObserver>,
264 connection_observer: Option<&dyn ConfigSyncConnectionObserver>,
265) -> Result<()>
266where
267 N: ConfigChangeNotifier + ?Sized,
268 R: FnMut() -> RFut,
269 RFut: Future<Output = Result<()>>,
270 F: FnMut(ConfigReloadMessage) -> Fut,
271 Fut: Future<Output = Result<()>>,
272{
273 let source = Arc::new(ConfigNotifierSubscriptionSource { notifier });
274 let (updates_tx, mut updates_rx) = mpsc::channel(1);
275 let supervisor = aster_forge_events::supervise_event_subscription(
276 source,
277 reconnect_policy,
278 shutdown.clone(),
279 updates_tx,
280 );
281 tokio::pin!(supervisor);
282
283 loop {
284 let update = tokio::select! {
285 () = shutdown.cancelled() => return Ok(()),
286 () = &mut supervisor => return Ok(()),
287 update = updates_rx.recv() => update,
288 };
289 match update {
290 Some(aster_forge_events::EventSubscriptionUpdate::Connection(observation)) => {
291 observe_config_sync_connection(
292 connection_observer,
293 observation.state,
294 observation.reconnect_attempt,
295 observation.backoff,
296 );
297 match observation.state {
298 ConfigSyncConnectionState::Connected | ConfigSyncConnectionState::Recovered => {
299 if observation.state == ConfigSyncConnectionState::Recovered {
300 tracing::info!(
301 reconnect_attempt = observation.reconnect_attempt,
302 "config reload subscription recovered"
303 );
304 }
305 if let Err(error) = reconcile().await {
306 tracing::warn!(
307 error = %error,
308 "failed to reconcile runtime config after subscription connected"
309 );
310 } else {
311 tracing::debug!(
312 "runtime config reconciled after subscription connected"
313 );
314 }
315 }
316 ConfigSyncConnectionState::Disconnected => {
317 tracing::warn!(
318 reconnect_attempt = observation.reconnect_attempt,
319 "config reload subscription disconnected"
320 );
321 }
322 ConfigSyncConnectionState::Reconnecting => {
323 tracing::warn!(
324 reconnect_attempt = observation.reconnect_attempt,
325 backoff_ms = duration_millis_u64(observation.backoff),
326 "waiting before config reload subscription reconnect"
327 );
328 }
329 }
330 }
331 Some(aster_forge_events::EventSubscriptionUpdate::Item(ConfigChangeEvent::Reload(
332 message,
333 ))) => {
334 process_config_reload_message(&config, message, reload, reload_observer).await;
335 }
336 None => return Ok(()),
337 }
338 }
339}
340
341async fn process_config_reload_message<F, Fut>(
342 config: &ConfigReloadWorkerConfig,
343 message: ConfigReloadMessage,
344 reload: &mut F,
345 observer: Option<&dyn ConfigReloadObserver>,
346) where
347 F: FnMut(ConfigReloadMessage) -> Fut,
348 Fut: Future<Output = Result<()>>,
349{
350 let changed_keys = message.keys.len();
351 let started = Instant::now();
352 match handle_config_reload_notification(config, message, reload).await {
353 Ok(ConfigReloadDecision::Reloaded) => {
354 observe_config_reload(
355 observer,
356 ConfigReloadDecision::Reloaded,
357 "ok",
358 changed_keys,
359 started,
360 );
361 tracing::debug!("runtime config reloaded after remote notification");
362 }
363 Ok(
364 decision @ (ConfigReloadDecision::IgnoredNamespace
365 | ConfigReloadDecision::IgnoredOrigin),
366 ) => {
367 observe_config_reload(observer, decision, "ok", changed_keys, started);
368 }
369 Err(error) => {
370 observe_config_reload(
371 observer,
372 ConfigReloadDecision::Reloaded,
373 "error",
374 changed_keys,
375 started,
376 );
377 tracing::warn!(
378 error = %error,
379 "failed to reload runtime config after remote notification"
380 );
381 }
382 }
383}
384
385#[cfg(test)]
386pub(super) fn config_reload_reconnect_delay(
387 policy: ConfigReloadReconnectPolicy,
388 reconnect_attempt: u32,
389) -> Duration {
390 policy.reconnect_delay(reconnect_attempt)
391}
392
393pub(super) fn duration_millis_u64(duration: Duration) -> u64 {
394 u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
395}
396
397fn observe_config_sync_connection(
398 observer: Option<&dyn ConfigSyncConnectionObserver>,
399 state: ConfigSyncConnectionState,
400 reconnect_attempt: u32,
401 backoff: Duration,
402) {
403 if let Some(observer) = observer {
404 observer.observe_config_sync_connection(ConfigSyncConnectionObservation::new(
405 state,
406 reconnect_attempt,
407 backoff,
408 ));
409 }
410}
411
412fn observe_config_reload(
413 observer: Option<&dyn ConfigReloadObserver>,
414 decision: ConfigReloadDecision,
415 status: &'static str,
416 changed_keys: usize,
417 started: Instant,
418) {
419 if let Some(observer) = observer {
420 observer.observe_config_reload(ConfigReloadObservation::new(
421 "pubsub",
422 decision,
423 status,
424 changed_keys,
425 started.elapsed().as_secs_f64(),
426 ));
427 }
428}
429struct ConfigNotifierSubscriptionSource<N: ?Sized> {
430 notifier: Arc<N>,
431}
432
433#[async_trait]
434impl<N> aster_forge_events::EventSubscriptionSource for ConfigNotifierSubscriptionSource<N>
435where
436 N: ConfigChangeNotifier + ?Sized,
437{
438 type Item = ConfigChangeEvent;
439 type Subscription = ConfigNotification;
440 type Error = ConfigCoreError;
441
442 async fn subscribe(&self) -> Result<Self::Subscription> {
443 self.notifier.subscribe().await
444 }
445
446 async fn receive(&self, subscription: &mut Self::Subscription) -> Result<Self::Item> {
447 subscription.recv().await
448 }
449}