feat(config): configurable restart_apply_mode + graceful shutdown (Stage 3b)

- console section: restart_apply_mode (manual | auto), an operator setting (runtime-safe,
  DB-overridable via /admin/config), env RESTART_APPLY_MODE default manual.
- Graceful shutdown: main serve now awaits SIGTERM/Ctrl-C OR an in-app shutdown signal
  (AppState.shutdown Notify) via with_graceful_shutdown, draining in-flight requests.
- PUT /admin/config in `auto` mode self-restarts shortly after responding to apply a
  restart-required change (systemd Restart=always brings it back with the new config);
  `manual` just flags restart_pending. The console warns before an auto save.

Stage 3 complete: operator config is readable + editable through the real validated,
audited, precedence-based layer; runtime-safe edits apply live, infra edits restart.
cargo all-targets clean, lib tests pass.
This commit is contained in:
jwinterm
2026-07-18 14:08:33 -04:00
parent 22d362b62f
commit a80fa46daf
6 changed files with 130 additions and 1 deletions
+28
View File
@@ -118,6 +118,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
web_challenges: Arc::default(),
prices: prices_cache,
admin_sessions,
shutdown: Arc::new(tokio::sync::Notify::new()),
});
// Periodic GC of expired website-auth challenges. Bounds the single-node
@@ -394,6 +395,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
});
}
let shutdown = state.shutdown.clone();
let app = build_router(state);
let listener = tokio::net::TcpListener::bind(&addr).await?;
@@ -403,6 +405,32 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
listener,
app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
)
.with_graceful_shutdown(shutdown_signal(shutdown))
.await?;
Ok(())
}
/// Completes on SIGTERM / Ctrl-C, or when the app signals a restart (an operator config
/// change in `auto` mode). Lets axum drain in-flight requests before exit; systemd
/// `Restart=always` then brings the instance back with the new config.
async fn shutdown_signal(app_triggered: Arc<tokio::sync::Notify>) {
let ctrl_c = async {
let _ = tokio::signal::ctrl_c().await;
};
#[cfg(unix)]
let term = async {
if let Ok(mut sig) =
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
{
sig.recv().await;
}
};
#[cfg(not(unix))]
let term = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {}
_ = term => {}
_ = app_triggered.notified() => {}
}
tracing::info!("shutdown signal received; draining in-flight requests");
}