Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions confik/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## Unreleased

- Add hot-reloadable configuration support with `ReloadableConfig` trait and `ReloadingConfig` wrapper. Requires the `reloading` feature (depends on `arc-swap`).
- `ReloadableConfig` trait defines how to build a configuration instance
- `ReloadingConfig<T, F>` provides lock-free atomic configuration swapping
- Optional callbacks via `with_on_update()` for reload notifications
- Optional SIGHUP signal handler via `set_signal_handler()` (requires `signal` feature, depends on `signal-hook`)
- Optional tracing support for logging reload errors (requires `tracing` feature)

## 0.15.1

- Add `confik(crate = ...)` option to `Configuration` macro.
Expand Down
18 changes: 18 additions & 0 deletions confik/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ env = ["dep:envious"]
json = ["dep:serde_json"]
toml = ["dep:toml"]

# Hot reloading
reloading = ["dep:arc-swap"]
signal = ["dep:signal-hook"]
tracing = ["dep:tracing"]

# Destination types
ahash = ["dep:ahash"]
bigdecimal = ["dep:bigdecimal"]
Expand All @@ -45,14 +50,22 @@ uuid = ["dep:uuid"]
[dependencies]
confik-macros = "=0.15.1"

# Always required
cfg-if = "1"
serde = { version = "1", default-features = false, features = ["std", "derive"] }
thiserror = "2"

# Source types
envious = { version = "0.2", optional = true }
serde_json = { version = "1", optional = true }
toml = { version = "0.9", optional = true, default-features = false, features = ["parse", "serde"] }

# Hot reloading
arc-swap = { version = "1", optional = true }
signal-hook = { version = "0.3", optional = true }
tracing = { version = "0.1", optional = true }

# Destination types
ahash = { version = "0.8", optional = true, features = ["serde"] }
bigdecimal = { version = "0.4", optional = true, features = ["serde"] }
bytesize = { version = "2", optional = true, features = ["serde"] }
Expand All @@ -66,6 +79,7 @@ url = { version = "2", optional = true, features = ["serde"] }
uuid = { version = "1", optional = true, features = ["serde"] }

[dev-dependencies]
anyhow = "1"
assert_matches = "1.5"
humantime-serde = "1"
indoc = "2"
Expand All @@ -92,3 +106,7 @@ required-features = ["toml", "bigdecimal"]
[[example]]
name = "ahash"
required-features = ["toml", "ahash"]

[[example]]
name = "reloading"
required-features = ["toml", "reloading"]
126 changes: 126 additions & 0 deletions confik/examples/reloading.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use std::{fs, path::PathBuf, sync::OnceLock};

use confik::{Configuration, FileSource, ReloadableConfig};

#[derive(Configuration, Debug)]
struct ServerConfig {
host: String,
port: u16,
max_connections: usize,
}

static CONFIG_PATH: OnceLock<PathBuf> = OnceLock::new();

impl ReloadableConfig for ServerConfig {
type Error = anyhow::Error;

fn build() -> Result<Self, Self::Error> {
let config = Self::builder()
.override_with(FileSource::new(
CONFIG_PATH
.get()
.expect("CONFIG_PATH not initialized")
.as_path(),
))
.try_build()?;

if config.max_connections > 1_000 {
anyhow::bail!("max_connections must be <= 1000");
}

Ok(config)
}
}

