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>(
126 notifier: Arc<N>,
127 config: ConfigReloadWorkerConfig,
128 shutdown: CancellationToken,
129 reload: F,
130) -> Result<()>
131where
132 N: ConfigChangeNotifier + ?Sized,
133 F: FnMut(ConfigReloadMessage) -> Fut,
134 Fut: Future<Output = Result<()>>,
135{
136 run_config_reload_worker_with_observer(
137 notifier,
138 config,
139 shutdown,
140 reload,
141 None::<&dyn ConfigReloadObserver>,
142 )
143 .await
144}
145
146pub async fn run_config_reload_worker_with_observer<N, F, Fut>(
148 notifier: Arc<N>,
149 config: ConfigReloadWorkerConfig,
150 shutdown: CancellationToken,
151 mut reload: F,
152 observer: Option<&dyn ConfigReloadObserver>,
153) -> Result<()>
154where
155 N: ConfigChangeNotifier + ?Sized,
156 F: FnMut(ConfigReloadMessage) -> Fut,
157 Fut: Future<Output = Result<()>>,
158{
159 let mut reconcile = || async { Ok(()) };
160 run_config_reload_supervisor_inner(
161 notifier,
162 config,
163 default_config_reload_reconnect_policy(),
164 shutdown,
165 &mut reconcile,
166 &mut reload,
167 observer,
168 None,
169 )
170 .await
171}
172
173pub async fn run_config_reload_supervisor<N, R, RFut, F, Fut>(
187 notifier: Arc<N>,
188 config: ConfigReloadWorkerConfig,
189 shutdown: CancellationToken,
190 reconcile: R,
191 reload: F,
192) -> Result<()>
193where
194 N: ConfigChangeNotifier + ?Sized,
195 R: FnMut() -> RFut,
196 RFut: Future<Output = Result<()>>,
197 F: FnMut(ConfigReloadMessage) -> Fut,
198 Fut: Future<Output = Result<()>>,
199{
200 run_config_reload_supervisor_with_observers(
201 notifier, config, shutdown, reconcile, reload, None, None,
202 )
203 .await
204}
205
206pub async fn run_config_reload_supervisor_with_observers<N, R, RFut, F, Fut>(
208 notifier: Arc<N>,
209 config: ConfigReloadWorkerConfig,
210 shutdown: CancellationToken,
211 mut reconcile: R,
212 mut reload: F,
213 reload_observer: Option<&dyn ConfigReloadObserver>,
214 connection_observer: Option<&dyn ConfigSyncConnectionObserver>,
215) -> Result<()>
216where
217 N: ConfigChangeNotifier + ?Sized,
218 R: FnMut() -> RFut,
219 RFut: Future<Output = Result<()>>,
220 F: FnMut(ConfigReloadMessage) -> Fut,
221 Fut: Future<Output = Result<()>>,
222{
223 run_config_reload_supervisor_inner(
224 notifier,
225 config,
226 default_config_reload_reconnect_policy(),
227 shutdown,
228 &mut reconcile,
229 &mut reload,
230 reload_observer,
231 connection_observer,
232 )
233 .await
234}
235
236#[allow(clippy::too_many_arguments)]
237pub(super) async fn run_config_reload_supervisor_inner<N, R, RFut, F, Fut>(
238 notifier: Arc<N>,
239 config: ConfigReloadWorkerConfig,
240 reconnect_policy: ConfigReloadReconnectPolicy,
241 shutdown: CancellationToken,
242 reconcile: &mut R,
243 reload: &mut F,
244 reload_observer: Option<&dyn ConfigReloadObserver>,
245 connection_observer: Option<&dyn ConfigSyncConnectionObserver>,
246) -> Result<()>
247where
248 N: ConfigChangeNotifier + ?Sized,
249 R: FnMut() -> RFut,
250 RFut: Future<Output = Result<()>>,
251 F: FnMut(ConfigReloadMessage) -> Fut,
252 Fut: Future<Output = Result<()>>,
253{
254 let source = Arc::new(ConfigNotifierSubscriptionSource { notifier });
255 let (updates_tx, mut updates_rx) = mpsc::channel(1);
256 let supervisor = aster_forge_events::supervise_event_subscription(
257 source,
258 reconnect_policy,
259 shutdown.clone(),
260 updates_tx,
261 );
262 tokio::pin!(supervisor);
263
264 loop {
265 let update = tokio::select! {
266 () = shutdown.cancelled() => return Ok(()),
267 () = &mut supervisor => return Ok(()),
268 update = updates_rx.recv() => update,
269 };
270 match update {
271 Some(aster_forge_events::EventSubscriptionUpdate::Connection(observation)) => {
272 observe_config_sync_connection(
273 connection_observer,
274 observation.state,
275 observation.reconnect_attempt,
276 observation.backoff,
277 );
278 match observation.state {
279 ConfigSyncConnectionState::Connected | ConfigSyncConnectionState::Recovered => {
280 if observation.state == ConfigSyncConnectionState::Recovered {
281 tracing::info!(
282 reconnect_attempt = observation.reconnect_attempt,
283 "config reload subscription recovered"
284 );
285 }
286 if let Err(error) = reconcile().await {
287 tracing::warn!(
288 error = %error,
289 "failed to reconcile runtime config after subscription connected"
290 );
291 } else {
292 tracing::debug!(
293 "runtime config reconciled after subscription connected"
294 );
295 }
296 }
297 ConfigSyncConnectionState::Disconnected => {
298 tracing::warn!(
299 reconnect_attempt = observation.reconnect_attempt,
300 "config reload subscription disconnected"
301 );
302 }
303 ConfigSyncConnectionState::Reconnecting => {
304 tracing::warn!(
305 reconnect_attempt = observation.reconnect_attempt,
306 backoff_ms = duration_millis_u64(observation.backoff),
307 "waiting before config reload subscription reconnect"
308 );
309 }
310 }
311 }
312 Some(aster_forge_events::EventSubscriptionUpdate::Item(ConfigChangeEvent::Reload(
313 message,
314 ))) => {
315 process_config_reload_message(&config, message, reload, reload_observer).await;
316 }
317 None => return Ok(()),
318 }
319 }
320}
321
322async fn process_config_reload_message<F, Fut>(
323 config: &ConfigReloadWorkerConfig,
324 message: ConfigReloadMessage,
325 reload: &mut F,
326 observer: Option<&dyn ConfigReloadObserver>,
327) where
328 F: FnMut(ConfigReloadMessage) -> Fut,
329 Fut: Future<Output = Result<()>>,
330{
331 let changed_keys = message.keys.len();
332 let started = Instant::now();
333 match handle_config_reload_notification(config, message, reload).await {
334 Ok(ConfigReloadDecision::Reloaded) => {
335 observe_config_reload(
336 observer,
337 ConfigReloadDecision::Reloaded,
338 "ok",
339 changed_keys,
340 started,
341 );
342 tracing::debug!("runtime config reloaded after remote notification");
343 }
344 Ok(
345 decision @ (ConfigReloadDecision::IgnoredNamespace
346 | ConfigReloadDecision::IgnoredOrigin),
347 ) => {
348 observe_config_reload(observer, decision, "ok", changed_keys, started);
349 }
350 Err(error) => {
351 observe_config_reload(
352 observer,
353 ConfigReloadDecision::Reloaded,
354 "error",
355 changed_keys,
356 started,
357 );
358 tracing::warn!(
359 error = %error,
360 "failed to reload runtime config after remote notification"
361 );
362 }
363 }
364}
365
366#[cfg(test)]
367pub(super) fn config_reload_reconnect_delay(
368 policy: ConfigReloadReconnectPolicy,
369 reconnect_attempt: u32,
370) -> Duration {
371 policy.reconnect_delay(reconnect_attempt)
372}
373
374pub(super) fn duration_millis_u64(duration: Duration) -> u64 {
375 u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
376}
377
378fn observe_config_sync_connection(
379 observer: Option<&dyn ConfigSyncConnectionObserver>,
380 state: ConfigSyncConnectionState,
381 reconnect_attempt: u32,
382 backoff: Duration,
383) {
384 if let Some(observer) = observer {
385 observer.observe_config_sync_connection(ConfigSyncConnectionObservation::new(
386 state,
387 reconnect_attempt,
388 backoff,
389 ));
390 }
391}
392
393fn observe_config_reload(
394 observer: Option<&dyn ConfigReloadObserver>,
395 decision: ConfigReloadDecision,
396 status: &'static str,
397 changed_keys: usize,
398 started: Instant,
399) {
400 if let Some(observer) = observer {
401 observer.observe_config_reload(ConfigReloadObservation::new(
402 "pubsub",
403 decision,
404 status,
405 changed_keys,
406 started.elapsed().as_secs_f64(),
407 ));
408 }
409}
410struct ConfigNotifierSubscriptionSource<N: ?Sized> {
411 notifier: Arc<N>,
412}
413
414#[async_trait]
415impl<N> aster_forge_events::EventSubscriptionSource for ConfigNotifierSubscriptionSource<N>
416where
417 N: ConfigChangeNotifier + ?Sized,
418{
419 type Item = ConfigChangeEvent;
420 type Subscription = ConfigNotification;
421 type Error = ConfigCoreError;
422
423 async fn subscribe(&self) -> Result<Self::Subscription> {
424 self.notifier.subscribe().await
425 }
426
427 async fn receive(&self, subscription: &mut Self::Subscription) -> Result<Self::Item> {
428 subscription.recv().await
429 }
430}