Reload configuration at runtime¶
Three ways to pick up a config change without restarting, in increasing order of
machinery: call reload() yourself, subscribe to changes, or let the hot-reload
watcher call reload() for you.
Re-read the configuration on demand¶
reload() re-reads every registered source (embedded strings, files and environment
variables) and swaps the result in atomically. Existing snapshots taken with get()
keep the value they had; the next get() returns the new one.
If the parse fails, the stored value is left exactly as it was and the error comes back to you. A config file saved mid-edit cannot drop a running process onto defaults.
Good triggers for an explicit reload: a SIGHUP handler, an admin endpoint, a timer,
or the start of each unit of work in a long-running batch.
React to a change rather than polling¶
let mut rx = cfg.subscribe();
tokio::spawn(async move {
loop {
if rx.changed().await.is_err() {
break; // the Config was dropped
}
let current = rx.borrow().clone();
apply_log_level(¤t.log_level);
}
});
rx.borrow() is valid immediately, because the receiver holds the current value before
any reload happens, and changed().await resolves after each successful reload. A
failed reload wakes nobody.
Two properties worth designing around: subscribing late is safe (a new receiver sees the newest value, not the start-up one), and the channel keeps only the latest value, so two reloads in quick succession may wake a subscriber once. You are told that configuration changed, not which key changed.
Reload automatically when the file changes¶
Enable the feature:
Then start the watcher and keep the handle alive:
let handle = cfg.watch_files()?; // dropping this stops the watcher
// ... run the service ...
drop(handle); // stops watching
The watcher follows the directory of every path given to user_file, coalesces
file-system events over a fixed 250 ms window, and calls reload() for each batch that
touches one of those files. Subscribers wake on each successful reload. A reload that
fails is swallowed, so a bad save does not kill the watcher.
watch_files returns ConfigError::Watch if no user_file was registered, or if a
registered file's directory does not exist. The file itself may appear later.
Back the watcher up when a missed change matters¶
The watcher sees in-place writes, rename-replace saves and symlink swaps (see which file changes the watcher sees), but it is not a delivery guarantee: an OS event queue can overflow, and a reload that fails to parse is swallowed. If your tool must not miss a change, re-read on a timer as well:
let _handle = cfg.watch_files()?;
tokio::spawn({
let cfg = cfg.clone();
async move {
let mut tick = tokio::time::interval(std::time::Duration::from_secs(30));
loop {
tick.tick().await;
let _ = cfg.reload();
}
}
});
Config is Clone and clones share the same stored value and subscriber channel, so
the timer task and the rest of the process see the same configuration.
Decide what is safe to reload¶
Reloading changes the value your code reads next. It does not re-run anything you did with the old value at start-up, so a setting only takes effect on reload if something re-reads it:
| Setting | Reloadable? |
|---|---|
| Log level, timeouts, feature flags, rate limits | Yes, if read per unit of work |
| Connection-pool size | Only if something resizes the pool on change |
| Listen address, port | No, the socket is already bound |
For the middle row, subscribe and act. For the last, treat the change as requiring a restart and say so in your tool's own documentation.
Next¶
- Why configuration is a snapshot, not a shared value
- Cargo features: what
hot-reloadpulls in