fn main() {
println!("=== Hot-Reloadable Configuration Example ===\n");

// Initialize the OnceLock statics
let config_file = tempfile::Builder::new()
.suffix(".toml")
.tempfile()
.expect("Failed to create temp file");
let config_path = CONFIG_PATH.get_or_init(|| config_file.path().to_path_buf());

// Initialize with the first configuration
fs::write(
config_path,
r#"
host = "localhost"
port = 8080
max_connections = 100
"#,
)
.expect("Failed to write initial config");
println!("Created config file at: {:?}\n", config_path);

// Create a reloading config using the convenient method
let config = ServerConfig::reloading().expect("Failed to load initial config");

// Load and use the current configuration
let current = config.load();
println!("Initial configuration:");
println!(" Host: {}", current.host);
println!(" Port: {}", current.port);
println!(" Max Connections: {}", current.max_connections);

// Add a callback to be notified when config reloads
let config_with_callback = config.with_on_update(|| {
println!("✓ Configuration reloaded successfully!");
});

// Modify the config file
println!("\n--- Modifying the config file ---");
fs::write(
config_path,
r#"
host = "0.0.0.0"
port = 80
max_connections = 10
"#,
)
.expect("Failed to write updated config");
println!("Updated config file with new values");

println!("\n--- Reloading configuration ---");
config_with_callback
.reload()
.expect("Failed to reload config");

// The configuration is atomically updated with new values!
let updated = config_with_callback.load();
println!("After reload:");
println!(" Host: {}", updated.host);
println!(" Port: {}", updated.port);
println!(" Max Connections: {}", updated.max_connections);

// Verify the values actually changed
assert_eq!(updated.host, "0.0.0.0");
assert_eq!(updated.port, 80);
assert_eq!(updated.max_connections, 10);
println!("\n✓ Configuration values changed successfully!");

// Write invalid config
fs::write(
config_path,
r#"
host = "localhost"
port = 8080
max_connections = 99999999
"#,
)
.expect("Failed to write updated config");
println!("Updated config file with new values");

println!("\n--- Reloading configuration ---");
config_with_callback
.reload()
.expect_err("Invalid config rejected");

// Verify the values actually changed
assert_eq!(updated.host, "0.0.0.0");
assert_eq!(updated.port, 80);
assert_eq!(updated.max_connections, 10);

println!("\n=== Example complete ===");
}
68 changes: 68 additions & 0 deletions confik/src/lib.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,74 @@ assert_eq!(
# }
```

## Hot Reloading

Configuration can be made hot-reloadable using the [`ReloadingConfig`] wrapper. This allows you to atomically swap configuration at runtime without restarting your application. Requires the `reloading` feature.

```no_run
# #[cfg(all(feature = "toml", feature = "reloading"))]
# {
use confik::{Configuration, ReloadableConfig, FileSource};

#[derive(Debug, Configuration)]
struct AppConfig {
host: String,
port: u16,
}

impl ReloadableConfig for AppConfig {
type Error = confik::Error;

fn build() -> Result<Self, Self::Error> {
Self::builder()
.override_with(FileSource::new("config.toml"))
.try_build()
}
}

// Create a reloading config
let config = AppConfig::reloading().unwrap();

// Access the current config (cheap, non-blocking)
let current = config.load();
println!("Host: {}", current.host);

// Reload from sources
config.reload().unwrap();

// Add a callback for reload notifications
let config = config.with_on_update(|| {
println!("Config reloaded!");
});
# }
```

### Signal Handling

When the `signal` feature is enabled (requires `reloading`), you can also set up automatic reloading on SIGHUP:

```no_run
# #[cfg(all(feature = "signal", feature = "toml"))]
# {
# use confik::{Configuration, ReloadableConfig, FileSource};
# #[derive(Debug, Configuration)]
# struct AppConfig { host: String, port: u16 }
# impl ReloadableConfig for AppConfig {
# type Error = confik::Error;
# fn build() -> Result<Self, Self::Error> {
# Self::builder().override_with(FileSource::new("config.toml")).try_build()
# }
# }
let config = AppConfig::reloading().unwrap();
let handle = config.set_signal_handler().unwrap();

// Config will now reload when receiving SIGHUP
// Send SIGHUP: kill -HUP <pid>
# }
```

When the `tracing` feature is enabled, reload errors in the signal handler will be automatically logged with `tracing::error!`.

## Sources

A [`Source`] is any type that can create [`ConfigurationBuilder`]s. This crate implements the following sources:
Expand Down
Loading