use std::fmt;
use std::{future::Future, sync::Arc, time::Duration};
#[cfg(unix)]
use tokio::signal::unix::{SignalKind, signal};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker;
use tracing::{debug, info, warn};
use crate::runtime::failure::ErrorShutdown;
use crate::runtime::lifecycle::{BoxError, BoxFuture};
use super::service::RegisteredBroker;
use super::{LifecycleHook, RustStream, RustStreamError};
type BoundHook = Box<dyn FnOnce() -> BoxFuture<'static, Result<(), BoxError>> + Send>;
fn bind_hooks<St: Send + Sync + 'static>(
hooks: Vec<LifecycleHook<St>>,
state: &Arc<St>,
) -> Vec<BoundHook> {
hooks
.into_iter()
.map(|hook| {
let state = Arc::clone(state);
Box::new(move || hook(state)) as BoundHook
})
.collect()
}
impl<L: Send, St: Send + Sync + 'static, PP> RustStream<L, St, PP> {
pub async fn run(self) -> Result<(), RustStreamError> {
self.run_until(wait_for_signal()).await
}
pub async fn run_until<F>(self, shutdown: F) -> Result<(), RustStreamError>
where
F: Future<Output = ()> + Send,
{
let running = self.start().await?;
tokio::select! {
() = shutdown => info!(target: "ruststream::lifecycle", "shutdown signal received"),
() = running.stopping() => {
info!(target: "ruststream::lifecycle", "fail-fast shutdown triggered");
}
}
running.shutdown().await
}
pub async fn start(self) -> Result<RunningApp, RustStreamError> {
let Self {
info,
brokers,
starters,
handlers,
state_init,
after_startup,
on_shutdown,
after_shutdown,
shutdown_timeout,
continuations,
..
} = self;
info!(
target: "ruststream::lifecycle",
service = %info.title,
version = %info.version,
brokers = brokers.len(),
subscribers = starters.len(),
"starting service",
);
debug!(target: "ruststream::lifecycle", "producing application state");
let state = state_init().await.map_err(RustStreamError::Startup)?;
let state = Arc::new(state);
for broker in &brokers {
broker
.lifecycle
.connect()
.await
.map_err(RustStreamError::Connect)?;
info!(
target: "ruststream::lifecycle",
broker = broker.label.as_deref().unwrap_or_else(|| broker.lifecycle.name()),
"broker connected",
);
}
let token = CancellationToken::new();
let error_shutdown = ErrorShutdown::new(token.clone());
let mut handles = Vec::with_capacity(starters.len());
for (starter, meta) in starters.into_iter().zip(handlers) {
let handle = starter(state.clone(), error_shutdown.clone(), token.clone())
.await
.map_err(RustStreamError::Subscribe)?;
info!(
target: "ruststream::dispatch",
subscriber = %meta.name,
input = meta.input_type,
"subscriber started",
);
handles.push(handle);
}
if !after_startup.is_empty() {
debug!(target: "ruststream::lifecycle", count = after_startup.len(), "running after_startup hooks");
}
for hook in after_startup {
hook(Arc::clone(&state))
.await
.map_err(RustStreamError::Startup)?;
}
info!(target: "ruststream::lifecycle", subscribers = handles.len(), "service running");
Ok(RunningApp {
token,
error_shutdown,
handles,
on_shutdown: bind_hooks(on_shutdown, &state),
after_shutdown: bind_hooks(after_shutdown, &state),
brokers,
shutdown_timeout,
continuations,
})
}
}
#[must_use = "dropping the handle detaches the service without graceful shutdown"]
pub struct RunningApp {
token: CancellationToken,
error_shutdown: ErrorShutdown,
handles: Vec<JoinHandle<()>>,
on_shutdown: Vec<BoundHook>,
after_shutdown: Vec<BoundHook>,
brokers: Vec<RegisteredBroker>,
shutdown_timeout: Option<Duration>,
continuations: TaskTracker,
}
impl fmt::Debug for RunningApp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RunningApp")
.field("subscribers", &self.handles.len())
.field("brokers", &self.brokers.len())
.field("shutdown_timeout", &self.shutdown_timeout)
.finish_non_exhaustive()
}
}
impl RunningApp {
pub fn stopping(&self) -> impl Future<Output = ()> + Send + 'static {
self.token.clone().cancelled_owned()
}
pub async fn shutdown(self) -> Result<(), RustStreamError> {
let Self {
token,
error_shutdown,
handles,
on_shutdown,
after_shutdown,
brokers,
shutdown_timeout,
continuations,
} = self;
for hook in on_shutdown {
if let Err(err) = hook().await {
warn!(target: "ruststream::lifecycle", error = %err, "on_shutdown hook failed");
}
}
token.cancel();
debug!(target: "ruststream::lifecycle", "draining in-flight handlers");
drain_handles(handles, shutdown_timeout).await?;
drain_continuations(continuations, shutdown_timeout).await;
for broker in brokers.iter().rev() {
broker
.lifecycle
.shutdown()
.await
.map_err(RustStreamError::Shutdown)?;
debug!(
target: "ruststream::lifecycle",
broker = broker.label.as_deref().unwrap_or_else(|| broker.lifecycle.name()),
"broker shut down",
);
}
for hook in after_shutdown {
if let Err(err) = hook().await {
warn!(target: "ruststream::lifecycle", error = %err, "after_shutdown hook failed");
}
}
info!(target: "ruststream::lifecycle", "service stopped");
if let Some(reason) = error_shutdown.taken_failure() {
return Err(RustStreamError::Dispatch(reason));
}
Ok(())
}
}
async fn drain_handles(
handles: Vec<JoinHandle<()>>,
timeout: Option<Duration>,
) -> Result<(), RustStreamError> {
let Some(timeout) = timeout else {
for handle in handles {
handle.await.map_err(RustStreamError::Join)?;
}
return Ok(());
};
let aborts: Vec<_> = handles.iter().map(JoinHandle::abort_handle).collect();
if tokio::time::timeout(timeout, futures::future::join_all(handles))
.await
.is_err()
{
warn!(
target: "ruststream::lifecycle",
"graceful shutdown timed out; aborting in-flight handlers",
);
for abort in aborts {
abort.abort();
}
}
Ok(())
}
async fn drain_continuations(continuations: TaskTracker, timeout: Option<Duration>) {
continuations.close();
if continuations.is_empty() {
return;
}
debug!(target: "ruststream::lifecycle", "draining post-settle continuations");
match timeout {
Some(timeout) => {
if tokio::time::timeout(timeout, continuations.wait())
.await
.is_err()
{
warn!(
target: "ruststream::lifecycle",
"graceful shutdown timed out; abandoning in-flight continuations",
);
}
}
None => continuations.wait().await,
}
}
async fn wait_for_signal() {
#[cfg(unix)]
{
let Ok(mut term) = signal(SignalKind::terminate()) else {
let _ = tokio::signal::ctrl_c().await;
return;
};
tokio::select! {
_ = tokio::signal::ctrl_c() => {}
_ = term.recv() => {}
}
}
#[cfg(not(unix))]
{
let _ = tokio::signal::ctrl_c().await;
}
}