1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
//! A hot-reloadable Actix Web server: edit the file, the next request sees it.
//!
//! ```text
//! cargo run -p dynamic-config --example actix_hello --features watch,json
//!
//! curl localhost:8081/
//! # then edit /tmp/dynamic-config-actix/config.json and curl again
//! ```
//!
//! The same shape as the axum example, and deliberately so: configuration is
//! not application state, it is read where it is used. Actix runs handlers on
//! several worker threads, which makes the point sharper — `current()` is an
//! atomic load, so every worker sees the new snapshot without a lock between
//! them and without anyone being told to reload.
use actix_web::{get, App, HttpResponse, HttpServer, Responder};
use dynamic_config::dynamic_config;
use serde::{Deserialize, Serialize};
const DIRECTORY: &str = "/tmp/dynamic-config-actix";
#[dynamic_config(
files = ["/tmp/dynamic-config-actix/config.json"],
key = "server",
env = "APP_",
watch,
diff,
validate,
)]
#[derive(Debug, Deserialize, Serialize)]
struct ServerConfig {
greeting: String,
port: u16,
/// Applied per request, so a change takes effect without a restart.
max_items: usize,
}
impl ServerConfig {
fn validate(&self) -> Result<(), String> {
if self.max_items == 0 {
return Err("max_items = 0 would serve nothing".to_owned());
}
Ok(())
}
}
#[derive(Serialize)]
struct Greeting {
greeting: String,
items: Vec<usize>,
}
#[get("/")]
async fn hello() -> impl Responder {
// One load per request, reused for the whole handler: two calls could
// straddle a reload and let one response mix two configurations.
let config = ServerConfig::current();
HttpResponse::Ok().json(Greeting {
greeting: config.greeting.clone(),
items: (0..config.max_items).collect(),
})
}
#[get("/config/check")]
async fn check() -> impl Responder {
// Reports what the process *would* load, without loading it — for catching
// a bad edit before it is rolled out.
match ServerConfig::check() {
Ok(report) if report.is_clean() => HttpResponse::Ok().body("ok"),
Ok(report) => HttpResponse::Ok().body(format!("{} unknown keys", report.unknown.len())),
Err(error) => HttpResponse::ServiceUnavailable().body(error.to_string()),
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let directory = std::path::PathBuf::from(DIRECTORY);
std::fs::create_dir_all(&directory)?;
std::fs::write(
directory.join("config.json"),
r#"{
"server": {
"greeting": "hello from dynamic-config",
"port": 8081,
"max_items": 3
}
}
"#,
)?;
ServerConfig::init().map_err(|error| std::io::Error::new(std::io::ErrorKind::Other, error))?;
// Read once, before the watcher starts: a listener cannot move to a new
// port without dropping every connection on the old one, so `port` is
// start-up configuration wearing the same struct as the rest.
let port = ServerConfig::current().port;
// `.detach()` because this watcher should outlive `main`'s body.
ServerConfig::start_watch()?.detach();
println!("listening on http://127.0.0.1:{port}");
println!("edit {DIRECTORY}/config.json and curl again — no restart");
// Note what is *not* here: no `web::Data<ServerConfig>`. Handing a snapshot
// to the app factory would freeze it at start-up, and every worker would
// then serve the configuration that existed when it was built.
HttpServer::new(|| App::new().service(hello).service(check))
.bind(("127.0.0.1", port))?
.run()
.await
}