use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use axum::extract::{ConnectInfo, State};
use axum::response::{Html, Redirect};
use axum::routing::get;
use axum::{Extension, Router};
#[cfg(feature = "events")]
use continuo::LifecycleBus;
use continuo::{
Error, Finalizable, Provider, ProviderOrder, Registry, ReloadState, Reloadable, Result,
Runnable, Runtime, SharedState,
};
use tokio_util::sync::CancellationToken;
#[derive(Clone)]
struct AppState(Arc<Inner>);
struct Inner {
shutdown: CancellationToken,
registry: Registry<AppState>,
#[cfg(feature = "events")]
events: LifecycleBus,
}
impl AppState {
fn new() -> Self {
Self(Arc::new(Inner {
shutdown: CancellationToken::new(),
registry: Registry::default(),
#[cfg(feature = "events")]
events: LifecycleBus::new(),
}))
}
}
impl SharedState for AppState {
fn shutdown_token(&self) -> CancellationToken {
self.0.shutdown.clone()
}
fn registry_ref(&self) -> &Registry<Self> {
&self.0.registry
}
#[cfg(feature = "events")]
fn events(&self) -> &LifecycleBus {
&self.0.events
}
}
#[async_trait]
impl ReloadState for AppState {
async fn reload(&self) -> Result<()> {
Ok(())
}
}
const COUNTER_FILE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/counter.txt");
struct CounterService {
hits: AtomicU64,
by_client: Mutex<HashMap<SocketAddr, u64>>,
}
impl CounterService {
fn new() -> Self {
Self { hits: AtomicU64::new(0), by_client: Mutex::new(HashMap::new()) }
}
fn hit(
&self,
client: SocketAddr,
) -> (u64, u64) {
let total = self.hits.fetch_add(1, Ordering::Relaxed) + 1;
let mut clients = self.by_client.lock().expect("client map poisoned");
let yours = clients.entry(client).or_insert(0);
*yours += 1;
(total, *yours)
}
fn total(&self) -> u64 {
self.hits.load(Ordering::Relaxed)
}
}
#[async_trait]
impl Provider<AppState> for CounterService {
fn name(&self) -> &'static str {
"counter"
}
async fn boot(
&self,
_state: &AppState,
) -> Result<()> {
let restored = match tokio::fs::read_to_string(COUNTER_FILE).await {
Ok(text) => {
let n = text.trim().parse::<u64>().unwrap_or(0);
println!("counter booted: starting from {n} (restored from {COUNTER_FILE})");
n
}
Err(_) => {
println!("counter booted: starting fresh from 0 (no {COUNTER_FILE} yet)");
0
}
};
self.hits.store(restored, Ordering::Relaxed);
Ok(())
}
fn as_reloadable(&self) -> Option<&dyn Reloadable<AppState>> {
Some(self)
}
fn as_finalizable(&self) -> Option<&dyn Finalizable<AppState>> {
Some(self)
}
}
#[async_trait]
impl Reloadable<AppState> for CounterService {
async fn reload(
&self,
_state: &AppState,
) -> Result<()> {
self.hits.store(0, Ordering::Relaxed);
self.by_client.lock().expect("client map poisoned").clear();
println!("counter reloaded: reset to 0");
Ok(())
}
}
#[async_trait]
impl Finalizable<AppState> for CounterService {
async fn finalize(
&self,
_state: &AppState,
) -> Result<()> {
let total = self.total();
tokio::fs::write(COUNTER_FILE, total.to_string()).await?;
println!("counter finalize: persisted final value = {total} -> {COUNTER_FILE}");
Ok(())
}
}
struct HttpService {
addr: SocketAddr,
}
impl HttpService {
fn new(addr: SocketAddr) -> Self {
Self { addr }
}
}
#[async_trait]
impl Provider<AppState> for HttpService {
fn name(&self) -> &'static str {
"http"
}
fn order(&self) -> ProviderOrder {
ProviderOrder::new().after::<CounterService>()
}
fn as_runnable(self: Arc<Self>) -> Option<Arc<dyn Runnable<AppState>>> {
Some(self)
}
}
async fn hit(
ConnectInfo(client): ConnectInfo<SocketAddr>,
Extension(counter): Extension<Arc<CounterService>>,
) -> Html<String> {
let (total, yours) = counter.hit(client);
let port = client.port();
println!("http: hit #{total} (client port {port} → its #{yours})");
Html(format!(
"<h1>hit #{total}</h1>\
<p>application scope: the total is shared by every connection.<br>\
your connection (port <code>{port}</code>) is on its hit #{yours}.</p>\
<p><a href=\"/reload\">reset</a> — or from a terminal: \
<code>kill -HUP {}</code></p>",
std::process::id()
))
}
async fn reload(State(app): State<AppState>) -> Redirect {
match app.registry_ref().reload_all(&app).await {
Ok(()) => println!("http: /reload — providers reloaded"),
Err(e) => println!("http: /reload failed: {e}"),
}
Redirect::to("/hit")
}
struct SignalService;
#[async_trait]
impl Provider<AppState> for SignalService {
fn name(&self) -> &'static str {
"signals"
}
fn as_runnable(self: Arc<Self>) -> Option<Arc<dyn Runnable<AppState>>> {
Some(self)
}
}
#[async_trait]
impl Runnable<AppState> for SignalService {
async fn run(
self: Arc<Self>,
state: AppState,
) -> Result<()> {
let pid = std::process::id();
println!("signals: ready (pid {pid})");
println!("signals: reload with `kill -HUP {pid}` or GET /reload — resets the counter");
println!("signals: stop with Ctrl+C or `kill -INT {pid}` — drains, then finalizes");
let token = state.shutdown_token();
let mut hangup = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup())?;
loop {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
println!("signals: Ctrl+C — initiating shutdown");
state.initiate_shutdown();
return Ok(());
}
_ = hangup.recv() => {
println!("signals: SIGHUP — reloading providers");
state.registry_ref().reload_all(&state).await?;
}
_ = token.cancelled() => return Ok(()),
}
}
}
}
#[async_trait]
impl Runnable<AppState> for HttpService {
async fn run(
self: Arc<Self>,
state: AppState,
) -> Result<()> {
let counter = state
.registry_ref()
.resolve::<CounterService>()
.ok_or_else(|| Error::msg("http: CounterService not registered"))?;
let app = Router::new()
.route("/hit", get(hit))
.route("/reload", get(reload))
.layer(Extension(counter))
.with_state(state.clone());
let listener = tokio::net::TcpListener::bind(self.addr).await?;
println!("http listening on http://{}/hit (Ctrl+C to stop)", self.addr);
let token = state.shutdown_token();
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
.with_graceful_shutdown(async move { token.cancelled().await })
.await?;
println!("http drained and stopped");
Ok(())
}
}
#[tokio::main]
async fn main() -> Result<()> {
let state = AppState::new();
let registry = state.registry_ref();
registry
.insert(Arc::new(CounterService::new()))
.insert(Arc::new(HttpService::new(SocketAddr::from(([127, 0, 0, 1], 3000)))))
.insert(Arc::new(SignalService));
registry.boot_all(&state).await?;
registry.validate_all(&state)?;
let mut runtime = Runtime::<AppState>::default();
runtime.spawn_all(state.registry_ref(), state.clone());
runtime.wait_until_shutdown(&state).await?;
runtime.drain().await?;
registry.finalize_all(&state).await?;
println!("🏁 all services finished gracefully");
Ok(())
}