From 341e846cfd9d1c1576dd512103ad69269a0c5380 Mon Sep 17 00:00:00 2001 From: Mauro Ezequiel Moltrasio Date: Thu, 27 Aug 2026 17:15:31 +0200 Subject: [PATCH] fix(hotreload): disabling hotreloading preserves watch channels In the context of #1536 I tried to disable hot-reloading configuration and that is currently causing the main `Reloader` object to be dropped and closing all its `watch::Sender`s, which in turn causes all receiver ends to constantly return errors that are ignored by all components listening on them and flooding the logs with messages of configuration being reloaded. This is fixed by keeping the reloader task alive when hot-reloading is disabled, but having it ignore any attempts to reload the configuration. --- fact/src/config/reloader/mod.rs | 11 ++++-- fact/src/config/reloader/tests.rs | 59 +++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/fact/src/config/reloader/mod.rs b/fact/src/config/reloader/mod.rs index cb99b898..a4b04161 100644 --- a/fact/src/config/reloader/mod.rs +++ b/fact/src/config/reloader/mod.rs @@ -40,11 +40,16 @@ impl Reloader { pub fn start(mut self, mut running: watch::Receiver) { if !self.enabled { info!("Configuration hotreload is disabled, changes will require a restart."); - return; } tokio::spawn(async move { - let mut ticker = interval(Duration::from_secs(10)); + let tick = if self.enabled { + Duration::from_secs(10) + } else { + Duration::MAX + }; + let mut ticker = interval(tick); + loop { tokio::select! { _ = ticker.tick() => self.reload(), @@ -225,7 +230,7 @@ impl Reloader { /// Recreate the configuration and notify of changes to any /// subscribers. fn reload(&mut self) { - if !self.update_cache() { + if !self.enabled || !self.update_cache() { return; } diff --git a/fact/src/config/reloader/tests.rs b/fact/src/config/reloader/tests.rs index a5e128b5..edcfb7dd 100644 --- a/fact/src/config/reloader/tests.rs +++ b/fact/src/config/reloader/tests.rs @@ -1,6 +1,7 @@ use std::{ fmt::Debug, net::{IpAddr, Ipv4Addr, SocketAddr}, + time::Duration, }; use crate::config::BackoffConfig; @@ -1417,3 +1418,61 @@ generate_otel_test! { }, None } + +#[tokio::test] +async fn test_reloader_disabled_keeps_channels_alive() { + let config = FactConfig { + hotreload: Some(false), + ..Default::default() + }; + let reloader = Reloader::from(config); + + let mut paths_rx = reloader.paths(); + let mut endpoint_rx = reloader.endpoint(); + let mut grpc_rx = reloader.grpc(); + let mut otel_rx = reloader.otel(); + let mut scan_interval_rx = reloader.scan_interval(); + let mut rate_limit_rx = reloader.rate_limit(); + + let (_, rx) = watch::channel(true); + reloader.start(rx); + + let timeout = Duration::from_millis(50); + + assert!( + tokio::time::timeout(timeout, paths_rx.changed()) + .await + .is_err(), + "paths sender was dropped" + ); + assert!( + tokio::time::timeout(timeout, endpoint_rx.changed()) + .await + .is_err(), + "endpoint sender was dropped" + ); + assert!( + tokio::time::timeout(timeout, grpc_rx.changed()) + .await + .is_err(), + "grpc sender was dropped" + ); + assert!( + tokio::time::timeout(timeout, otel_rx.changed()) + .await + .is_err(), + "otel sender was dropped" + ); + assert!( + tokio::time::timeout(timeout, scan_interval_rx.changed()) + .await + .is_err(), + "scan_interval sender was dropped" + ); + assert!( + tokio::time::timeout(timeout, rate_limit_rx.changed()) + .await + .is_err(), + "rate_limit sender was dropped" + ); +}