| Crates.io | hot_reload |
| lib.rs | hot_reload |
| version | 0.3.5 |
| created_at | 2023-07-03 10:26:42.327427+00 |
| updated_at | 2025-11-02 14:48:20.050895+00 |
| description | Trait and service definition of periodic hot reloader and notifier for config-file, KVS, etc. |
| homepage | |
| repository | https://github.com/junkurihara/rust-hot-reloader |
| max_upload_size | |
| id | 906866 |
| size | 64,383 |
This provides a Rust trait definition and service library for hot-reloading your files, KVS, etc. by periodically checking the system.
Reload Trait DefinitionTo use this library, you need to prepare your own struct implementing reloader::Reload trait, defined as follows:
#[async_trait]
/// Trait defining the responsibility of reloaders to periodically load the target value `V` from `Source`.
/// Source could be a file, a KVS, whatever if you can implement `Reload<V, S>` with `Reload<V, S>::Source`.
/// The generic parameters allow for flexible error handling and value types.
pub trait Reload<V, S = &'static str>
where
V: Eq + PartialEq,
S: Into<std::borrow::Cow<'static, str>> + std::fmt::Display,
{
type Source;
async fn new(src: &Self::Source) -> ReloadResult<Self, V, S>
where
Self: Sized;
async fn reload(&self) -> ReloadResult<Option<V>, V, S>;
}
This trait defines the source type (file, KVS, etc) and reloaded object type V. The generic parameter S allows for flexible error message types. The following is an example of periodic-reloading a config-file through a given file path string.
pub struct ConfigReloader {
pub config_path: PathBuf,
}
#[async_trait]
impl Reload<ServerConfig> for ConfigReloader {
type Source = String;
async fn new(source: &Self::Source) -> ReloadResult<Self, ServerConfig> {
Ok(Self {
config_path: PathBuf::from(source),
})
}
async fn reload(&self) -> ReloadResult<Option<ServerConfig>, ServerConfig> {
let config_str = std::fs::read_to_string(&self.config_path)
.map_err(|e| ReloaderError::Reload(format!("Failed to read config file: {}", e)))?;
let config: ServerConfig = config_object_from_str(config_str)
.map_err(|e| ReloaderError::Reload(format!("Failed to parse config: {}", e)))?;
Ok(Some(config))
}
}
The library supports three monitoring strategies:
notify crate)The ReloaderConfig struct allows you to customize the reloader behavior:
pub struct ReloaderConfig {
/// Period between reload attempts in seconds (used in Polling mode)
pub watch_delay_sec: u32,
/// If true, broadcast updates even when values haven't changed
pub force_reload: bool,
/// Strategy for watching the target value
pub strategy: WatchStrategy,
}
The service provides several convenience methods:
ReloaderService::with_defaults(): Uses default config (10 second polling, no force reload)ReloaderService::with_delay(delay_sec): Custom delay with pollingReloaderService::new(source, config): Full control over configurationStrategies can be set using:
ReloaderConfig::polling(delay_sec): Polling strategyReloaderConfig::realtime(): Realtime strategyReloaderConfig::hybrid(delay_sec): Hybrid strategy (recommended)use hot_reload::*;
// Create reloader service with default configuration (10 second polling)
let (service, mut rx) = ReloaderService::<ConfigReloader, ServerConfig>::with_defaults(&config_path).await.unwrap();
// Start the reloader service in a background task
tokio::spawn(async move { service.start().await });
// Main event loop
loop {
tokio::select! {
// Add main logic of the event loop with up-to-date value
_ = something.happened() => {
// ...
}
// immediately update if watcher detects the change
_ = rx.changed() => {
if let Some(value) = rx.get() {
info!("Received updated value: {:?}", value);
} else {
break; // Service terminated
}
}
else => break
}
}
To use realtime monitoring, implement the RealtimeWatch trait:
use hot_reload::{Reload, RealtimeWatch, RealtimeWatchHandle, WatchEvent};
use notify::{Watcher, RecommendedWatcher, RecursiveMode};
#[async_trait]
impl RealtimeWatch<ServerConfig> for ConfigReloader {
async fn watch_realtime(&self) -> ReloadResult<RealtimeWatchHandle<ServerConfig>, ServerConfig> {
let (tx, rx) = tokio::sync::mpsc::channel(100);
let config_path = self.config_path.clone();
let mut watcher: RecommendedWatcher = notify::recommended_watcher(move |res: notify::Result<Event>| {
// Handle file system events and send via tx
// See server-bin/src/config.rs for complete example
})?;
watcher.watch(&config_path, RecursiveMode::NonRecursive)?;
Ok(RealtimeWatchHandle::with_cleanup(rx, Box::new(watcher)))
}
}
Then use the realtime (or hybrid) strategy:
// Realtime mode
let config = ReloaderConfig::realtime();
let (service, rx) = ReloaderService::new(&config_path, config).await?;
// Or hybrid mode (realtime with automatic polling fallback)
let config = ReloaderConfig::hybrid(10);
let (service, rx) = ReloaderService::new(&config_path, config).await?;
// Use start_with_realtime() for types implementing RealtimeWatch
tokio::spawn(async move { service.start_with_realtime().await });
| Strategy | Latency | Resource Usage | Compatibility | Recommended For |
|---|---|---|---|---|
| Polling | Seconds (configurable) | Low | All data sources | KVS, databases, APIs |
| Realtime | Milliseconds | Medium | File systems (with notify) |
File-based configs |
| Hybrid | Milliseconds (with fallback) | Medium | File systems (with notify) |
Production use, resiliency-critical scenarios |
Note: The core library now includes a built-in FileReloader implementation when the file-reloader feature is enabled (see below). The server-bin example demonstrates additional patterns for custom implementations.
file-reloader)The library provides a ready-to-use FileReloader implementation for file-based data sources with built-in realtime monitoring support.
Add this to your Cargo.toml:
[dependencies]
hot_reload = { version = "0.3", features = ["file-reloader"] }
The FileReloader is a generic implementation that works with any type implementing the required traits:
use hot_reload::{FileReloader, ReloaderService, ReloaderConfig, AsyncFileLoad};
use std::path::Path;
// Implement AsyncFileLoad for your config type
#[async_trait]
impl AsyncFileLoad for ServerConfig {
type Error = anyhow::Error;
async fn async_load_from<T>(path: T) -> Result<Self, Self::Error>
where
T: AsRef<Path> + Send,
{
let content = tokio::fs::read_to_string(path).await?;
let config = toml::from_str(&content)?;
Ok(config)
}
}
// Also implement TryFrom<&PathBuf> for synchronous reload support
impl TryFrom<&PathBuf> for ServerConfig {
type Error = anyhow::Error;
fn try_from(path: &PathBuf) -> Result<Self, Self::Error> {
let content = std::fs::read_to_string(path)?;
let config = toml::from_str(&content)?;
Ok(config)
}
}
// Create and use the FileReloader
let config = ReloaderConfig::hybrid(10);
let (service, mut rx) = ReloaderService::<FileReloader<ServerConfig>, ServerConfig, String>::new(
&config_path,
config,
).await?;
// Start with realtime monitoring
tokio::spawn(async move { service.start_with_realtime().await });
The FileReloader implementation includes:
notify callbacks with async Tokio runtime seamlesslyThe FileReloader uses a sophisticated debouncing algorithm:
This ensures that even when file system events fire multiple times (e.g., write + metadata update), only one reload occurs.
This repository includes a complete working example in the server-bin/ and server-lib/ directories:
server-lib/Server struct and ServerContext for managing application statetokio::select! for handling reloaded valuesentrypoint() and entrypoint_with_realtime() methodsserver-bin/RealtimeWatch trait for ConfigReloader using the notify crate--watch-mode to choose between polling, realtime, and hybrid strategieshybridFileReloader for more complex use casesRunning the example:
# Build and run with hybrid mode (default)
cargo run --package server-bin -- --config config.toml
# Run with specific watch mode
cargo run --package server-bin -- --config config.toml --watch-mode realtime|polling|hybrid
# Try modifying config.toml while the server is running to see hot-reloading in action
See also:
RealtimeWatch implementation